/* GStreamer demultiplexer plugin for Interplay MVE movie files
 *
 * Copyright (C) 2006-2008 Jens Granseuer <jensgr@gmx.net>
 *
 * 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.
 *
 * For more information about the Interplay MVE format, visit:
 *   http://www.pcisys.net/~melanson/codecs/interplay-mve.txt
 */

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

#include <string.h>
#include "gstmvedemux.h"
#include "mve.h"

GST_DEBUG_CATEGORY_STATIC (mvedemux_debug);
#define GST_CAT_DEFAULT mvedemux_debug

enum MveDemuxState
{
  MVEDEMUX_STATE_INITIAL,       /* initial state, header not read */
  MVEDEMUX_STATE_NEXT_CHUNK,    /* parsing chunk/segment header */
  MVEDEMUX_STATE_MOVIE,         /* reading the stream */
  MVEDEMUX_STATE_SKIP           /* skipping chunk */
};

static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("video/x-mve")
    );

static GstStaticPadTemplate vidsrc_template = GST_STATIC_PAD_TEMPLATE ("video",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS ("video/x-raw-rgb, "
        "width = (int) [ 1, MAX ], "
        "height = (int) [ 1, MAX ], "
        "framerate = (fraction) [ 0, MAX ], "
        "bpp = (int) 16, "
        "depth = (int) 15, "
        "endianness = (int) BYTE_ORDER, "
        "red_mask = (int) 31744, "
        "green_mask = (int) 992, "
        "blue_mask = (int) 31; "
        "video/x-raw-rgb, "
        "width = (int) [ 1, MAX ], "
        "height = (int) [ 1, MAX ], "
        "framerate = (fraction) [ 0, MAX ], "
        "bpp = (int) 8, " "depth = (int) 8, " "endianness = (int) BYTE_ORDER")
    );

static GstStaticPadTemplate audsrc_template = GST_STATIC_PAD_TEMPLATE ("audio",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS ("audio/x-raw-int, "
        "width = (int) 8, "
        "rate = (int) [ 1, MAX ], "
        "channels = (int) [ 1, 2 ], "
        "depth = (int) 8, "
        "signed = (boolean) false; "
        "audio/x-raw-int, "
        "width = (int) 16, "
        "rate = (int) [ 1, MAX ], "
        "channels = (int) [ 1, 2 ], "
        "depth = (int) 16, "
        "signed = (boolean) true, "
        "endianness = (int) { LITTLE_ENDIAN, BIG_ENDIAN }")
    );

#define MVE_DEFAULT_AUDIO_STREAM 0x01

static void gst_mve_demux_class_init (GstMveDemuxClass * klass);
static void gst_mve_demux_base_init (GstMveDemuxClass * klass);
static void gst_mve_demux_init (GstMveDemux * mve);

#define GST_MVE_SEGMENT_SIZE(data)    (GST_READ_UINT16_LE (data))
#define GST_MVE_SEGMENT_TYPE(data)    (GST_READ_UINT8 (data + 2))
#define GST_MVE_SEGMENT_VERSION(data) (GST_READ_UINT8 (data + 3))

static GstElementClass *parent_class = NULL;

static void
gst_mve_demux_reset (GstMveDemux * mve)
{
  gst_adapter_clear (mve->adapter);

  if (mve->video_stream != NULL) {
    if (mve->video_stream->pad)
      gst_element_remove_pad (GST_ELEMENT (mve), mve->video_stream->pad);
    if (mve->video_stream->caps)
      gst_caps_unref (mve->video_stream->caps);
    if (mve->video_stream->palette)
      gst_buffer_unref (mve->video_stream->palette);
    g_free (mve->video_stream->code_map);
    if (mve->video_stream->buffer)
      gst_buffer_unref (mve->video_stream->buffer);
    g_free (mve->video_stream);
    mve->video_stream = NULL;
  }

  if (mve->audio_stream != NULL) {
    if (mve->audio_stream->pad)
      gst_element_remove_pad (GST_ELEMENT (mve), mve->audio_stream->pad);
    if (mve->audio_stream->caps)
      gst_caps_unref (mve->audio_stream->caps);
    if (mve->audio_stream->buffer)
      gst_buffer_unref (mve->audio_stream->buffer);
    g_free (mve->audio_stream);
    mve->audio_stream = NULL;
  }

  mve->state = MVEDEMUX_STATE_INITIAL;
  mve->needed_bytes = MVE_PREAMBLE_SIZE;
  mve->frame_duration = GST_CLOCK_TIME_NONE;

  mve->chunk_size = 0;
  mve->chunk_offset = 0;
}

static const GstQueryType *
gst_mve_demux_get_src_query_types (GstPad * pad)
{
  static const GstQueryType src_types[] = {
    GST_QUERY_POSITION,
    GST_QUERY_SEEKING,
    0
  };

  return src_types;
}

