/* GStreamer
 * Copyright (C) 2009 Igalia S.L. <itoral@igalia.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.
 */

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

#include "gstbasedecode.h"

#define GST_BASE_DECODE_GET_PRIVATE(obj)  \
    (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GST_TYPE_BASE_DECODE, GstBaseDecodePrivate))

GST_DEBUG_CATEGORY_STATIC (gst_base_decode_debug);
#define GST_CAT_DEFAULT gst_base_decode_debug

struct _GstBaseDecodePrivate
{
  gint flag;
};

static GstElementClass *parent_class = NULL;

static void gst_base_decode_class_init (GstBaseDecodeClass * klass);
static void gst_base_decode_init (GstBaseDecode * dec, 
				  GstBaseDecodeClass * bclass);
static void gst_base_decode_finalize (GObject * object);
static gboolean gst_base_decode_sink_event (GstPad * pad, GstEvent * event);
static gboolean gst_base_decode_src_event (GstPad * pad, GstEvent * event);
static GstFlowReturn gst_base_decode_chain (GstPad * pad,  GstBuffer * buffer);
static gboolean gst_base_decode_clip_buffer (GstBaseDecode * dec, 
					     GstBuffer * buffer);
static void gst_base_decode_clear_queued (GstBaseDecode * dec);
static GstStateChangeReturn gst_base_decode_change_state (GstElement * element, 
							  GstStateChange transition);
static gboolean gst_base_decode_seek (GstBaseDecode *dec, 
				      GstPad *pad, 
				      GstEvent *event);


GType
gst_base_decode_get_type (void)
{
  static GType base_decode_type = 0;

  if (!base_decode_type) {
    static const GTypeInfo base_decode_info = {
      sizeof (GstBaseDecodeClass),
      NULL,
      NULL,
      (GClassInitFunc) gst_base_decode_class_init,
      NULL,
      NULL,
      sizeof (GstBaseDecode),
      0,
      (GInstanceInitFunc) gst_base_decode_init,
    };

    base_decode_type = g_type_register_static (GST_TYPE_ELEMENT,
        "GstBaseDecode", &base_decode_info, G_TYPE_FLAG_ABSTRACT);
  }
  return base_decode_type;
}

static void
gst_base_decode_class_init (GstBaseDecodeClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gstelement_class = GST_ELEMENT_CLASS (klass);

  GST_DEBUG_CATEGORY_INIT (gst_base_decode_debug, "basedecode", 0,
			   "basedecode element");

  GST_DEBUG ("gst_base_decode_class_init");

  gobject_class->finalize = gst_base_decode_finalize;

  gstelement_class->change_state = gst_base_decode_change_state;

  g_type_class_add_private (klass, sizeof (GstBaseDecodePrivate));

  parent_class = g_type_class_peek_parent (klass);
}


static void
gst_base_decode_init (GstBaseDecode * dec,
		      GstBaseDecodeClass * bclass)
{
  GstPadTemplate *pad_template;

  GST_DEBUG ("gst_base_decode_init");

  dec->priv = GST_BASE_DECODE_GET_PRIVATE (dec);

  pad_template =
      gst_element_class_get_pad_template (GST_ELEMENT_CLASS (bclass), "sink");
  g_return_if_fail (pad_template != NULL);
  dec->sinkpad = gst_pad_new_from_template (pad_template, "sink");
  gst_pad_set_event_function (dec->sinkpad,
			      GST_DEBUG_FUNCPTR (gst_base_decode_sink_event));
  gst_pad_set_event_function (dec->srcpad,
			      GST_DEBUG_FUNCPTR (gst_base_decode_src_event));
  gst_pad_set_chain_function (dec->sinkpad,
			      GST_DEBUG_FUNCPTR (gst_base_decode_chain));
  gst_element_add_pad (GST_ELEMENT (dec), dec->sinkpad);

  pad_template =
      gst_element_class_get_pad_template (GST_ELEMENT_CLASS (bclass), "src");
  g_return_if_fail (pad_template != NULL);
  dec->srcpad = gst_pad_new_from_template (pad_template, "src");
  gst_element_add_pad (GST_ELEMENT (dec), dec->srcpad);

  gst_segment_init (&dec->segment, GST_FORMAT_UNDEFINED);
  dec->queued = NULL;
}

