/* GStreamer
 * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <string.h>

#include "audio.h"

#include <gst/gststructure.h>

#ifndef GST_DISABLE_GST_DEBUG
#define GST_CAT_DEFAULT ensure_debug_category()
static GstDebugCategory *
ensure_debug_category (void)
{
  static gsize cat_gonce = 0;

  if (g_once_init_enter (&cat_gonce)) {
    gsize cat_done;

    cat_done = (gsize) _gst_debug_category_new ("audio-info", 0,
        "audio-info object");

    g_once_init_leave (&cat_gonce, cat_done);
  }

  return (GstDebugCategory *) cat_gonce;
}
#else
#define ensure_debug_category() /* NOOP */
#endif /* GST_DISABLE_GST_DEBUG */


/**
 * gst_audio_info_copy:
 * @info: a #GstAudioInfo
 *
 * Copy a GstAudioInfo structure.
 *
 * Returns: a new #GstAudioInfo. free with gst_audio_info_free.
 */
GstAudioInfo *
gst_audio_info_copy (const GstAudioInfo * info)
{
  return g_slice_dup (GstAudioInfo, info);
}

/**
 * gst_audio_info_free:
 * @info: a #GstAudioInfo
 *
 * Free a GstAudioInfo structure previously allocated with gst_audio_info_new()
 * or gst_audio_info_copy().
 */
void
gst_audio_info_free (GstAudioInfo * info)
{
  g_slice_free (GstAudioInfo, info);
}

G_DEFINE_BOXED_TYPE (GstAudioInfo, gst_audio_info,
    (GBoxedCopyFunc) gst_audio_info_copy, (GBoxedFreeFunc) gst_audio_info_free);

/**
 * gst_audio_info_new:
 *
 * Allocate a new #GstAudioInfo that is also initialized with
 * gst_audio_info_init().
 *
 * Returns: a new #GstAudioInfo. free with gst_audio_info_free().
 */
GstAudioInfo *
gst_audio_info_new (void)
{
  GstAudioInfo *info;

  info = g_slice_new (GstAudioInfo);
  gst_audio_info_init (info);

  return info;
}

/**
 * gst_audio_info_init:
 * @info: a #GstAudioInfo
 *
 * Initialize @info with default values.
 */
void
gst_audio_info_init (GstAudioInfo * info)
{
  g_return_if_fail (info != NULL);

  memset (info, 0, sizeof (GstAudioInfo));

  info->finfo = gst_audio_format_get_info (GST_AUDIO_FORMAT_UNKNOWN);
}

/**
 * gst_audio_info_set_format:
 * @info: a #GstAudioInfo
 * @format: the format
 * @rate: the samplerate
 * @channels: the number of channels
 * @position: (array fixed-size=64) (nullable): the channel positions
 *
 * Set the default info for the audio info of @format and @rate and @channels.
 *
 * Note: This initializes @info first, no values are preserved.
 */
void
gst_audio_info_set_format (GstAudioInfo * info, GstAudioFormat format,
    gint rate, gint channels, const GstAudioChannelPosition * position)
{
  const GstAudioFormatInfo *finfo;
  gint i;

  g_return_if_fail (info != NULL);
  g_return_if_fail (format != GST_AUDIO_FORMAT_UNKNOWN);
  g_return_if_fail (channels <= 64 || position == NULL);

  gst_audio_info_init (info);

  finfo = gst_audio_format_get_info (format);

  info->flags = 0;
  info->layout = GST_AUDIO_LAYOUT_INTERLEAVED;
  info->finfo = finfo;
  info->rate = rate;
  info->channels = channels;
  info->bpf = (finfo->width * channels) / 8;

  memset (&info->position, 0xff, sizeof (info->position));

  if (!position && channels == 1) {
    info->position[0] = GST_AUDIO_CHANNEL_POSITION_MONO;
    return;
  } else if (!position && channels == 2) {
    info->position[0] = GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT;
    info->position[1] = GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT;
    return;
  } else {
    if (!position
        || !gst_audio_check_valid_channel_positions (position, channels,
            TRUE)) {
      if (position)
        g_warning ("Invalid channel positions");
    } else {
      memcpy (&info->position, position,
          info->channels * sizeof (info->position[0]));
      if (info->position[0] == GST_AUDIO_CHANNEL_POSITION_NONE)
        info->flags |= GST_AUDIO_FLAG_UNPOSITIONED;
      return;
    }
  }

  /* Otherwise a NONE layout */
  info->flags |= GST_AUDIO_FLAG_UNPOSITIONED;
  for (i = 0; i < MIN (64, channels); i++)
    info->position[i] = GST_AUDIO_CHANNEL_POSITION_NONE;
}