static gboolean
gst_mve_demux_handle_src_query (GstPad * pad, GstQuery * query)
{
  gboolean res = FALSE;

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_POSITION:{
      GstFormat format;

      gst_query_parse_position (query, &format, NULL);

      /* we only support TIME */
      if (format == GST_FORMAT_TIME) {
        GstMveDemuxStream *s = gst_pad_get_element_private (pad);

        if (s != NULL) {
          GST_OBJECT_LOCK (s);
          gst_query_set_position (query, GST_FORMAT_TIME, s->last_ts);
          GST_OBJECT_UNLOCK (s);
          res = TRUE;
        }
      }
      break;
    }
    case GST_QUERY_SEEKING:{
      GstFormat format;

      gst_query_parse_seeking (query, &format, NULL, NULL, NULL);
      if (format == GST_FORMAT_TIME) {
        gst_query_set_seeking (query, GST_FORMAT_TIME, FALSE, 0, -1);
        res = TRUE;
      }
      break;
    }
    case GST_QUERY_DURATION:{
      /* FIXME: really should implement/estimate this somehow */
      res = FALSE;
      break;
    }
    default:
      res = gst_pad_query_default (pad, query);
      break;
  }

  return res;
}

static gboolean
gst_mve_demux_handle_src_event (GstPad * pad, GstEvent * event)
{
  gboolean res;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_SEEK:
      GST_DEBUG ("seeking not supported");
      res = FALSE;
      break;
    default:
      res = gst_pad_event_default (pad, event);
      break;
  }

  return res;
}


static GstStateChangeReturn
gst_mve_demux_change_state (GstElement * element, GstStateChange transition)
{
  GstMveDemux *mve = GST_MVE_DEMUX (element);

  if (GST_ELEMENT_CLASS (parent_class)->change_state) {
    GstStateChangeReturn ret;

    ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
    if (ret != GST_STATE_CHANGE_SUCCESS)
      return ret;
  }

  switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      gst_mve_demux_reset (mve);
      break;
    default:
      break;
  }

  return GST_STATE_CHANGE_SUCCESS;
}

static gboolean
gst_mve_add_stream (GstMveDemux * mve, GstMveDemuxStream * stream,
    GstTagList * list)
{
  GstPadTemplate *templ;
  gboolean ret = FALSE;

  if (stream->pad == NULL) {
    if (stream == mve->video_stream) {
      templ = gst_static_pad_template_get (&vidsrc_template);
      stream->pad = gst_pad_new_from_template (templ, "video");
    } else {
      templ = gst_static_pad_template_get (&audsrc_template);
      stream->pad = gst_pad_new_from_template (templ, "audio");
    }
    gst_object_unref (templ);

    gst_pad_set_query_type_function (stream->pad,
        GST_DEBUG_FUNCPTR (gst_mve_demux_get_src_query_types));
    gst_pad_set_query_function (stream->pad,
        GST_DEBUG_FUNCPTR (gst_mve_demux_handle_src_query));
    gst_pad_set_event_function (stream->pad,
        GST_DEBUG_FUNCPTR (gst_mve_demux_handle_src_event));
    gst_pad_set_element_private (stream->pad, stream);

    GST_DEBUG_OBJECT (mve, "adding pad %s", GST_PAD_NAME (stream->pad));
    gst_pad_set_active (stream->pad, TRUE);
    gst_element_add_pad (GST_ELEMENT (mve), stream->pad);
    ret = TRUE;
  }

  GST_DEBUG_OBJECT (mve, "setting caps %" GST_PTR_FORMAT, stream->caps);
  gst_pad_set_caps (stream->pad, stream->caps);

  if (list)
    gst_element_found_tags_for_pad (GST_ELEMENT (mve), stream->pad, list);

  return ret;
}

static GstFlowReturn
gst_mve_stream_error (GstMveDemux * mve, guint16 req, guint16 avail)
{
  GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
      ("wanted to read %d bytes from stream, %d available", req, avail));
  return GST_FLOW_ERROR;
}

