/* GStreamer
 * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
 *
 * Authors: Peter van Hardenberg <pvh@songbirdnest.com>
 *
 * 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/* Based on ADPCM encoders in libsndfile, 
   Copyright (C) 1999-2002 Erik de Castro Lopo <erikd@zip.com.au
 */

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

#include <gst/gst.h>
#include <gst/base/gstadapter.h>

#define GST_TYPE_ADPCM_ENC \
    (adpcmenc_get_type ())

#define GST_TYPE_ADPCMENC_LAYOUT \
    (adpcmenc_layout_get_type ())

#define GST_ADPCM_ENC(obj) \
    (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_ADPCM_ENC, ADPCMEnc))

#define GST_CAT_DEFAULT adpcmenc_debug
GST_DEBUG_CATEGORY_STATIC (adpcmenc_debug);

static GstStaticPadTemplate adpcmenc_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw-int, "
        "depth = (int)16, "
        "width = (int)16, " "channels = (int) [1,2], " "rate = (int)[1, MAX]")
    );

static GstStaticPadTemplate adpcmenc_src_template =
    GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-adpcm, "
        " layout=(string){dvi}, "
        " block_align = (int) [64, 8192], "
        " rate = (int)[ 1, MAX ], " "channels = (int)[1,2];")
    );

#define MIN_ADPCM_BLOCK_SIZE 64
#define MAX_ADPCM_BLOCK_SIZE 8192
#define DEFAULT_ADPCM_BLOCK_SIZE 1024
#define DEFAULT_ADPCM_LAYOUT LAYOUT_ADPCM_DVI

static int ima_indx_adjust[16] = {
  -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8,
};

static int ima_step_size[89] = {
  7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
  50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230,
  253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963,
  1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327,
  3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442,
  11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
  32767
};


enum adpcm_properties
{
  ARG_0,
  ARG_BLOCK_SIZE,
  ARG_LAYOUT
};

enum adpcm_layout
{
  LAYOUT_ADPCM_DVI
};

static GType
adpcmenc_layout_get_type (void)
{
  static GType adpcmenc_layout_type = 0;

  if (!adpcmenc_layout_type) {
    static GEnumValue layout_types[] = {
      {LAYOUT_ADPCM_DVI, "DVI/IMA APDCM", "dvi"},
      {0, NULL, NULL},
    };

    adpcmenc_layout_type = g_enum_register_static ("GstADPCMEncLayout",
        layout_types);
  }

  return adpcmenc_layout_type;
}

typedef struct _ADPCMEncClass
{
  GstElementClass parent_class;
} ADPCMEncClass;

typedef struct _ADPCMEnc
{
  GstElement parent;

  GstPad *sinkpad;
  GstPad *srcpad;

  GstCaps *output_caps;

  enum adpcm_layout layout;
  int rate;
  int channels;
  int blocksize;
  int samples_per_block;

  guint8 step_index[2];

  gboolean is_setup;

  GstClockTime timestamp;
  GstClockTime base_timestamp;

  guint64 out_samples;

  GstAdapter *adapter;

} ADPCMEnc;

GType adpcmenc_get_type (void);
GST_BOILERPLATE (ADPCMEnc, adpcmenc, GstElement, GST_TYPE_ELEMENT);
static gboolean
adpcmenc_setup (ADPCMEnc * enc)
{
  const int DVI_IMA_HEADER_SIZE = 4;
  const int ADPCM_SAMPLES_PER_BYTE = 2;
  guint64 sample_bytes;
  const char *layout;

  switch (enc->layout) {
    case LAYOUT_ADPCM_DVI:
      layout = "dvi";
      /* IMA ADPCM includes a 4-byte header per channel, */
      sample_bytes = enc->blocksize - (DVI_IMA_HEADER_SIZE * enc->channels);
      /* two samples per byte, plus a single sample in the header. */
      enc->samples_per_block =
          ((sample_bytes * ADPCM_SAMPLES_PER_BYTE) / enc->channels) + 1;
      break;
    default:
      GST_WARNING_OBJECT (enc, "Invalid layout");
      return FALSE;
  }

  enc->output_caps = gst_caps_new_simple ("audio/x-adpcm",
      "rate", G_TYPE_INT, enc->rate,
      "channels", G_TYPE_INT, enc->channels,
      "layout", G_TYPE_STRING, layout,
      "block_align", G_TYPE_INT, enc->blocksize, NULL);

  if (enc->output_caps) {
    gst_pad_set_caps (enc->srcpad, enc->output_caps);
  }

  enc->is_setup = TRUE;
  enc->timestamp = GST_CLOCK_TIME_NONE;
  enc->base_timestamp = GST_CLOCK_TIME_NONE;
  enc->adapter = gst_adapter_new ();
  enc->out_samples = 0;

  /* Step index state is carried between blocks. */
  enc->step_index[0] = 0;
  enc->step_index[1] = 0;

  return TRUE;
}