/**
 * gst_audio_info_from_caps:
 * @info: a #GstAudioInfo
 * @caps: a #GstCaps
 *
 * Parse @caps and update @info.
 *
 * Returns: TRUE if @caps could be parsed
 */
gboolean
gst_audio_info_from_caps (GstAudioInfo * info, const GstCaps * caps)
{
  GstStructure *str;
  const gchar *s;
  GstAudioFormat format = GST_AUDIO_FORMAT_UNKNOWN;
  gint rate = 0;
  gint channels = 0;
  guint64 channel_mask = 0;
  gint i;
  GstAudioChannelPosition position[64];
  GstAudioFlags flags;
  GstAudioLayout layout = GST_AUDIO_LAYOUT_INTERLEAVED;

  g_return_val_if_fail (info != NULL, FALSE);
  g_return_val_if_fail (caps != NULL, FALSE);
  g_return_val_if_fail (gst_caps_is_fixed (caps), FALSE);

  GST_DEBUG ("parsing caps %" GST_PTR_FORMAT, caps);

  flags = 0;

  str = gst_caps_get_structure (caps, 0);

  if (gst_structure_has_name (str, "audio/x-raw")) {
    if (!(s = gst_structure_get_string (str, "format")))
      goto no_format;

    format = gst_audio_format_from_string (s);
    if (format == GST_AUDIO_FORMAT_UNKNOWN)
      goto unknown_format;
  } else if (g_str_has_prefix (gst_structure_get_name (str), "audio/")) {
    format = GST_AUDIO_FORMAT_ENCODED;
  } else {
    goto wrong_name;
  }

  if (format != GST_AUDIO_FORMAT_ENCODED) {
    if (!(s = gst_structure_get_string (str, "layout")))
      goto no_layout;
    if (g_str_equal (s, "interleaved"))
      layout = GST_AUDIO_LAYOUT_INTERLEAVED;
    else if (g_str_equal (s, "non-interleaved"))
      layout = GST_AUDIO_LAYOUT_NON_INTERLEAVED;
    else
      goto unknown_layout;
  }

  if (!gst_structure_get_int (str, "rate", &rate) &&
      format != GST_AUDIO_FORMAT_ENCODED)
    goto no_rate;
  if (!gst_structure_get_int (str, "channels", &channels) &&
      format != GST_AUDIO_FORMAT_ENCODED)
    goto no_channels;

  if (!gst_structure_get (str, "channel-mask", GST_TYPE_BITMASK, &channel_mask,
          NULL) || (channel_mask == 0 && channels == 1)) {
    if (channels == 1) {
      position[0] = GST_AUDIO_CHANNEL_POSITION_MONO;
    } else if (channels == 2) {
      position[0] = GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT;
      position[1] = GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT;
    } else if (format != GST_AUDIO_FORMAT_ENCODED) {
      goto no_channel_mask;
    }
  } else if (channel_mask == 0) {
    flags |= GST_AUDIO_FLAG_UNPOSITIONED;
    for (i = 0; i < MIN (64, channels); i++)
      position[i] = GST_AUDIO_CHANNEL_POSITION_NONE;
  } else {
    if (!gst_audio_channel_positions_from_mask (channels, channel_mask,
            position))
      goto invalid_channel_mask;
  }

  gst_audio_info_set_format (info, format, rate, channels,
      (channels > 64) ? NULL : position);

  info->flags = flags;
  info->layout = layout;

  return TRUE;

  /* ERROR */
wrong_name:
  {
    GST_ERROR ("wrong name, expected audio/x-raw");
    return FALSE;
  }
no_format:
  {
    GST_ERROR ("no format given");
    return FALSE;
  }
unknown_format:
  {
    GST_ERROR ("unknown format given");
    return FALSE;
  }
no_layout:
  {
    GST_ERROR ("no layout given");
    return FALSE;
  }
unknown_layout:
  {
    GST_ERROR ("unknown layout given");
    return FALSE;
  }
no_rate:
  {
    GST_ERROR ("no rate property given");
    return FALSE;
  }
no_channels:
  {
    GST_ERROR ("no channels property given");
    return FALSE;
  }
no_channel_mask:
  {
    GST_ERROR ("no channel-mask property given");
    return FALSE;
  }
invalid_channel_mask:
  {
    GST_ERROR ("Invalid channel mask 0x%016" G_GINT64_MODIFIER
        "x for %d channels", channel_mask, channels);
    return FALSE;
  }
}