static GstFlowReturn
gst_mve_buffer_alloc_for_pad (GstMveDemuxStream * stream,
    guint32 size, GstBuffer ** buffer)
{
  *buffer = gst_buffer_new_and_alloc (size);
  gst_buffer_set_caps (*buffer, stream->caps);
  GST_BUFFER_TIMESTAMP (*buffer) = stream->last_ts;
  GST_BUFFER_OFFSET (*buffer) = stream->offset;
  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_init (GstMveDemux * mve, const guint8 * data)
{
  GST_DEBUG_OBJECT (mve, "init video");

  if (mve->video_stream == NULL) {
    GstMveDemuxStream *stream = g_new0 (GstMveDemuxStream, 1);

    stream->buffer = NULL;
    stream->back_buf1 = NULL;
    stream->back_buf2 = NULL;
    stream->offset = 0;
    stream->width = 0;
    stream->height = 0;
    stream->code_map = NULL;
    stream->code_map_avail = FALSE;
    stream->palette = NULL;
    stream->caps = NULL;
    stream->last_ts = GST_CLOCK_TIME_NONE;
    stream->last_flow = GST_FLOW_OK;
    mve->video_stream = stream;
  }

  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_create_buffer (GstMveDemux * mve, guint8 version,
    const guint8 * data, guint16 len)
{
  GstBuffer *buf;
  guint16 w, h, n, true_color, bpp;
  guint required, size;

  GST_DEBUG_OBJECT (mve, "create video buffer");

  if (mve->video_stream == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("trying to create video buffer for uninitialized stream"));
    return GST_FLOW_ERROR;
  }

  /* need 4 to 8 more bytes */
  required = (version > 1) ? 8 : (version * 2);
  if (len < required)
    return gst_mve_stream_error (mve, required, len);

  w = GST_READ_UINT16_LE (data) << 3;
  h = GST_READ_UINT16_LE (data + 2) << 3;

  if (version > 0)
    n = GST_READ_UINT16_LE (data + 4);
  else
    n = 1;

  if (version > 1)
    true_color = GST_READ_UINT16_LE (data + 6);
  else
    true_color = 0;

  bpp = (true_color ? 2 : 1);
  size = w * h * bpp;

  if (mve->video_stream->buffer != NULL) {
    GST_DEBUG_OBJECT (mve, "video buffer already created");

    if (GST_BUFFER_SIZE (mve->video_stream->buffer) == size * 2)
      return GST_FLOW_OK;

    GST_DEBUG_OBJECT (mve, "video buffer size has changed");
    gst_buffer_unref (mve->video_stream->buffer);
  }

  GST_DEBUG_OBJECT (mve,
      "allocating video buffer, w:%u, h:%u, n:%u, true_color:%u", w, h, n,
      true_color);

  /* we need a buffer to keep the last 2 frames, since those may be
     needed for decoding the next one */
  buf = gst_buffer_new_and_alloc (size * 2);

  mve->video_stream->bpp = bpp;
  mve->video_stream->width = w;
  mve->video_stream->height = h;
  mve->video_stream->buffer = buf;
  mve->video_stream->back_buf1 = GST_BUFFER_DATA (buf);
  mve->video_stream->back_buf2 = mve->video_stream->back_buf1 + size;
  mve->video_stream->max_block_offset = (h - 7) * w - 8;
  memset (mve->video_stream->back_buf1, 0, size * 2);

  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_palette (GstMveDemux * mve, const guint8 * data, guint16 len)
{
  GstBuffer *buf;
  guint16 start, count;
  const guint8 *pal;
  guint32 *pal_ptr;
  gint i;

  GST_DEBUG_OBJECT (mve, "video palette");

  if (mve->video_stream == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("found palette before video stream was initialized"));
    return GST_FLOW_ERROR;
  }

  /* need 4 more bytes now, more later */
  if (len < 4)
    return gst_mve_stream_error (mve, 4, len);

  len -= 4;

  start = GST_READ_UINT16_LE (data);
  count = GST_READ_UINT16_LE (data + 2);
  GST_DEBUG_OBJECT (mve, "found palette start:%u, count:%u", start, count);

  /* need more bytes */
  if (len < count * 3)
    return gst_mve_stream_error (mve, count * 3, len);

  /* make sure we don't exceed the buffer */
  if (start + count > MVE_PALETTE_COUNT) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("palette too large for buffer"));
    return GST_FLOW_ERROR;
  }

  if (mve->video_stream->palette != NULL) {
    /* older buffers floating around might still use the old
       palette, so make sure we can update it */
    buf = gst_buffer_make_writable (mve->video_stream->palette);
  } else {
    buf = gst_buffer_new_and_alloc (MVE_PALETTE_COUNT * 4);
    memset (GST_BUFFER_DATA (buf), 0, GST_BUFFER_SIZE (buf));
  }

  mve->video_stream->palette = buf;

  pal = data + 4;
  pal_ptr = ((guint32 *) GST_BUFFER_DATA (buf)) + start;
  for (i = 0; i < count; ++i) {
    /* convert from 6-bit VGA to 8-bit palette */
    guint8 r, g, b;

    r = (*pal) << 2;
    ++pal;
    g = (*pal) << 2;
    ++pal;
    b = (*pal) << 2;
    ++pal;
    *pal_ptr = (r << 16) | (g << 8) | (b);
    ++pal_ptr;
  }
  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_palette_compressed (GstMveDemux * mve, const guint8 * data,
    guint16 len)
{
  guint8 mask;
  gint i, j;
  guint32 *col;

  GST_DEBUG_OBJECT (mve, "compressed video palette");

  if (mve->video_stream == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("found palette before video stream was initialized"));
    return GST_FLOW_ERROR;
  }

  if (mve->video_stream->palette == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("no palette available for modification"));
    return GST_FLOW_ERROR;
  }

  /* need at least 32 more bytes */
  if (len < 32)
    return gst_mve_stream_error (mve, 32, len);

  len -= 32;

  for (i = 0; i < 32; ++i) {
    mask = GST_READ_UINT8 (data);
    ++data;

    if (mask != 0) {
      for (j = 0; j < 8; ++j) {
        if (mask & (1 << j)) {
          guint8 r, g, b;

          /* need 3 more bytes */
          if (len < 3)
            return gst_mve_stream_error (mve, 3, len);

          len -= 3;

          r = (*data) << 2;
          ++data;
          g = (*data) << 2;
          ++data;
          b = (*data) << 2;
          ++data;
          col =
              ((guint32 *) GST_BUFFER_DATA (mve->video_stream->palette)) +
              i * 8 + j;
          *col = (r << 16) | (g << 8) | (b);
        }
      }
    }
  }

  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_code_map (GstMveDemux * mve, const guint8 * data, guint16 len)
{
  gint min;

  if (mve->video_stream == NULL || mve->video_stream->code_map == NULL) {
    GST_WARNING_OBJECT (mve, "video stream not initialized");
    return GST_FLOW_ERROR;
  }

  GST_DEBUG_OBJECT (mve, "found code map, size:%u", len);

  /* decoding is done in 8x8 blocks using 4-bit opcodes */
  min = (mve->video_stream->width * mve->video_stream->height) / (8 * 8 * 2);

  if (len < min)
    return gst_mve_stream_error (mve, min, len);

  memcpy (mve->video_stream->code_map, data, min);
  mve->video_stream->code_map_avail = TRUE;
  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_video_data (GstMveDemux * mve, const guint8 * data, guint16 len,
    GstBuffer ** output)
{
  GstFlowReturn ret = GST_FLOW_OK;
  gint16 cur_frame, last_frame;
  gint16 x_offset, y_offset;
  gint16 x_size, y_size;
  guint16 flags;
  gint dec;
  GstBuffer *buf = NULL;
  GstMveDemuxStream *s = mve->video_stream;

  GST_LOG_OBJECT (mve, "video data");

  if (s == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("trying to decode video data before stream was initialized"));
    return GST_FLOW_ERROR;
  }

  if (GST_CLOCK_TIME_IS_VALID (mve->frame_duration)) {
    if (GST_CLOCK_TIME_IS_VALID (s->last_ts))
      s->last_ts += mve->frame_duration;
    else
      s->last_ts = 0;
  }

  if (!s->code_map_avail) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("no code map available for decoding"));
    return GST_FLOW_ERROR;
  }

  /* need at least 14 more bytes */
  if (len < 14)
    return gst_mve_stream_error (mve, 14, len);

  len -= 14;

  cur_frame = GST_READ_UINT16_LE (data);
  last_frame = GST_READ_UINT16_LE (data + 2);
  x_offset = GST_READ_UINT16_LE (data + 4);
  y_offset = GST_READ_UINT16_LE (data + 6);
  x_size = GST_READ_UINT16_LE (data + 8);
  y_size = GST_READ_UINT16_LE (data + 10);
  flags = GST_READ_UINT16_LE (data + 12);
  data += 14;

  GST_DEBUG_OBJECT (mve,
      "video data hot:%d, cold:%d, xoff:%d, yoff:%d, w:%d, h:%d, flags:%x",
      cur_frame, last_frame, x_offset, y_offset, x_size, y_size, flags);

  if (flags & MVE_VIDEO_DELTA_FRAME) {
    guint8 *temp = s->back_buf1;

    s->back_buf1 = s->back_buf2;
    s->back_buf2 = temp;
  }

  ret = gst_mve_buffer_alloc_for_pad (s, s->width * s->height * s->bpp, &buf);
  if (ret != GST_FLOW_OK)
    return ret;

  if (s->bpp == 2) {
    dec = ipvideo_decode_frame16 (s, data, len);
  } else {
    if (s->palette == NULL) {
      GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL), ("no palette available"));
      goto error;
    }

    dec = ipvideo_decode_frame8 (s, data, len);
  }
  if (dec != 0)
    goto error;

  memcpy (GST_BUFFER_DATA (buf), s->back_buf1, GST_BUFFER_SIZE (buf));
  GST_BUFFER_DURATION (buf) = mve->frame_duration;
  GST_BUFFER_OFFSET_END (buf) = ++s->offset;

  if (s->bpp == 1) {
    GstCaps *caps;

    /* set the palette on the outgoing buffer */
    caps = gst_caps_copy (s->caps);
    gst_caps_set_simple (caps,
        "palette_data", GST_TYPE_BUFFER, s->palette, NULL);
    gst_buffer_set_caps (buf, caps);
    gst_caps_unref (caps);
  }

  *output = buf;
  return GST_FLOW_OK;