static void
adpcmenc_teardown (ADPCMEnc * enc)
{
  if (enc->output_caps) {
    gst_caps_unref (enc->output_caps);
    enc->output_caps = NULL;
  }
  if (enc->adapter) {
    g_object_unref (enc->adapter);
    enc->adapter = NULL;
  }
  enc->is_setup = FALSE;
}

static gboolean
adpcmenc_sink_setcaps (GstPad * pad, GstCaps * caps)
{
  ADPCMEnc *enc = (ADPCMEnc *) gst_pad_get_parent (pad);
  GstStructure *structure = gst_caps_get_structure (caps, 0);

  if (!gst_structure_get_int (structure, "rate", &enc->rate))
    return FALSE;
  if (!gst_structure_get_int (structure, "channels", &enc->channels))
    return FALSE;

  if (enc->is_setup) {
    adpcmenc_teardown (enc);
  }
  adpcmenc_setup (enc);

  gst_object_unref (enc);

  return TRUE;
}

static void
adpcmenc_set_property (GObject * object,
    guint prop_id, const GValue * value, GParamSpec * pspec)
{
  ADPCMEnc *enc = GST_ADPCM_ENC (object);

  switch (prop_id) {
    case ARG_BLOCK_SIZE:
      enc->blocksize = g_value_get_int (value);
      break;
    case ARG_LAYOUT:
      enc->layout = g_value_get_enum (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
adpcmenc_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec)
{
  ADPCMEnc *enc = GST_ADPCM_ENC (object);

  switch (prop_id) {
    case ARG_BLOCK_SIZE:
      g_value_set_int (value, enc->blocksize);
      break;
    case ARG_LAYOUT:
      g_value_set_enum (value, enc->layout);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static guint8
adpcmenc_encode_ima_sample (gint16 sample, gint16 * prev_sample,
    guint8 * stepindex)
{
  const int NEGATIVE_SIGN_BIT = 0x8;
  int diff, vpdiff, mask, step;
  int bytecode = 0x0;
  diff = sample - *prev_sample;
  step = ima_step_size[*stepindex];
  vpdiff = step >> 3;

  if (diff < 0) {
    diff = -diff;
    bytecode = NEGATIVE_SIGN_BIT;
  }

  mask = 0x4;
  while (mask > 0) {
    if (diff >= step) {
      bytecode |= mask;
      diff -= step;
      vpdiff += step;
    }
    step >>= 1;
    mask >>= 1;
  }

  if (bytecode & 8) {
    vpdiff = -vpdiff;
  }

  *prev_sample = CLAMP (*prev_sample + vpdiff, G_MININT16, G_MAXINT16);
  *stepindex = CLAMP (*stepindex + ima_indx_adjust[bytecode], 0, 88);

  return bytecode;
}

static gboolean
adpcmenc_encode_ima_block (ADPCMEnc * enc, gint16 * samples, guint8 * outbuf)
{
  const int HEADER_SIZE = 4;
  gint16 prev_sample[2] = { 0, 0 };
  guint32 write_pos = 0;
  guint32 read_pos = 0;
  guint8 channel = 0;

  /* Write a header for each channel.
   * The header consists of a sixteen-bit predicted sound value,
   * and an eight bit step_index, carried forward from any previous block.
   * These allow seeking within the file.
   */
  for (channel = 0; channel < enc->channels; channel++) {
    write_pos = channel * HEADER_SIZE;
    outbuf[write_pos + 0] = (samples[channel] & 0xFF);
    outbuf[write_pos + 1] = (samples[channel] >> 8) & 0xFF;
    outbuf[write_pos + 2] = enc->step_index[channel];
    outbuf[write_pos + 3] = 0;
    prev_sample[channel] = samples[channel];
  }

  /* raw-audio looks like this for a stereo stream:
   * [ L, R, L, R, L, R ... ]
   * encoded audio is in eight-sample blocks, two samples to a byte thusly:
   * [ LL, LL, LL, LL, RR, RR, RR, RR ... ] 
   */
  write_pos = HEADER_SIZE * enc->channels;
  read_pos = enc->channels;     /* the first sample is in the header. */
  while (write_pos < enc->blocksize) {
    gint8 CHANNEL_CHUNK_SIZE = 8;
    for (channel = 0; channel < enc->channels; channel++) {
      /* convert eight samples (four bytes) per channel, then swap */
      guint32 channel_chunk_base = read_pos + channel;
      gint8 chunk;
      for (chunk = 0; chunk < CHANNEL_CHUNK_SIZE; chunk++) {
        guint8 packed_byte = 0, encoded_sample;
        encoded_sample =
            adpcmenc_encode_ima_sample (samples[channel_chunk_base +
                (chunk * enc->channels)], &prev_sample[channel],
            &enc->step_index[channel]);
        packed_byte |= encoded_sample & 0x0F;

        chunk++;

        encoded_sample =
            adpcmenc_encode_ima_sample (samples[channel_chunk_base +
                (chunk * enc->channels)], &prev_sample[channel],
            &enc->step_index[channel]);
        packed_byte |= encoded_sample << 4 & 0xF0;

        outbuf[write_pos++] = packed_byte;
      }
    }
    /* advance to the next block of 8 samples per channel */
    read_pos += CHANNEL_CHUNK_SIZE * enc->channels;
    if (read_pos > enc->samples_per_block * enc->channels) {
      GST_LOG ("Ran past the end. (Reading %i of %i.)", read_pos,
          enc->samples_per_block);
    }
  }

  return TRUE;
}

static GstFlowReturn
adpcmenc_encode_block (ADPCMEnc * enc, gint16 * samples, int blocksize)
{
  gboolean res;
  GstBuffer *outbuf = NULL;

  if (enc->layout == LAYOUT_ADPCM_DVI) {
    outbuf = gst_buffer_new_and_alloc (enc->blocksize);
    res = adpcmenc_encode_ima_block (enc, samples, GST_BUFFER_DATA (outbuf));
  } else {
    GST_WARNING_OBJECT (enc, "Unknown layout");
    return GST_FLOW_ERROR;
  }

  if (!res) {
    gst_buffer_unref (outbuf);
    GST_WARNING_OBJECT (enc, "Encode of block failed");
    return GST_FLOW_ERROR;
  }

  gst_buffer_set_caps (outbuf, enc->output_caps);
  GST_BUFFER_TIMESTAMP (outbuf) = enc->timestamp;

  enc->out_samples += enc->samples_per_block;
  enc->timestamp = enc->base_timestamp +
      gst_util_uint64_scale_int (enc->out_samples, GST_SECOND, enc->rate);
  GST_BUFFER_DURATION (outbuf) = enc->timestamp - GST_BUFFER_TIMESTAMP (outbuf);

  return gst_pad_push (enc->srcpad, outbuf);
}

static GstFlowReturn
adpcmenc_chain (GstPad * pad, GstBuffer * buf)
{
  ADPCMEnc *enc = (ADPCMEnc *) gst_pad_get_parent (pad);
  GstFlowReturn ret = GST_FLOW_OK;
  gint16 *samples;
  GstBuffer *databuf = NULL;
  int input_bytes_per_block;
  const int BYTES_PER_SAMPLE = 2;

  if (enc->base_timestamp == GST_CLOCK_TIME_NONE) {
    enc->base_timestamp = GST_BUFFER_TIMESTAMP (buf);
    if (enc->base_timestamp == GST_CLOCK_TIME_NONE)
      enc->base_timestamp = 0;
    enc->timestamp = enc->base_timestamp;
  }

  gst_adapter_push (enc->adapter, buf);

  input_bytes_per_block =
      enc->samples_per_block * BYTES_PER_SAMPLE * enc->channels;

  while (gst_adapter_available (enc->adapter) >= input_bytes_per_block) {
    databuf = gst_adapter_take_buffer (enc->adapter, input_bytes_per_block);
    samples = (gint16 *) GST_BUFFER_DATA (databuf);
    ret = adpcmenc_encode_block (enc, samples, enc->blocksize);
    gst_buffer_unref (databuf);
    if (ret != GST_FLOW_OK)
      goto done;
  }

done:
  gst_object_unref (enc);
  return ret;
}

static gboolean
adpcmenc_sink_event (GstPad * pad, GstEvent * event)
{
  ADPCMEnc *enc = (ADPCMEnc *) gst_pad_get_parent (pad);
  gboolean res;
  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_FLUSH_STOP:
      enc->out_samples = 0;
      enc->timestamp = GST_CLOCK_TIME_NONE;
      enc->base_timestamp = GST_CLOCK_TIME_NONE;
      gst_adapter_clear (enc->adapter);
      /* Fall through */
    default:
      res = gst_pad_push_event (enc->srcpad, event);
      break;
  }
  gst_object_unref (enc);
  return res;
}

static GstStateChangeReturn
adpcmenc_change_state (GstElement * element, GstStateChange transition)
{
  GstStateChangeReturn ret;
  ADPCMEnc *enc = (ADPCMEnc *) element;

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      break;
    default:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      adpcmenc_teardown (enc);
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      break;
    default:
      break;
  }
  return ret;
}

static void
adpcmenc_dispose (GObject * obj)
{
  G_OBJECT_CLASS (parent_class)->dispose (obj);
}

static void
adpcmenc_init (ADPCMEnc * enc, ADPCMEncClass * klass)
{
  enc->sinkpad =
      gst_pad_new_from_static_template (&adpcmenc_sink_template, "sink");
  gst_pad_set_setcaps_function (enc->sinkpad,
      GST_DEBUG_FUNCPTR (adpcmenc_sink_setcaps));
  gst_pad_set_chain_function (enc->sinkpad, GST_DEBUG_FUNCPTR (adpcmenc_chain));
  gst_pad_set_event_function (enc->sinkpad,
      GST_DEBUG_FUNCPTR (adpcmenc_sink_event));
  gst_element_add_pad (GST_ELEMENT (enc), enc->sinkpad);

  enc->srcpad =
      gst_pad_new_from_static_template (&adpcmenc_src_template, "src");
  gst_element_add_pad (GST_ELEMENT (enc), enc->srcpad);

  /* Set defaults. */
  enc->blocksize = DEFAULT_ADPCM_BLOCK_SIZE;
  enc->layout = DEFAULT_ADPCM_LAYOUT;
}

static void
adpcmenc_class_init (ADPCMEncClass * klass)
{
  GObjectClass *gobjectclass = (GObjectClass *) klass;
  GstElementClass *gstelement_class = (GstElementClass *) klass;

  gobjectclass->set_property = adpcmenc_set_property;
  gobjectclass->get_property = adpcmenc_get_property;

  g_object_class_install_property (gobjectclass, ARG_LAYOUT,
      g_param_spec_enum ("layout", "Layout",
          "Layout for output stream",
          GST_TYPE_ADPCMENC_LAYOUT, DEFAULT_ADPCM_LAYOUT,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobjectclass, ARG_BLOCK_SIZE,
      g_param_spec_int ("blockalign", "Block Align",
          "Block size for output stream",
          MIN_ADPCM_BLOCK_SIZE, MAX_ADPCM_BLOCK_SIZE,
          DEFAULT_ADPCM_BLOCK_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gobjectclass->dispose = adpcmenc_dispose;
  gstelement_class->change_state = adpcmenc_change_state;
} static void

adpcmenc_base_init (gpointer klass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&adpcmenc_sink_template));
  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&adpcmenc_src_template));
  gst_element_class_set_details_simple (element_class, "ADPCM encoder",
      "Codec/Encoder/Audio",
      "Encode ADPCM audio",
      "Pioneers of the Inevitable <songbird@songbirdnest.com");
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  GST_DEBUG_CATEGORY_INIT (adpcmenc_debug, "adpcmenc", 0, "ADPCM Encoders");
  if (!gst_element_register (plugin, "adpcmenc", GST_RANK_PRIMARY,
          GST_TYPE_ADPCM_ENC)) {
    return FALSE;
  }
  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, "adpcmenc",
    "ADPCM encoder", plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME,
    GST_PACKAGE_ORIGIN);
