/* GStreamer
 * Copyright (C) 2001 RidgeRun, Inc. (www.ridgerun.com)
 *
 * gstautoplugcache.c: Data cache for the dynamic autoplugger
 *
 * 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.
 */

#include <gst/gst.h>

GstElementDetails gst_autoplugcache_details = {
  "AutoplugCache",
  "Generic",
  "LGPL",
  "Data cache for the dynamic autoplugger",
  VERSION,
  "Erik Walthinsen <omega@temple-baptist.com>",
  "(C) 2001 RidgeRun, Inc. (www.ridgerun.com)",
};

#define GST_TYPE_AUTOPLUGCACHE \
  (gst_autoplugcache_get_type())
#define GST_AUTOPLUGCACHE(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AUTOPLUGCACHE,GstAutoplugCache))
#define GST_AUTOPLUGCACHE_CLASS(klass) \
  (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AUTOPLUGCACHE,GstAutoplugCacheClass))
#define GST_IS_AUTOPLUGCACHE(obj) \
  (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AUTOPLUGCACHE))
#define GST_IS_AUTOPLUGCACHE_CLASS(obj) \
  (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AUTOPLUGCACHE))

typedef struct _GstAutoplugCache GstAutoplugCache;
typedef struct _GstAutoplugCacheClass GstAutoplugCacheClass;

struct _GstAutoplugCache {
  GstElement element;

  GstPad *sinkpad, *srcpad;

  gboolean caps_proxy;

  GList *cache;
  GList *cache_start;
  gint buffer_count;
  GList *current_playout;
  gboolean fire_empty;
  gboolean fire_first;
};

struct _GstAutoplugCacheClass {
  GstElementClass parent_class;

  void		(*first_buffer)		(GstElement *element, GstBuffer *buf);
  void		(*cache_empty)		(GstElement *element);
};


/* Cache signals and args */
enum {
  FIRST_BUFFER,
  CACHE_EMPTY,
  LAST_SIGNAL
};

enum {
  ARG_0,
  ARG_BUFFER_COUNT,
  ARG_CAPS_PROXY,
  ARG_RESET
};


static void			gst_autoplugcache_class_init	(GstAutoplugCacheClass *klass);
static void			gst_autoplugcache_init		(GstAutoplugCache *cache);

static void			gst_autoplugcache_set_property	(GObject *object, guint prop_id, 
								 const GValue *value, GParamSpec *pspec);
static void			gst_autoplugcache_get_property	(GObject *object, guint prop_id, 
								 GValue *value, GParamSpec *pspec);

static void			gst_autoplugcache_loop		(GstElement *element);

static GstElementStateReturn	gst_autoplugcache_change_state	(GstElement *element);


static GstElementClass *parent_class = NULL;
static guint gst_autoplugcache_signals[LAST_SIGNAL] = { 0 };

GType
gst_autoplugcache_get_type(void) {
  static GType autoplugcache_type = 0;

  if (!autoplugcache_type) {
    static const GTypeInfo autoplugcache_info = {
      sizeof(GstAutoplugCacheClass),
      NULL,
      NULL,
      (GClassInitFunc)gst_autoplugcache_class_init,
      NULL,
      NULL,
      sizeof(GstAutoplugCache),
      0,
      (GInstanceInitFunc)gst_autoplugcache_init,
    };
    autoplugcache_type = g_type_register_static (GST_TYPE_ELEMENT, "GstAutoplugCache", &autoplugcache_info, 0);
  }
  return autoplugcache_type;
}