error:
  gst_buffer_unref (buf);
  return GST_FLOW_ERROR;
}

static GstFlowReturn
gst_mve_audio_init (GstMveDemux * mve, guint8 version, const guint8 * data,
    guint16 len)
{
  GstMveDemuxStream *stream;
  guint16 flags;
  guint32 requested_buffer;
  GstTagList *list;
  gchar *name;

  GST_DEBUG_OBJECT (mve, "init audio");

  /* need 8 more bytes */
  if (len < 8)
    return gst_mve_stream_error (mve, 8, len);

  if (mve->audio_stream == NULL) {
    stream = g_new0 (GstMveDemuxStream, 1);
    stream->offset = 0;
    stream->last_ts = 0;
    stream->last_flow = GST_FLOW_OK;
    mve->audio_stream = stream;
  } else {
    stream = mve->audio_stream;
    gst_caps_unref (stream->caps);
  }

  flags = GST_READ_UINT16_LE (data + 2);
  stream->sample_rate = GST_READ_UINT16_LE (data + 4);
  requested_buffer = GST_READ_UINT32_LE (data + 6);

  /* bit 0: 0 = mono, 1 = stereo */
  stream->n_channels = (flags & MVE_AUDIO_STEREO) + 1;
  /* bit 1: 0 = 8 bit, 1 = 16 bit */
  stream->sample_size = (((flags & MVE_AUDIO_16BIT) >> 1) + 1) * 8;
  /* bit 2: 0 = uncompressed, 1 = compressed */
  stream->compression = ((version > 0) && (flags & MVE_AUDIO_COMPRESSED)) ?
      TRUE : FALSE;

  GST_DEBUG_OBJECT (mve, "audio init, sample_rate:%d, channels:%d, "
      "bits_per_sample:%d, compression:%d, buffer:%u",
      stream->sample_rate, stream->n_channels,
      stream->sample_size, stream->compression, requested_buffer);

  stream->caps = gst_caps_from_string ("audio/x-raw-int");
  if (stream->caps == NULL)
    return GST_FLOW_ERROR;

  gst_caps_set_simple (stream->caps,
      "signed", G_TYPE_BOOLEAN, (stream->sample_size == 8) ? FALSE : TRUE,
      "depth", G_TYPE_INT, stream->sample_size,
      "width", G_TYPE_INT, stream->sample_size,
      "channels", G_TYPE_INT, stream->n_channels,
      "rate", G_TYPE_INT, stream->sample_rate, NULL);
  if (stream->sample_size > 8) {
    /* for uncompressed audio we can simply copy the incoming buffer
       which is always in little endian format */
    gst_caps_set_simple (stream->caps, "endianness", G_TYPE_INT,
        (stream->compression ? G_BYTE_ORDER : G_LITTLE_ENDIAN), NULL);
  } else if (stream->compression) {
    GST_WARNING_OBJECT (mve,
        "compression is only supported for 16-bit samples");
    stream->compression = FALSE;
  }

  list = gst_tag_list_new ();
  name = g_strdup_printf ("Raw %d-bit PCM audio", stream->sample_size);
  gst_tag_list_add (list, GST_TAG_MERGE_REPLACE,
      GST_TAG_AUDIO_CODEC, name, NULL);
  g_free (name);

  if (gst_mve_add_stream (mve, stream, list))
    return gst_pad_push_event (mve->audio_stream->pad,
        gst_event_new_new_segment (FALSE, 1.0, GST_FORMAT_TIME,
            0, GST_CLOCK_TIME_NONE, 0)) ? GST_FLOW_OK : GST_FLOW_ERROR;
  else
    return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_audio_data (GstMveDemux * mve, guint8 type, const guint8 * data,
    guint16 len, GstBuffer ** output)
{
  GstFlowReturn ret;
  GstMveDemuxStream *s = mve->audio_stream;
  GstBuffer *buf = NULL;
  guint16 stream_mask;
  guint16 size;

  GST_LOG_OBJECT (mve, "audio data");

  if (s == NULL) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("trying to queue samples with no audio stream"));
    return GST_FLOW_ERROR;
  }

  /* need at least 6 more bytes */
  if (len < 6)
    return gst_mve_stream_error (mve, 6, len);

  len -= 6;

  stream_mask = GST_READ_UINT16_LE (data + 2);
  size = GST_READ_UINT16_LE (data + 4);
  data += 6;

  if (stream_mask & MVE_DEFAULT_AUDIO_STREAM) {
    guint16 n_samples = size / s->n_channels / (s->sample_size / 8);
    GstClockTime duration = (GST_SECOND / s->sample_rate) * n_samples;

    if (type == MVE_OC_AUDIO_DATA) {
      guint16 required = (s->compression ? size / 2 + s->n_channels : size);

      if (len < required)
        return gst_mve_stream_error (mve, required, len);

      ret = gst_mve_buffer_alloc_for_pad (s, size, &buf);

      if (ret != GST_FLOW_OK)
        return ret;

      if (s->compression)
        ipaudio_uncompress ((gint16 *) GST_BUFFER_DATA (buf), size,
            data, s->n_channels);
      else
        memcpy (GST_BUFFER_DATA (buf), data, size);

      GST_DEBUG_OBJECT (mve, "created audio buffer, size:%u, stream_mask:%x",
          size, stream_mask);
    } else {
      /* silence - create a minimal buffer with no sound */
      size = s->n_channels * (s->sample_size / 8);
      ret = gst_mve_buffer_alloc_for_pad (s, size, &buf);
      memset (GST_BUFFER_DATA (buf), 0, size);
    }

    GST_BUFFER_DURATION (buf) = duration;
    GST_BUFFER_OFFSET_END (buf) = s->offset + n_samples;
    *output = buf;

    s->offset += n_samples;
    s->last_ts += duration;
  } else {
    /* alternate audio streams not supported.
       are there any movies which use them? */
    if (type == MVE_OC_AUDIO_DATA)
      GST_WARNING_OBJECT (mve, "found non-empty alternate audio stream");
  }

  return GST_FLOW_OK;
}

