/*  GStreamer SBC audio encoder
 *  Copyright (C) 2004-2010  Marcel Holtmann <marcel@holtmann.org>
 *  Copyright (C) 2013       Tim-Philipp Müller <tim centricular net>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser 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
 *
 */

/**
 * SECTION:element-sbenc
 *
 * This element encodes raw integer PCM audio into a Bluetooth SBC audio.
 *
 * Encoding paramets such as blocks, subbands, bitpool, channel-mode, and
 * allocation-mode can be set by adding a capsfilter element with appropriate
 * filtercaps after the sbcenc encoder element.
 *
 * <refsect2>
 * <title>Example pipelines</title>
 * |[
 * gst-launch-1.0 -v audiotestsrc ! sbcenc ! rtpsbcpay ! udpsink
 * ]| Encode a sine wave into SBC, RTP payload it and send over the network using UDP
 * </refsect2>
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>

#include "gstsbcenc.h"

GST_DEBUG_CATEGORY_STATIC (sbc_enc_debug);
#define GST_CAT_DEFAULT sbc_enc_debug

G_DEFINE_TYPE (GstSbcEnc, gst_sbc_enc, GST_TYPE_AUDIO_ENCODER);

static GstStaticPadTemplate sbc_enc_sink_factory =
GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw, format=" GST_AUDIO_NE (S16) ", "
        "rate = (int) { 16000, 32000, 44100, 48000 }, "
        "channels = (int) [ 1, 2 ]"));

static GstStaticPadTemplate sbc_enc_src_factory =
GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-sbc, "
        "rate = (int) { 16000, 32000, 44100, 48000 }, "
        "channels = (int) [ 1, 2 ], "
        "channel-mode = (string) { mono, dual, stereo, joint }, "
        "blocks = (int) { 4, 8, 12, 16 }, "
        "subbands = (int) { 4, 8 }, "
        "allocation-method = (string) { snr, loudness }, "
        "bitpool = (int) [ 2, 64 ]"));


static gboolean gst_sbc_enc_start (GstAudioEncoder * enc);
static gboolean gst_sbc_enc_stop (GstAudioEncoder * enc);
static gboolean gst_sbc_enc_set_format (GstAudioEncoder * enc,
    GstAudioInfo * info);
static GstFlowReturn gst_sbc_enc_handle_frame (GstAudioEncoder * enc,
    GstBuffer * buffer);

static gboolean
gst_sbc_enc_set_format (GstAudioEncoder * audio_enc, GstAudioInfo * info)
{
  const gchar *allocation_method, *channel_mode;
  GstSbcEnc *enc = GST_SBC_ENC (audio_enc);
  GstStructure *s;
  GstCaps *caps, *filter_caps;
  GstCaps *output_caps;
  guint sampleframes_per_frame;

  enc->rate = GST_AUDIO_INFO_RATE (info);
  enc->channels = GST_AUDIO_INFO_CHANNELS (info);

  /* negotiate output format based on downstream caps restrictions */
  caps = gst_pad_get_allowed_caps (GST_AUDIO_ENCODER_SRC_PAD (enc));
  if (caps == GST_CAPS_NONE || gst_caps_is_empty (caps)) {
    gst_caps_unref (caps);
    return FALSE;
  }

  if (caps == NULL)
    caps = gst_static_pad_template_get_caps (&sbc_enc_src_factory);

  /* fixate output caps */
  filter_caps = gst_caps_new_simple ("audio/x-sbc", "rate", G_TYPE_INT,
      enc->rate, "channels", G_TYPE_INT, enc->channels, NULL);
  output_caps = gst_caps_intersect (caps, filter_caps);
  gst_caps_unref (filter_caps);

  if (output_caps == NULL || gst_caps_is_empty (output_caps)) {
    GST_WARNING_OBJECT (enc, "Couldn't negotiate output caps with input rate "
        "%d and input channels %d and allowed output caps %" GST_PTR_FORMAT,
        enc->rate, enc->channels, caps);
    if (output_caps)
      gst_caps_unref (output_caps);
    gst_caps_unref (caps);
    return FALSE;
  }

  gst_caps_unref (caps);
  caps = NULL;

  GST_DEBUG_OBJECT (enc, "fixating caps %" GST_PTR_FORMAT, output_caps);
  output_caps = gst_caps_truncate (output_caps);
  s = gst_caps_get_structure (output_caps, 0);
  if (enc->channels == 1)
    gst_structure_fixate_field_string (s, "channel-mode", "mono");
  else
    gst_structure_fixate_field_string (s, "channel-mode", "joint");

  gst_structure_fixate_field_nearest_int (s, "bitpool", 64);
  gst_structure_fixate_field_nearest_int (s, "blocks", 16);
  gst_structure_fixate_field_nearest_int (s, "subbands", 8);
  gst_structure_fixate_field_string (s, "allocation-method", "loudness");
  s = NULL;

  /* in case there's anything else left to fixate */
  output_caps = gst_caps_fixate (output_caps);
  gst_caps_set_simple (output_caps, "parsed", G_TYPE_BOOLEAN, TRUE, NULL);

  GST_INFO_OBJECT (enc, "output caps %" GST_PTR_FORMAT, output_caps);

  /* let's see what we fixated to */
  s = gst_caps_get_structure (output_caps, 0);
  gst_structure_get_int (s, "blocks", &enc->blocks);
  gst_structure_get_int (s, "subbands", &enc->subbands);
  gst_structure_get_int (s, "bitpool", &enc->bitpool);
  allocation_method = gst_structure_get_string (s, "allocation-method");
  channel_mode = gst_structure_get_string (s, "channel-mode");

  /* We want channel-mode and channels coherent */
  if (enc->channels == 1) {
    if (g_strcmp0 (channel_mode, "mono") != 0) {
      GST_ERROR_OBJECT (enc, "Can't have channel-mode '%s' for 1 channel",
          channel_mode);
      gst_caps_unref (output_caps);
      return FALSE;
    }
  } else {
    if (g_strcmp0 (channel_mode, "joint") != 0 &&
        g_strcmp0 (channel_mode, "stereo") != 0 &&
        g_strcmp0 (channel_mode, "dual") != 0) {
      GST_ERROR_OBJECT (enc, "Can't have channel-mode '%s' for 2 channels",
          channel_mode);
      gst_caps_unref (output_caps);
      return FALSE;
    }
  }

  /* we want to be handed all available samples in handle_frame, but always
   * enough to encode a frame */
  sampleframes_per_frame = enc->blocks * enc->subbands;
  gst_audio_encoder_set_frame_samples_min (audio_enc, sampleframes_per_frame);
  gst_audio_encoder_set_frame_samples_max (audio_enc, sampleframes_per_frame);
  gst_audio_encoder_set_frame_max (audio_enc, 0);

  /* FIXME: what to do with left-over samples at the end? can we encode them? */
  gst_audio_encoder_set_hard_min (audio_enc, TRUE);

  /* and configure encoder based on the output caps we negotiated */
  if (enc->rate == 16000)
    enc->sbc.frequency = SBC_FREQ_16000;
  else if (enc->rate == 32000)
    enc->sbc.frequency = SBC_FREQ_32000;
  else if (enc->rate == 44100)
    enc->sbc.frequency = SBC_FREQ_44100;
  else if (enc->rate == 48000)
    enc->sbc.frequency = SBC_FREQ_48000;
  else
    return FALSE;

  if (enc->blocks == 4)
    enc->sbc.blocks = SBC_BLK_4;
  else if (enc->blocks == 8)
    enc->sbc.blocks = SBC_BLK_8;
  else if (enc->blocks == 12)
    enc->sbc.blocks = SBC_BLK_12;
  else if (enc->blocks == 16)
    enc->sbc.blocks = SBC_BLK_16;
  else
    return FALSE;

  enc->sbc.subbands = (enc->subbands == 4) ? SBC_SB_4 : SBC_SB_8;
  enc->sbc.bitpool = enc->bitpool;

  if (channel_mode == NULL || allocation_method == NULL)
    return FALSE;

  if (strcmp (channel_mode, "joint") == 0)
    enc->sbc.mode = SBC_MODE_JOINT_STEREO;
  else if (strcmp (channel_mode, "stereo") == 0)
    enc->sbc.mode = SBC_MODE_STEREO;
  else if (strcmp (channel_mode, "dual") == 0)
    enc->sbc.mode = SBC_MODE_DUAL_CHANNEL;
  else if (strcmp (channel_mode, "mono") == 0)
    enc->sbc.mode = SBC_MODE_MONO;
  else if (strcmp (channel_mode, "auto") == 0)
    enc->sbc.mode = SBC_MODE_JOINT_STEREO;
  else
    return FALSE;

  if (strcmp (allocation_method, "loudness") == 0)
    enc->sbc.allocation = SBC_AM_LOUDNESS;
  else if (strcmp (allocation_method, "snr") == 0)
    enc->sbc.allocation = SBC_AM_SNR;
  else
    return FALSE;

  if (!gst_audio_encoder_set_output_format (audio_enc, output_caps))
    return FALSE;

  return gst_audio_encoder_negotiate (audio_enc);
}