/**
 * gst_audio_info_to_caps:
 * @info: a #GstAudioInfo
 *
 * Convert the values of @info into a #GstCaps.
 *
 * Returns: (transfer full): the new #GstCaps containing the
 *          info of @info.
 */
GstCaps *
gst_audio_info_to_caps (const GstAudioInfo * info)
{
  GstCaps *caps;
  const gchar *format;
  const gchar *layout;
  GstAudioFlags flags;

  g_return_val_if_fail (info != NULL, NULL);
  g_return_val_if_fail (info->finfo != NULL, NULL);
  g_return_val_if_fail (info->finfo->format != GST_AUDIO_FORMAT_UNKNOWN, NULL);

  format = gst_audio_format_to_string (info->finfo->format);
  g_return_val_if_fail (format != NULL, NULL);

  if (info->layout == GST_AUDIO_LAYOUT_INTERLEAVED)
    layout = "interleaved";
  else if (info->layout == GST_AUDIO_LAYOUT_NON_INTERLEAVED)
    layout = "non-interleaved";
  else
    g_return_val_if_reached (NULL);

  flags = info->flags;
  if ((flags & GST_AUDIO_FLAG_UNPOSITIONED) && info->channels > 1
      && info->position[0] != GST_AUDIO_CHANNEL_POSITION_NONE) {
    flags &= ~GST_AUDIO_FLAG_UNPOSITIONED;
    g_warning ("Unpositioned audio channel position flag set but "
        "channel positions present");
  } else if (!(flags & GST_AUDIO_FLAG_UNPOSITIONED) && info->channels > 1
      && info->position[0] == GST_AUDIO_CHANNEL_POSITION_NONE) {
    flags |= GST_AUDIO_FLAG_UNPOSITIONED;
    g_warning ("Unpositioned audio channel position flag not set "
        "but no channel positions present");
  }

  caps = gst_caps_new_simple ("audio/x-raw",
      "format", G_TYPE_STRING, format,
      "layout", G_TYPE_STRING, layout,
      "rate", G_TYPE_INT, info->rate,
      "channels", G_TYPE_INT, info->channels, NULL);

  if (info->channels > 1
      || info->position[0] != GST_AUDIO_CHANNEL_POSITION_MONO) {
    guint64 channel_mask = 0;

    if ((flags & GST_AUDIO_FLAG_UNPOSITIONED)) {
      channel_mask = 0;
    } else {
      if (!gst_audio_channel_positions_to_mask (info->position, info->channels,
              TRUE, &channel_mask))
        goto invalid_channel_positions;
    }

    if (info->channels == 1
        && info->position[0] == GST_AUDIO_CHANNEL_POSITION_MONO) {
      /* Default mono special case */
    } else {
      gst_caps_set_simple (caps, "channel-mask", GST_TYPE_BITMASK, channel_mask,
          NULL);
    }
  }

  return caps;

invalid_channel_positions:
  {
    GST_ERROR ("Invalid channel positions");
    gst_caps_unref (caps);
    return NULL;
  }
}

/**
 * gst_audio_info_convert:
 * @info: a #GstAudioInfo
 * @src_fmt: #GstFormat of the @src_val
 * @src_val: value to convert
 * @dest_fmt: #GstFormat of the @dest_val
 * @dest_val: (out): pointer to destination value
 *
 * Converts among various #GstFormat types.  This function handles
 * GST_FORMAT_BYTES, GST_FORMAT_TIME, and GST_FORMAT_DEFAULT.  For
 * raw audio, GST_FORMAT_DEFAULT corresponds to audio frames.  This
 * function can be used to handle pad queries of the type GST_QUERY_CONVERT.
 *
 * Returns: TRUE if the conversion was successful.
 */