static GstFlowReturn
gst_mve_timer_create (GstMveDemux * mve, const guint8 * data, guint16 len,
    GstBuffer ** buf)
{
  guint32 t_rate;
  guint16 t_subdiv;
  GstMveDemuxStream *s;
  GstTagList *list;
  gint rate_nom, rate_den;

  g_return_val_if_fail (mve->video_stream != NULL, GST_FLOW_ERROR);

  /* need 6 more bytes */
  if (len < 6)
    return gst_mve_stream_error (mve, 6, len);

  t_rate = GST_READ_UINT32_LE (data);
  t_subdiv = GST_READ_UINT16_LE (data + 4);

  GST_DEBUG_OBJECT (mve, "found timer:%ux%u", t_rate, t_subdiv);
  mve->frame_duration = t_rate * t_subdiv * GST_USECOND;

  /* now really start rolling... */
  s = mve->video_stream;

  if ((s->buffer == NULL) || (s->width == 0) || (s->height == 0)) {
    GST_ELEMENT_ERROR (mve, STREAM, DECODE, (NULL),
        ("missing or invalid create-video-buffer segment (%dx%d)",
            s->width, s->height));
    return GST_FLOW_ERROR;
  }

  if (s->pad != NULL) {
    if (s->caps != NULL) {
      gst_caps_unref (s->caps);
      s->caps = NULL;
    }
    if (s->code_map != NULL) {
      g_free (s->code_map);
      s->code_map = NULL;
    }
    list = NULL;
  } else {
    list = gst_tag_list_new ();
    gst_tag_list_add (list, GST_TAG_MERGE_REPLACE,
        GST_TAG_VIDEO_CODEC, "Raw RGB video", NULL);
  }

  s->caps = gst_caps_from_string ("video/x-raw-rgb");
  if (s->caps == NULL)
    return GST_FLOW_ERROR;

  rate_nom = GST_SECOND / GST_USECOND;
  rate_den = mve->frame_duration / GST_USECOND;

  gst_caps_set_simple (s->caps,
      "bpp", G_TYPE_INT, s->bpp * 8,
      "depth", G_TYPE_INT, (s->bpp == 1) ? 8 : 15,
      "width", G_TYPE_INT, s->width,
      "height", G_TYPE_INT, s->height,
      "framerate", GST_TYPE_FRACTION, rate_nom, rate_den,
      "endianness", G_TYPE_INT, G_BYTE_ORDER, NULL);
  if (s->bpp > 1) {
    gst_caps_set_simple (s->caps, "red_mask", G_TYPE_INT, 0x7C00,       /* 31744 */
        "green_mask", G_TYPE_INT, 0x03E0,       /*   992 */
        "blue_mask", G_TYPE_INT, 0x001F,        /*    31 */
        NULL);
  }

  s->code_map = g_malloc ((s->width * s->height) / (8 * 8 * 2));

  if (gst_mve_add_stream (mve, s, list))
    return gst_pad_push_event (s->pad,
        gst_event_new_new_segment (FALSE, 1.0, GST_FORMAT_TIME,
            0, GST_CLOCK_TIME_NONE, 0)) ? GST_FLOW_OK : GST_FLOW_ERROR;
  else
    return GST_FLOW_OK;
}