static void
gst_base_decode_finalize (GObject * object)
{
  gst_base_decode_clear_queued (GST_BASE_DECODE (object));
  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static GstStateChangeReturn
gst_base_decode_change_state (GstElement * element, GstStateChange transition)
{
  GstStateChangeReturn ret;
  GstBaseDecode  *dec = GST_BASE_DECODE (element);
  GstBaseDecodeClass *dec_class = GST_BASE_DECODE_GET_CLASS (dec);
  
  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      if (dec_class->init) {
	if (!dec_class->init (dec)) {
	  goto init_failed;
	}
      }
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      gst_segment_init (&dec->segment, GST_FORMAT_UNDEFINED);
      if (dec_class->reset) {
	if (!dec_class->reset (dec)) {
	  goto reset_failed;
	}
      }
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
    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:
      gst_base_decode_clear_queued (dec);
      if (dec_class->stop) {
	if (!dec_class->stop (dec)) {
	  goto stop_failed;
	}
      }      
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      if (dec_class->clean) {
	if (!dec_class->clean (dec)) {
	  goto clean_failed;
	}
      }      
      break;
    default:
      break;
  }

  return ret;

  /* ERRORS */
init_failed:
  {
    GST_ELEMENT_ERROR (dec, LIBRARY, INIT,
        (NULL), ("Failed to initialize decoder"));
    return GST_STATE_CHANGE_FAILURE;
  }
reset_failed:
  {
    GST_ELEMENT_ERROR (dec, LIBRARY, INIT,
        (NULL), ("Failed to reset decoder"));
    return GST_STATE_CHANGE_FAILURE;
  }
stop_failed:
  {
    GST_ELEMENT_ERROR (dec, LIBRARY, INIT,
        (NULL), ("Failed to stop decoder"));
    return GST_STATE_CHANGE_FAILURE;
  }
clean_failed:
  {
    GST_ELEMENT_ERROR (dec, LIBRARY, INIT,
        (NULL), ("Failed to clean decoder"));
    return GST_STATE_CHANGE_FAILURE;
  }  
}

static void
gst_base_decode_clear_queued (GstBaseDecode * dec)
{
  g_list_foreach (dec->queued, (GFunc) gst_mini_object_unref, NULL);
  g_list_free (dec->queued);
  dec->queued = NULL;
}

static GstFlowReturn
gst_base_decode_flush_queued (GstBaseDecode * dec)
{
  GstFlowReturn ret = GST_FLOW_OK;

  /* iterate ouput queue and push downstream */
  while (dec->queued) {
    GstBuffer *buf = GST_BUFFER_CAST (dec->queued->data);

    GST_LOG_OBJECT (dec, "pushing buffer %p, timestamp %"
        GST_TIME_FORMAT ", duration %" GST_TIME_FORMAT, buf,
        GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf)),
        GST_TIME_ARGS (GST_BUFFER_DURATION (buf)));

    ret = gst_pad_push (dec->srcpad, buf);
    dec->queued = g_list_delete_link (dec->queued, dec->queued);
  }

  return ret;
}

static gboolean
gst_base_decode_clip_buffer (GstBaseDecode * dec, GstBuffer * buffer)
{
  gboolean res = TRUE;
  GstClockTime in_ts, in_dur, stop;
  gint64 cstart, cstop;

  in_ts = GST_BUFFER_TIMESTAMP (buffer);
  in_dur = GST_BUFFER_DURATION (buffer);

  GST_LOG_OBJECT (dec,
      "timestamp:%" GST_TIME_FORMAT " , duration:%" GST_TIME_FORMAT,
      GST_TIME_ARGS (in_ts), GST_TIME_ARGS (in_dur));

  /* can't clip without TIME segment */
  if (dec->segment.format != GST_FORMAT_TIME)
    goto beach;

  /* we need a start time */
  if (!GST_CLOCK_TIME_IS_VALID (in_ts))
    goto beach;

  /* generate valid stop, if duration unknown, we have unknown stop */
  stop =
      GST_CLOCK_TIME_IS_VALID (in_dur) ? (in_ts + in_dur) : GST_CLOCK_TIME_NONE;

  /* now clip */
  if (!(res = gst_segment_clip (&dec->segment, GST_FORMAT_TIME,
              in_ts, stop, &cstart, &cstop)))
    goto beach;

  /* update timestamp and possibly duration if the clipped stop time is
   * valid */
  GST_BUFFER_TIMESTAMP (buffer) = cstart;
  if (GST_CLOCK_TIME_IS_VALID (cstop))
    GST_BUFFER_DURATION (buffer) = cstop - cstart;

beach:
  return res;
}