gboolean
gst_audio_info_convert (const GstAudioInfo * info,
    GstFormat src_fmt, gint64 src_val, GstFormat dest_fmt, gint64 * dest_val)
{
  gboolean res = TRUE;
  gint bpf, rate;

  GST_DEBUG ("converting value %" G_GINT64_FORMAT " from %s (%d) to %s (%d)",
      src_val, gst_format_get_name (src_fmt), src_fmt,
      gst_format_get_name (dest_fmt), dest_fmt);

  if (src_fmt == dest_fmt || src_val == -1) {
    *dest_val = src_val;
    goto done;
  }

  /* get important info */
  bpf = GST_AUDIO_INFO_BPF (info);
  rate = GST_AUDIO_INFO_RATE (info);

  if (bpf == 0 || rate == 0) {
    GST_DEBUG ("no rate or bpf configured");
    res = FALSE;
    goto done;
  }

  switch (src_fmt) {
    case GST_FORMAT_BYTES:
      switch (dest_fmt) {
        case GST_FORMAT_TIME:
          *dest_val = GST_FRAMES_TO_CLOCK_TIME (src_val / bpf, rate);
          break;
        case GST_FORMAT_DEFAULT:
          *dest_val = src_val / bpf;
          break;
        default:
          res = FALSE;
          break;
      }
      break;
    case GST_FORMAT_DEFAULT:
      switch (dest_fmt) {
        case GST_FORMAT_TIME:
          *dest_val = GST_FRAMES_TO_CLOCK_TIME (src_val, rate);
          break;
        case GST_FORMAT_BYTES:
          *dest_val = src_val * bpf;
          break;
        default:
          res = FALSE;
          break;
      }
      break;
    case GST_FORMAT_TIME:
      switch (dest_fmt) {
        case GST_FORMAT_DEFAULT:
          *dest_val = GST_CLOCK_TIME_TO_FRAMES (src_val, rate);
          break;
        case GST_FORMAT_BYTES:
          *dest_val = GST_CLOCK_TIME_TO_FRAMES (src_val, rate);
          *dest_val *= bpf;
          break;
        default:
          res = FALSE;
          break;
      }
      break;
    default:
      res = FALSE;
      break;
  }
done:

  GST_DEBUG ("ret=%d result %" G_GINT64_FORMAT, res, res ? *dest_val : -1);

  return res;
}

/**
 * gst_audio_info_is_equal:
 * @info: a #GstAudioInfo
 * @other: a #GstAudioInfo
 *
 * Compares two #GstAudioInfo and returns whether they are equal or not
 *
 * Returns: %TRUE if @info and @other are equal, else %FALSE.
 *
 * Since: 1.2
 *
 */
gboolean
gst_audio_info_is_equal (const GstAudioInfo * info, const GstAudioInfo * other)
{
  if (info == other)
    return TRUE;
  if (info->finfo == NULL || other->finfo == NULL)
    return FALSE;
  if (GST_AUDIO_INFO_FORMAT (info) != GST_AUDIO_INFO_FORMAT (other))
    return FALSE;
  if (GST_AUDIO_INFO_FLAGS (info) != GST_AUDIO_INFO_FLAGS (other))
    return FALSE;
  if (GST_AUDIO_INFO_LAYOUT (info) != GST_AUDIO_INFO_LAYOUT (other))
    return FALSE;
  if (GST_AUDIO_INFO_RATE (info) != GST_AUDIO_INFO_RATE (other))
    return FALSE;
  if (GST_AUDIO_INFO_CHANNELS (info) != GST_AUDIO_INFO_CHANNELS (other))
    return FALSE;
  if (GST_AUDIO_INFO_CHANNELS (info) > 64)
    return TRUE;
  if (memcmp (info->position, other->position,
          GST_AUDIO_INFO_CHANNELS (info) * sizeof (GstAudioChannelPosition)) !=
      0)
    return FALSE;

  return TRUE;
}