static void
gst_mve_end_chunk (GstMveDemux * mve)
{
  GST_LOG_OBJECT (mve, "end of chunk");

  if (mve->video_stream != NULL)
    mve->video_stream->code_map_avail = FALSE;
}

/* parse segment */
static GstFlowReturn
gst_mve_parse_segment (GstMveDemux * mve, GstMveDemuxStream ** stream,
    GstBuffer ** send)
{
  GstFlowReturn ret = GST_FLOW_OK;
  const guint8 *buffer, *data;
  guint8 type, version;
  guint16 len;

  buffer = gst_adapter_peek (mve->adapter, mve->needed_bytes);

  type = GST_MVE_SEGMENT_TYPE (buffer);

  /* check whether to handle the segment */
  if (type < 32) {
    version = GST_MVE_SEGMENT_VERSION (buffer);
    len = GST_MVE_SEGMENT_SIZE (buffer);
    data = buffer + 4;

    switch (type) {

      case MVE_OC_END_OF_CHUNK:
        gst_mve_end_chunk (mve);
        break;
      case MVE_OC_CREATE_TIMER:
        ret = gst_mve_timer_create (mve, data, len, send);
        *stream = mve->audio_stream;
        break;
      case MVE_OC_AUDIO_BUFFERS:
        ret = gst_mve_audio_init (mve, version, data, len);
        break;
      case MVE_OC_VIDEO_BUFFERS:
        ret = gst_mve_video_create_buffer (mve, version, data, len);
        break;
      case MVE_OC_AUDIO_DATA:
      case MVE_OC_AUDIO_SILENCE:
        ret = gst_mve_audio_data (mve, type, data, len, send);
        *stream = mve->audio_stream;
        break;
      case MVE_OC_VIDEO_MODE:
        ret = gst_mve_video_init (mve, data);
        break;
      case MVE_OC_PALETTE:
        ret = gst_mve_video_palette (mve, data, len);
        break;
      case MVE_OC_PALETTE_COMPRESSED:
        ret = gst_mve_video_palette_compressed (mve, data, len);
        break;
      case MVE_OC_CODE_MAP:
        ret = gst_mve_video_code_map (mve, data, len);
        break;
      case MVE_OC_VIDEO_DATA:
        ret = gst_mve_video_data (mve, data, len, send);
        *stream = mve->video_stream;
        break;

      case MVE_OC_END_OF_STREAM:
      case MVE_OC_PLAY_AUDIO:
      case MVE_OC_PLAY_VIDEO:
        /* these are chunks we don't need to handle */
        GST_LOG_OBJECT (mve, "ignored segment type:0x%02x, version:0x%02x",
            type, version);
        break;
      case 0x13:               /* ??? */
      case 0x14:               /* ??? */
      case 0x15:               /* ??? */
        /* these are chunks we know exist but we don't care about */
        GST_DEBUG_OBJECT (mve,
            "known but unhandled segment type:0x%02x, version:0x%02x", type,
            version);
        break;
      default:
        GST_WARNING_OBJECT (mve,
            "unhandled segment type:0x%02x, version:0x%02x", type, version);
        break;
    }
  }

  gst_adapter_flush (mve->adapter, mve->needed_bytes);
  return ret;
}