static void
gst_autoplugcache_class_init (GstAutoplugCacheClass *klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = (GObjectClass*)klass;
  gstelement_class = (GstElementClass*)klass;

  parent_class = g_type_class_ref (GST_TYPE_ELEMENT);

  g_object_class_install_property(G_OBJECT_CLASS(klass), ARG_BUFFER_COUNT,
    g_param_spec_int("buffer_count","buffer_count","buffer_count",
                     0,G_MAXINT,0,G_PARAM_READABLE)); /* CHECKME! */
  g_object_class_install_property(G_OBJECT_CLASS(klass), ARG_CAPS_PROXY,
    g_param_spec_boolean("caps_proxy","caps_proxy","caps_proxy",
                         FALSE,G_PARAM_READWRITE)); /* CHECKME! */
  g_object_class_install_property(G_OBJECT_CLASS(klass), ARG_RESET,
    g_param_spec_boolean("reset","reset","reset",
                         FALSE,G_PARAM_WRITABLE)); /* CHECKME! */

  gst_autoplugcache_signals[FIRST_BUFFER] =
    g_signal_new ("first_buffer", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST,
                    G_STRUCT_OFFSET (GstAutoplugCacheClass, first_buffer), NULL, NULL,
                    g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
                    G_TYPE_POINTER);
  gst_autoplugcache_signals[CACHE_EMPTY] =
    g_signal_new ("cache_empty", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST,
                    G_STRUCT_OFFSET (GstAutoplugCacheClass, cache_empty), NULL, NULL,
                    g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);

  gobject_class->set_property = gst_autoplugcache_set_property;
  gobject_class->get_property = gst_autoplugcache_get_property;

  gstelement_class->change_state = gst_autoplugcache_change_state;
}

static void
gst_autoplugcache_init (GstAutoplugCache *cache)
{
  gst_element_set_loop_function(GST_ELEMENT(cache), GST_DEBUG_FUNCPTR(gst_autoplugcache_loop));

  cache->sinkpad = gst_pad_new ("sink", GST_PAD_SINK);
/*  gst_pad_set_negotiate_function (cache->sinkpad, gst_autoplugcache_nego_sink); */
  gst_element_add_pad (GST_ELEMENT(cache), cache->sinkpad);

  cache->srcpad = gst_pad_new ("src", GST_PAD_SRC);
/*  gst_pad_set_negotiate_function (cache->srcpad, gst_autoplugcache_nego_src); */
  gst_element_add_pad (GST_ELEMENT(cache), cache->srcpad);

  cache->caps_proxy = FALSE;

  /* provide a zero basis for the cache */
  cache->cache = g_list_prepend(NULL, NULL);
  cache->cache_start = cache->cache;
  cache->buffer_count = 0;
  cache->current_playout = 0;
  cache->fire_empty = FALSE;
  cache->fire_first = FALSE;
}