static GstFlowReturn
gst_sbc_enc_handle_frame (GstAudioEncoder * audio_enc, GstBuffer * buffer)
{
  GstSbcEnc *enc = GST_SBC_ENC (audio_enc);
  GstMapInfo in_map, out_map;
  GstBuffer *outbuf = NULL;
  guint samples_per_frame, frames, i = 0;

  /* no fancy draining */
  if (buffer == NULL)
    return GST_FLOW_OK;

  if (G_UNLIKELY (enc->channels == 0 || enc->blocks == 0 || enc->subbands == 0))
    return GST_FLOW_NOT_NEGOTIATED;

  samples_per_frame = enc->channels * enc->blocks * enc->subbands;

  if (!gst_buffer_map (buffer, &in_map, GST_MAP_READ))
    goto map_failed;

  frames = in_map.size / (samples_per_frame * sizeof (gint16));

  GST_LOG_OBJECT (enc,
      "encoding %" G_GSIZE_FORMAT " samples into %u SBC frames",
      in_map.size / (enc->channels * sizeof (gint16)), frames);

  if (frames > 0) {
    gsize frame_len;

    frame_len = sbc_get_frame_length (&enc->sbc);
    outbuf = gst_audio_encoder_allocate_output_buffer (audio_enc,
        frames * frame_len);

    if (outbuf == NULL)
      goto no_buffer;

    gst_buffer_map (outbuf, &out_map, GST_MAP_WRITE);

    for (i = 0; i < frames; ++i) {
      gssize ret, written = 0;

      ret = sbc_encode (&enc->sbc, in_map.data + (i * samples_per_frame * 2),
          samples_per_frame * 2, out_map.data + (i * frame_len), frame_len,
          &written);

      if (ret < 0 || written != frame_len) {
        GST_WARNING_OBJECT (enc, "encoding error, ret = %" G_GSSIZE_FORMAT ", "
            "written = %" G_GSSIZE_FORMAT, ret, written);
        break;
      }
    }

    gst_buffer_unmap (outbuf, &out_map);

    if (i > 0)
      gst_buffer_set_size (outbuf, i * frame_len);
    else
      gst_buffer_replace (&outbuf, NULL);
  }

done:

  gst_buffer_unmap (buffer, &in_map);

  return gst_audio_encoder_finish_frame (audio_enc, outbuf,
      i * (samples_per_frame / enc->channels));

/* ERRORS */
no_buffer:
  {
    GST_ERROR_OBJECT (enc, "could not allocate output buffer");
    goto done;
  }
map_failed:
  {
    GST_ERROR_OBJECT (enc, "could not map input buffer");
    goto done;
  }
}