static GstFlowReturn
gst_mve_demux_chain (GstPad * sinkpad, GstBuffer * inbuf)
{
  GstMveDemux *mve = GST_MVE_DEMUX (GST_PAD_PARENT (sinkpad));
  GstFlowReturn ret = GST_FLOW_OK;

  gst_adapter_push (mve->adapter, inbuf);

  GST_DEBUG_OBJECT (mve, "queuing buffer, needed:%d, available:%u",
      mve->needed_bytes, gst_adapter_available (mve->adapter));

  while ((gst_adapter_available (mve->adapter) >= mve->needed_bytes) &&
      (ret == GST_FLOW_OK)) {
    GstMveDemuxStream *stream = NULL;
    GstBuffer *outbuf = NULL;

    switch (mve->state) {
      case MVEDEMUX_STATE_INITIAL:
        gst_adapter_flush (mve->adapter, mve->needed_bytes);

        mve->chunk_offset += mve->needed_bytes;
        mve->needed_bytes = 4;
        mve->state = MVEDEMUX_STATE_NEXT_CHUNK;
        break;

      case MVEDEMUX_STATE_NEXT_CHUNK:{
        const guint8 *data;
        guint16 size;

        data = gst_adapter_peek (mve->adapter, mve->needed_bytes);
        size = GST_MVE_SEGMENT_SIZE (data);

        if (mve->chunk_offset >= mve->chunk_size) {
          /* new chunk, flush buffer and proceed with next segment */
          guint16 chunk_type = GST_READ_UINT16_LE (data + 2);

          gst_adapter_flush (mve->adapter, mve->needed_bytes);
          mve->chunk_size = size;
          mve->chunk_offset = 0;

          if (chunk_type > MVE_CHUNK_END) {
            GST_WARNING_OBJECT (mve,
                "skipping unknown chunk type 0x%02x of size:%u", chunk_type,
                size);
            mve->needed_bytes += size;
            mve->state = MVEDEMUX_STATE_SKIP;
          } else {
            GST_DEBUG_OBJECT (mve, "found new chunk type 0x%02x of size:%u",
                chunk_type, size);
          }
        } else if (mve->chunk_offset <= mve->chunk_size) {
          /* new segment */
          GST_DEBUG_OBJECT (mve, "found segment type 0x%02x of size:%u",
              GST_MVE_SEGMENT_TYPE (data), size);

          mve->needed_bytes += size;
          mve->state = MVEDEMUX_STATE_MOVIE;
        }
      }
        break;

      case MVEDEMUX_STATE_MOVIE:
        ret = gst_mve_parse_segment (mve, &stream, &outbuf);

        if ((ret == GST_FLOW_OK) && (outbuf != NULL)) {
          /* send buffer */
          GST_DEBUG_OBJECT (mve,
              "pushing buffer with time %" GST_TIME_FORMAT
              " (%u bytes) on pad %s",
              GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (outbuf)),
              GST_BUFFER_SIZE (outbuf), GST_PAD_NAME (stream->pad));

          ret = gst_pad_push (stream->pad, outbuf);
          stream->last_flow = ret;
        }

        if (ret == GST_FLOW_NOT_LINKED) {
          if (mve->audio_stream
              && mve->audio_stream->last_flow != GST_FLOW_NOT_LINKED)
            ret = GST_FLOW_OK;
          if (mve->video_stream
              && mve->video_stream->last_flow != GST_FLOW_NOT_LINKED)
            ret = GST_FLOW_OK;
        }

        /* update current offset */
        mve->chunk_offset += mve->needed_bytes;

        mve->state = MVEDEMUX_STATE_NEXT_CHUNK;
        mve->needed_bytes = 4;
        break;

      case MVEDEMUX_STATE_SKIP:
        mve->chunk_offset += mve->needed_bytes;
        gst_adapter_flush (mve->adapter, mve->needed_bytes);
        mve->state = MVEDEMUX_STATE_NEXT_CHUNK;
        mve->needed_bytes = 4;
        break;

      default:
        GST_ERROR_OBJECT (mve, "invalid state: %d", mve->state);
        break;
    }
  }

  return ret;
}