static gboolean
gst_base_decode_sink_event (GstPad * pad, GstEvent * event)
{
  gboolean ret = TRUE;
  GstBaseDecode *dec;
  GstBaseDecodeClass *dec_class;

  dec = GST_BASE_DECODE (GST_PAD_PARENT (pad));
  dec_class = GST_BASE_DECODE_GET_CLASS (dec);

  GST_LOG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
  
  switch (GST_EVENT_TYPE (event)) {
  case GST_EVENT_NEWSEGMENT:
    {
      GstFormat format;
      gdouble rate, arate;
      gint64 start, stop, time;
      gboolean update;
      
      if (dec->segment.rate < 0.0) {
        GST_DEBUG_OBJECT (dec, "flushing queued buffers");
        gst_base_decode_flush_queued (dec);
      }

      gst_event_parse_new_segment_full (event, &update, &rate, &arate, &format,
					&start, &stop, &time);

      if (format != GST_FORMAT_TIME || !GST_CLOCK_TIME_IS_VALID (start)) {
        GST_WARNING ("No time in newsegment event %p (format is %s)",
		     event, gst_format_get_name (format));
	gst_event_unref (event);
      } else {
        ret = gst_pad_push_event (dec->srcpad, event);
	gst_segment_set_newsegment_full (&dec->segment, update, rate, arate,
					 format, start, stop, time);
      }
      break;
    }
  case GST_EVENT_FLUSH_START:
    {
      ret = gst_pad_push_event (dec->srcpad, event);
      break;  
    }
  case GST_EVENT_FLUSH_STOP:
    {
      gst_base_decode_clear_queued (dec);
      if (dec_class->reset) {
	ret = dec_class->reset (dec);
      }
      ret = gst_pad_push_event (dec->srcpad, event);
      break;  
    }
  default:
    {
      ret = gst_pad_push_event (dec->srcpad, event);
      break;
    }
  }

  return ret;
}

#ifndef GST_DISABLE_INDEX
static gboolean
gst_base_decode_index_seek (GstBaseDecode *dec, GstIndex *index, 
			    GstPad * pad, GstEvent * event)
{
  GstIndexEntry *entry;
  gdouble rate;
  GstFormat format;
  GstSeekFlags flags;
  GstSeekType cur_type, stop_type;
  gint64 cur, stop;
  gint index_id;

  gst_event_parse_seek (event, &rate, &format, &flags,
			&cur_type, &cur, &stop_type, &stop);

  gst_index_get_writer_id (index, GST_OBJECT (dec), &index_id);
  entry = gst_index_get_assoc_entry (index, 
				     index_id,
				     GST_INDEX_LOOKUP_BEFORE, 
				     GST_ASSOCIATION_FLAG_KEY_UNIT, 
				     format, cur);

  if (entry && gst_pad_is_linked (dec->sinkpad)) {
    const GstFormat *peer_formats, *try_formats;

    /* since we know the exact byteoffset of the frame, 
       make sure to seek on bytes first */
    const GstFormat try_all_formats[] = {
      GST_FORMAT_BYTES,
      GST_FORMAT_TIME,
      0
    };

    try_formats = try_all_formats;

#if 0 
    /* FIXE ME */
    peer_formats = gst_pad_get_formats (GST_PAD_PEER (dec->sinkpad));
#else
    peer_formats = try_all_formats;
#endif

    while (gst_formats_contains (peer_formats, *try_formats)) {
      gint64 value;

      if (gst_index_entry_assoc_map (entry, *try_formats, &value)) {
        GstEvent *seek_event;

        GST_DEBUG_OBJECT (dec, "index %s %" G_GINT64_FORMAT
			  " -> %s %" G_GINT64_FORMAT,
			  gst_format_get_details (format)->nick,
			  cur, 
			  gst_format_get_details (*try_formats)->nick, 
			  value);
	
        seek_event = gst_event_new_seek (rate, *try_formats, flags, 
					 cur_type, value, stop_type, stop);
	
        if (gst_pad_push_event (dec->sinkpad, seek_event)) {
          return TRUE;
        }
      }

      try_formats++;
    }
  }

  return FALSE;
}
#endif

static gboolean
gst_base_decode_normal_seek (GstBaseDecode *dec, GstPad *pad, GstEvent *event)
{
  gdouble rate;
  GstFormat format, conv;
  GstSeekFlags flags;
  GstSeekType cur_type, stop_type;
  gint64 cur, stop;
  gint64 time_cur, bytes_cur;
  gint64 time_stop, bytes_stop;
  gboolean res;
  GstEvent *peer_event;
  GstBaseDecodeClass *dec_class;

  dec_class = GST_BASE_DECODE_GET_CLASS (dec);

  gst_event_parse_seek (event, &rate, &format, &flags,
			&cur_type, &cur, &stop_type, &stop); 

  res = FALSE;

  /* Try to seek in time */
  if (dec_class->src_convert) {
    conv = GST_FORMAT_TIME;
    if (!dec_class->src_convert(dec, pad, format, cur, &conv, &time_cur))
      goto convert_failed;
    if (!dec_class->src_convert (dec, pad, format, stop, &conv, &time_stop))
      goto convert_failed;

    GST_DEBUG ("seek to time %" GST_TIME_FORMAT "-%" GST_TIME_FORMAT,
	       GST_TIME_ARGS (time_cur), GST_TIME_ARGS (time_stop));
    
    peer_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags,
				     cur_type, time_cur, stop_type, time_stop);
    
    res = gst_pad_push_event (dec->sinkpad, peer_event);
  }

  /* Try seek in bytes if seek in time failed */
  if (!res && dec_class->sink_convert) {
    conv = GST_FORMAT_BYTES;
    if (!dec_class->sink_convert (dec, pad, GST_FORMAT_TIME, time_cur,
				  &conv, &bytes_cur))
      goto convert_failed;
    if (!dec_class->sink_convert (dec, pad, GST_FORMAT_TIME, time_stop,
				  &conv, &bytes_stop))
      goto convert_failed;
    
    peer_event =
      gst_event_new_seek (rate, GST_FORMAT_BYTES, flags,
			  cur_type, bytes_cur, stop_type, bytes_stop);
    
    res = gst_pad_push_event (dec->sinkpad, peer_event);
  }

  return res;

  /* ERRORS */
 convert_failed:
  {
    GST_DEBUG_OBJECT (dec, "failed to convert format %u", format);
    return FALSE;
  }
}