static void
gst_autoplugcache_loop (GstElement *element)
{
  GstAutoplugCache *cache;
  GstBuffer *buf = NULL;

  cache = GST_AUTOPLUGCACHE (element);

  /* Theory:
   * The cache is a doubly-linked list.  The front of the list is the most recent
   * buffer, the end of the list is the first buffer.  The playout pointer always
   * points to the latest buffer sent out the end.  cache points to the front
   * (most reccent) of the list at all times.  cache_start points to the first 
   * buffer, i.e. the end of the list.
   * If the playout pointer does not have a prev (towards the most recent) buffer 
   * (== NULL), a buffer must be pulled from the sink pad and added to the cache.
   * When the playout pointer gets reset (as in a set_property), the cache is walked
   * without problems, because the playout pointer has a non-NULL next.  When
   * the playout pointer hits the end of cache again it has to start pulling.
   */

  /* the first time through, the current_playout pointer is going to be NULL */
  if (cache->current_playout == NULL) {
    /* get a buffer */
    buf = gst_pad_pull (cache->sinkpad);
    if (GST_IS_EVENT (buf)) {
      gst_pad_event_default (cache->sinkpad, GST_EVENT (buf));
      return;
    }

    /* add it to the cache, though cache == NULL */
    gst_buffer_ref (buf);
    cache->cache = g_list_prepend (cache->cache, buf);
    cache->buffer_count++;

    /* set the current_playout pointer */
    cache->current_playout = cache->cache;

    g_signal_emit (G_OBJECT(cache), gst_autoplugcache_signals[FIRST_BUFFER], 0, buf);

    /* send the buffer on its way */
    gst_pad_push (cache->srcpad, buf);
  }
  /* the steady state is where the playout is at the front of the cache */
  else if (g_list_previous(cache->current_playout) == NULL) {

    /* if we've been told to fire an empty signal (after a reset) */
    if (cache->fire_empty) {
      int oldstate = GST_STATE(cache);
      GST_DEBUG(0,"at front of cache, about to pull, but firing signal");
      gst_object_ref (GST_OBJECT (cache));
      g_signal_emit (G_OBJECT(cache), gst_autoplugcache_signals[CACHE_EMPTY], 0, NULL);
      if (GST_STATE(cache) != oldstate) {
        gst_object_unref (GST_OBJECT (cache));
        GST_DEBUG(GST_CAT_AUTOPLUG, "state changed during signal, aborting");
	return;
      }
      gst_object_unref (GST_OBJECT (cache));
    }

    /* get a buffer */
    buf = gst_pad_pull (cache->sinkpad);
    if (GST_IS_EVENT (buf)) {
      gst_pad_event_default (cache->sinkpad, GST_EVENT (buf));
      return;
    }

    /* add it to the front of the cache */
    gst_buffer_ref (buf);
    cache->cache = g_list_prepend (cache->cache, buf);
    cache->buffer_count++;

    /* set the current_playout pointer */
    cache->current_playout = cache->cache;

    /* send the buffer on its way */
    gst_pad_push (cache->srcpad, buf);
  }

  /* otherwise we're trundling through existing cached buffers */
  else {
    /* move the current_playout pointer */
    cache->current_playout = g_list_previous (cache->current_playout);

    if (cache->fire_first) {
      g_signal_emit (G_OBJECT(cache), gst_autoplugcache_signals[FIRST_BUFFER], 0, buf);
      cache->fire_first = FALSE;
    }

    /* push that buffer */
    gst_pad_push (cache->srcpad, GST_BUFFER(cache->current_playout->data));
  }
}

static GstElementStateReturn
gst_autoplugcache_change_state (GstElement *element)
{
  /* FIXME this should do something like free the cache on ->NULL */
  if (GST_ELEMENT_CLASS (parent_class)->change_state)
    return GST_ELEMENT_CLASS (parent_class)->change_state (element);

  return GST_STATE_SUCCESS;
}

static void
gst_autoplugcache_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
  GstAutoplugCache *cache;

  cache = GST_AUTOPLUGCACHE (object);

  switch (prop_id) {
    case ARG_CAPS_PROXY:
      cache->caps_proxy = g_value_get_boolean (value);
GST_DEBUG(0,"caps_proxy is %d",cache->caps_proxy);
      if (cache->caps_proxy) {
      } else {
      }
      break;
    case ARG_RESET:
      /* no idea why anyone would set this to FALSE, but just in case ;-) */
      if (g_value_get_boolean (value)) {
        GST_DEBUG(0,"resetting playout pointer");
        /* reset the playout pointer to the begining again */
        cache->current_playout = cache->cache_start;
        /* now we can fire a signal when the cache runs dry */
        cache->fire_empty = TRUE;
        /* also set it up to fire the first_buffer signal again */
        cache->fire_first = TRUE;
      }
      break;
    default:
      break;
  }
}

static void
gst_autoplugcache_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
  GstAutoplugCache *cache;

  cache = GST_AUTOPLUGCACHE (object);

  switch (prop_id) {
    case ARG_BUFFER_COUNT:
      g_value_set_int (value, cache->buffer_count);
      break;
    case ARG_CAPS_PROXY:
      g_value_set_boolean (value, cache->caps_proxy);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
plugin_init (GModule *module, GstPlugin *plugin)
{
  GstElementFactory *factory;

  factory = gst_element_factory_new ("autoplugcache", GST_TYPE_AUTOPLUGCACHE,
                                    &gst_autoplugcache_details);
  g_return_val_if_fail (factory != NULL, FALSE);

  gst_plugin_add_feature (plugin, GST_PLUGIN_FEATURE (factory));

  return TRUE;
}

GstPluginDesc plugin_desc = {
  GST_VERSION_MAJOR,
  GST_VERSION_MINOR,
  "autoplugcache",
  plugin_init
};