static void
gst_mve_demux_dispose (GObject * obj)
{
  GstMveDemux *mve = GST_MVE_DEMUX (obj);

  if (mve->adapter) {
    g_object_unref (mve->adapter);
    mve->adapter = NULL;
  }

  G_OBJECT_CLASS (parent_class)->dispose (obj);
}

static void
gst_mve_demux_base_init (GstMveDemuxClass * klass)
{

  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  gst_element_class_add_static_pad_template (element_class, &sink_template);
  gst_element_class_add_static_pad_template (element_class, &vidsrc_template);
  gst_element_class_add_static_pad_template (element_class, &audsrc_template);

  gst_element_class_set_details_simple (element_class, "MVE Demuxer",
      "Codec/Demuxer",
      "Demultiplex an Interplay movie (MVE) stream into audio and video",
      "Jens Granseuer <jensgr@gmx.net>");
}

static void
gst_mve_demux_class_init (GstMveDemuxClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  parent_class = g_type_class_peek_parent (klass);

  gobject_class->dispose = GST_DEBUG_FUNCPTR (gst_mve_demux_dispose);

  element_class->change_state = GST_DEBUG_FUNCPTR (gst_mve_demux_change_state);
}

static void
gst_mve_demux_init (GstMveDemux * mve)
{
  mve->sinkpad = gst_pad_new_from_static_template (&sink_template, "sink");
  gst_pad_set_chain_function (mve->sinkpad,
      GST_DEBUG_FUNCPTR (gst_mve_demux_chain));
  gst_element_add_pad (GST_ELEMENT (mve), mve->sinkpad);

  mve->adapter = gst_adapter_new ();
  gst_mve_demux_reset (mve);
}

GType
gst_mve_demux_get_type (void)
{
  static GType plugin_type = 0;

  if (!plugin_type) {
    const GTypeInfo plugin_info = {
      sizeof (GstMveDemuxClass),
      (GBaseInitFunc) gst_mve_demux_base_init,
      NULL,
      (GClassInitFunc) gst_mve_demux_class_init,
      NULL,
      NULL,
      sizeof (GstMveDemux),
      0,
      (GInstanceInitFunc) gst_mve_demux_init,
    };

    GST_DEBUG_CATEGORY_INIT (mvedemux_debug, "mvedemux",
        0, "Interplay MVE movie demuxer");

    plugin_type = g_type_register_static (GST_TYPE_ELEMENT,
        "GstMveDemux", &plugin_info, 0);
  }
  return plugin_type;
}