static gboolean
gst_base_decode_seek (GstBaseDecode *dec, GstPad *pad, GstEvent *event)
{
  gboolean res;

#ifndef GST_DISABLE_INDEX
  GstIndex *index = gst_element_get_index (GST_ELEMENT (dec));
  if (index) {
    res = gst_base_decode_index_seek (dec, index, pad, event);
    gst_object_unref (index);
  } else
#endif
    res = gst_base_decode_normal_seek (dec, pad, event);

  return res;
}

static gboolean
gst_base_decode_src_event (GstPad * pad, GstEvent * event)
{
  gboolean res;
  GstBaseDecode *dec;
  GstBaseDecodeClass *dec_class;
  
  dec = GST_BASE_DECODE (GST_PAD_PARENT (pad));
  dec_class = GST_BASE_DECODE_GET_CLASS (dec);

  switch (GST_EVENT_TYPE (event)) {
  case GST_EVENT_SEEK:{
    gst_event_ref (event);
    res = gst_pad_push_event (dec->sinkpad, event);
    if (!res) {
      /* FIXME: maybe it is possible to implement 
	 src_convert and sink_convert here if we 
	 know the type of decoder we are... */
      res = gst_base_decode_seek (dec, pad, event);
    }
    gst_event_unref (event);
    break;
  }
  default:
    res = gst_pad_push_event (dec->sinkpad, event);
    break;
  }
  return res;
}

static GstFlowReturn 
gst_base_decode_chain (GstPad * pad,  GstBuffer * buffer)
{
  GstBaseDecode *dec;
  GstBaseDecodeClass *dec_class;
  GstBuffer *out_buffer;
  GstFlowReturn ret;

  dec = GST_BASE_DECODE (GST_PAD_PARENT (pad));
  dec_class = GST_BASE_DECODE_GET_CLASS (dec);

  /* Handle discontinuous buffers */
  if (GST_BUFFER_IS_DISCONT (buffer)) {
    GST_LOG_OBJECT (dec, "DISCONT buffer received");
    if (dec_class->handle_discont) {
      dec_class->handle_discont (dec);
    }
  }

  /* Decode buffer */
  out_buffer = NULL;
  ret = dec_class->decode (dec, buffer, &out_buffer);
  if (ret == GST_FLOW_OK && out_buffer) {

    /* Clip buffer */
    if (!gst_base_decode_clip_buffer (dec, out_buffer)) {
      GST_LOG_OBJECT (dec, "Dropping buffer (clipped)");
      gst_buffer_unref (out_buffer);
      goto done;
    }

    /* FIXME: should decode() be responsible for pushing
       the buffers? */
    if (dec->segment.rate >= 0.0) {
      GST_LOG_OBJECT (dec, "pushing buffer %p, timestamp %"
		      GST_TIME_FORMAT ", duration %" GST_TIME_FORMAT,
		      out_buffer,
		      GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (out_buffer)),
		      GST_TIME_ARGS (GST_BUFFER_DURATION (out_buffer)));
      
      ret = gst_pad_push (dec->srcpad, out_buffer);
      GST_DEBUG_OBJECT (dec, "pushed with result %s",
			gst_flow_get_name (ret));
    } else {
      GST_DEBUG_OBJECT (dec, "queued frame");
      dec->queued = g_list_prepend (dec->queued, out_buffer);
      ret = GST_FLOW_OK;
    }
  } else if (ret != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (dec, "exit decode loop, reason: %s",
		      gst_flow_get_name (ret));    
  } else {
    GST_DEBUG_OBJECT (dec, "exit decode loop, reason: NULL decoded buffer ");
  }

 done:
  gst_buffer_unref (buffer);

  return ret;
}