static gboolean
gst_sbc_enc_start (GstAudioEncoder * audio_enc)
{
  GstSbcEnc *enc = GST_SBC_ENC (audio_enc);

  GST_INFO_OBJECT (enc, "Setup subband codec");
  sbc_init (&enc->sbc, 0);

  return TRUE;
}

static gboolean
gst_sbc_enc_stop (GstAudioEncoder * audio_enc)
{
  GstSbcEnc *enc = GST_SBC_ENC (audio_enc);

  GST_INFO_OBJECT (enc, "Finish subband codec");
  sbc_finish (&enc->sbc);

  enc->subbands = 0;
  enc->blocks = 0;
  enc->rate = 0;
  enc->channels = 0;
  enc->bitpool = 0;

  return TRUE;
}

static void
gst_sbc_enc_class_init (GstSbcEncClass * klass)
{
  GstAudioEncoderClass *encoder_class = GST_AUDIO_ENCODER_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  encoder_class->start = GST_DEBUG_FUNCPTR (gst_sbc_enc_start);
  encoder_class->stop = GST_DEBUG_FUNCPTR (gst_sbc_enc_stop);
  encoder_class->set_format = GST_DEBUG_FUNCPTR (gst_sbc_enc_set_format);
  encoder_class->handle_frame = GST_DEBUG_FUNCPTR (gst_sbc_enc_handle_frame);

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sbc_enc_sink_factory));

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sbc_enc_src_factory));

  gst_element_class_set_static_metadata (element_class,
      "Bluetooth SBC audio encoder", "Codec/Encoder/Audio",
      "Encode an SBC audio stream", "Marcel Holtmann <marcel@holtmann.org>");

  GST_DEBUG_CATEGORY_INIT (sbc_enc_debug, "sbcenc", 0, "SBC encoding element");
}

static void
gst_sbc_enc_init (GstSbcEnc * self)
{
  self->subbands = 0;
  self->blocks = 0;
  self->rate = 0;
  self->channels = 0;
  self->bitpool = 0;
}
