/* GStreamer
 * Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/* TODO/FIXME:
 *
 * * BUFFERING MESSAGES
 * ** How/Where do we deal with buffering messages from a new/prerolling
 *    source ? Ideally we want to re-use the same sourcebin ?
 * ** Remember last buffering messages per source handler, if the SourceEntry
 *    group_id is the one being currently outputted on the source ghostpads,
 *    post the (last) buffering messages.
 *    If no group_id is being outputted (still prerolling), then output
 *    the messages directly
 *
 * * ASYNC HANDLING
 * ** URIDECODEBIN3 is not async-aware.
 *
 * * GAPLESS HANDLING
 * ** Correlate group_id and URI to know when/which stream is being outputted/started
 */

/**
 * SECTION:element-uridecodebin3
 * @title: uridecodebin3
 *
 * Decodes data from a URI into raw media. It selects a source element that can
 * handle the given #GstURIDecodeBin3:uri scheme and connects it to a decodebin.
 */

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

#include <string.h>

#include <gst/gst.h>
#include <gst/gst-i18n-plugin.h>
#include <gst/pbutils/missing-plugins.h>

#include "gstplay-enum.h"
#include "gstrawcaps.h"
#include "gstplayback.h"
#include "gstplaybackutils.h"

#define GST_TYPE_URI_DECODE_BIN3 \
  (gst_uri_decode_bin3_get_type())
#define GST_URI_DECODE_BIN3(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_URI_DECODE_BIN3,GstURIDecodeBin3))
#define GST_URI_DECODE_BIN3_CLASS(klass) \
  (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_URI_DECODE_BIN3,GstURIDecodeBin3Class))
#define GST_IS_URI_DECODE_BIN3(obj) \
  (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_URI_DECODE_BIN3))
#define GST_IS_URI_DECODE_BIN3_CLASS(klass) \
  (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_URI_DECODE_BIN3))
#define GST_URI_DECODE_BIN3_CAST(obj) ((GstURIDecodeBin3 *) (obj))

typedef struct _GstSourceGroup GstSourceGroup;
typedef struct _GstURIDecodeBin3 GstURIDecodeBin3;
typedef struct _GstURIDecodeBin3Class GstURIDecodeBin3Class;

#define GST_URI_DECODE_BIN3_LOCK(dec) (g_mutex_lock(&((GstURIDecodeBin3*)(dec))->lock))
#define GST_URI_DECODE_BIN3_UNLOCK(dec) (g_mutex_unlock(&((GstURIDecodeBin3*)(dec))->lock))

typedef struct _GstPlayItem GstPlayItem;
typedef struct _GstSourceItem GstSourceItem;
typedef struct _GstSourceHandler GstSourceHandler;
typedef struct _OutputPad OutputPad;

/* A structure describing a play item, which travels through the elements
 * over time. */
struct _GstPlayItem
{
  GstURIDecodeBin3 *uridecodebin;

  /* Main URI */
  GstSourceItem *main_item;

  /* Auxiliary URI */
  /* FIXME : Replace by a list later */
  GstSourceItem *sub_item;

  /* The group_id used to identify this play item via STREAM_START events
   * This is the group_id which will be used externally (i.e. rewritten
   * to outgoing STREAM_START events and in emitted signals).
   * The urisourcebin-specific group_id is located in GstSourceItem */
  guint group_id;

  /* Is this play item the one being currently outputted by decodebin3
   * and on our source ghostpads */
  gboolean currently_outputted;
};

struct _GstSourceItem
{
  /* The GstPlayItem to which this GstSourceItem belongs to */
  GstPlayItem *play_item;

  gchar *uri;

  /* The urisourcebin controlling this uri
   * Can be NULL */
  GstSourceHandler *handler;

  /* Last buffering information */
  gint last_perc;
  GstMessage *last_buffering_message;

  /* The groupid created by urisourcebin for this uri */
  guint internal_groupid;

  /* FIXME : Add tag lists and other uri-specific items here ? */
};

/* Structure wrapping everything related to a urisourcebin */
struct _GstSourceHandler
{
  GstURIDecodeBin3 *uridecodebin;

  GstElement *urisourcebin;

  /* Signal handlers */
  gulong pad_added_id;
  gulong pad_removed_id;
  gulong source_setup_id;
  gulong about_to_finish_id;

  /* TRUE if the controlled urisourcebin was added to uridecodebin */
  gboolean active;

  /* whether urisourcebin is drained or not.
   * Reset if/when setting a new URI */
  gboolean drained;

  /* Whether urisourcebin posted EOS on all pads and
   * there is no pending entry */
  gboolean is_eos;

  /* TRUE if the urisourcebin handles main item */
  gboolean is_main_source;

  /* buffering message stored for after switching */
  GstMessage *pending_buffering_msg;
};

/* Controls an output source pad */
struct _OutputPad
{
  GstURIDecodeBin3 *uridecodebin;

  GstPad *target_pad;
  GstPad *ghost_pad;

  /* Downstream event probe id */
  gulong probe_id;

  /* TRUE if the pad saw EOS. Resetted to FALSE on STREAM_START */
  gboolean is_eos;

  /* The last seen (i.e. current) group_id
   * Can be (guint)-1 if no group_id was seen yet */
  guint current_group_id;
};

/**
 * GstURIDecodeBin3
 *
 * uridecodebin3 element struct
 */
struct _GstURIDecodeBin3
{
  GstBin parent_instance;

  GMutex lock;                  /* lock for constructing */

  /* Properties */
  GstElement *source;
  guint64 connection_speed;     /* In bits/sec (0 = unknown) */
  GstCaps *caps;
  guint64 buffer_duration;      /* When buffering, buffer duration (ns) */
  guint buffer_size;            /* When buffering, buffer size (bytes) */
  gboolean download;
  gboolean use_buffering;
  guint64 ring_buffer_max_size;

  GList *play_items;            /* List of GstPlayItem ordered by time of
                                 * creation. Head of list is therefore the
                                 * current (or pending if initial) one being
                                 * outputted */
  GstPlayItem *current;         /* Currently active GstPlayItem. Can be NULL
                                 * if no entry is active yet (i.e. no source
                                 * pads) */

  /* sources.
   * FIXME : Replace by a more modular system later on */
  GstSourceHandler *main_handler;
  GstSourceHandler *sub_handler;

  /* URI handling
   * FIXME : Switch to a playlist-based API */
  gchar *uri;
  gboolean uri_changed;         /* TRUE if uri changed */
  gchar *suburi;
  gboolean suburi_changed;      /* TRUE if suburi changed */

  /* A global decodebin3 that's used to actually do decoding */
  GstElement *decodebin;

  /* db3 signals */
  gulong db_pad_added_id;
  gulong db_pad_removed_id;
  gulong db_select_stream_id;
  gulong db_about_to_finish_id;

  GList *output_pads;           /* List of OutputPad */

  GList *source_handlers;       /* List of SourceHandler */

  /* Whether we already signalled about-to-finish or not
   * FIXME: Track this by group-id ! */
  gboolean posted_about_to_finish;
};

struct _GstURIDecodeBin3Class
{
  GstBinClass parent_class;

    gint (*select_stream) (GstURIDecodeBin3 * dbin,
      GstStreamCollection * collection, GstStream * stream);
};

GST_DEBUG_CATEGORY_STATIC (gst_uri_decode_bin3_debug);
#define GST_CAT_DEFAULT gst_uri_decode_bin3_debug

/* signals */
enum
{
  SIGNAL_SELECT_STREAM,
  SIGNAL_SOURCE_SETUP,
  SIGNAL_ABOUT_TO_FINISH,
  LAST_SIGNAL
};

#if 0
static GstStaticCaps raw_audio_caps = GST_STATIC_CAPS ("audio/x-raw(ANY)");
static GstStaticCaps raw_video_caps = GST_STATIC_CAPS ("video/x-raw(ANY)");
#endif

/* properties */
#define DEFAULT_PROP_URI            NULL
#define DEFAULT_PROP_SUBURI            NULL
#define DEFAULT_PROP_SOURCE         NULL
#define DEFAULT_CONNECTION_SPEED    0
#define DEFAULT_CAPS                (gst_static_caps_get (&default_raw_caps))
#define DEFAULT_BUFFER_DURATION     -1
#define DEFAULT_BUFFER_SIZE         -1
#define DEFAULT_DOWNLOAD            FALSE
#define DEFAULT_USE_BUFFERING       FALSE
#define DEFAULT_RING_BUFFER_MAX_SIZE 0

enum
{
  PROP_0,
  PROP_URI,
  PROP_CURRENT_URI,
  PROP_SUBURI,
  PROP_CURRENT_SUBURI,
  PROP_SOURCE,
  PROP_CONNECTION_SPEED,
  PROP_BUFFER_SIZE,
  PROP_BUFFER_DURATION,
  PROP_DOWNLOAD,
  PROP_USE_BUFFERING,
  PROP_RING_BUFFER_MAX_SIZE,
  PROP_CAPS
};

static guint gst_uri_decode_bin3_signals[LAST_SIGNAL] = { 0 };

static GstStaticCaps default_raw_caps = GST_STATIC_CAPS (DEFAULT_RAW_CAPS);

static GstStaticPadTemplate video_src_template =
GST_STATIC_PAD_TEMPLATE ("video_%u",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);

static GstStaticPadTemplate audio_src_template =
GST_STATIC_PAD_TEMPLATE ("audio_%u",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);

static GstStaticPadTemplate text_src_template =
GST_STATIC_PAD_TEMPLATE ("text_%u",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src_%u",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);

GType gst_uri_decode_bin3_get_type (void);
#define gst_uri_decode_bin3_parent_class parent_class
G_DEFINE_TYPE (GstURIDecodeBin3, gst_uri_decode_bin3, GST_TYPE_BIN);

#define REMOVE_SIGNAL(obj,id)            \
if (id) {                                \
  g_signal_handler_disconnect (obj, id); \
  id = 0;                                \
}

static void gst_uri_decode_bin3_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_uri_decode_bin3_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_uri_decode_bin3_finalize (GObject * obj);

static GstStateChangeReturn gst_uri_decode_bin3_change_state (GstElement *
    element, GstStateChange transition);

static gboolean
_gst_int_accumulator (GSignalInvocationHint * ihint,
    GValue * return_accu, const GValue * handler_return, gpointer dummy)
{
  gint res = g_value_get_int (handler_return);

  if (!(ihint->run_type & G_SIGNAL_RUN_CLEANUP))
    g_value_set_int (return_accu, res);

  if (res == -1)
    return TRUE;

  return FALSE;
}


static void
gst_uri_decode_bin3_class_init (GstURIDecodeBin3Class * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

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

  gobject_class->set_property = gst_uri_decode_bin3_set_property;
  gobject_class->get_property = gst_uri_decode_bin3_get_property;
  gobject_class->finalize = gst_uri_decode_bin3_finalize;

  g_object_class_install_property (gobject_class, PROP_URI,
      g_param_spec_string ("uri", "URI", "URI to decode",
          DEFAULT_PROP_URI, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_CURRENT_URI,
      g_param_spec_string ("current-uri", "Current URI",
          "The currently playing URI", NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_SUBURI,
      g_param_spec_string ("suburi", ".sub-URI", "Optional URI of a subtitle",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_CURRENT_SUBURI,
      g_param_spec_string ("current-suburi", "Current .sub-URI",
          "The currently playing URI of a subtitle",
          NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_SOURCE,
      g_param_spec_object ("source", "Source", "Source object used",
          GST_TYPE_ELEMENT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_CONNECTION_SPEED,
      g_param_spec_uint64 ("connection-speed", "Connection Speed",
          "Network connection speed in kbps (0 = unknown)",
          0, G_MAXUINT64 / 1000, DEFAULT_CONNECTION_SPEED,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_BUFFER_SIZE,
      g_param_spec_int ("buffer-size", "Buffer size (bytes)",
          "Buffer size when buffering streams (-1 default value)",
          -1, G_MAXINT, DEFAULT_BUFFER_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_BUFFER_DURATION,
      g_param_spec_int64 ("buffer-duration", "Buffer duration (ns)",
          "Buffer duration when buffering streams (-1 default value)",
          -1, G_MAXINT64, DEFAULT_BUFFER_DURATION,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstURIDecodeBin3::download:
   *
   * For certain media type, enable download buffering.
   */
  g_object_class_install_property (gobject_class, PROP_DOWNLOAD,
      g_param_spec_boolean ("download", "Download",
          "Attempt download buffering when buffering network streams",
          DEFAULT_DOWNLOAD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  /**
   * GstURIDecodeBin3::use-buffering:
   *
   * Emit BUFFERING messages based on low-/high-percent thresholds of the
   * demuxed or parsed data.
   * When download buffering is activated and used for the current media
   * type, this property does nothing. Otherwise perform buffering on the
   * demuxed or parsed media.
   */
  g_object_class_install_property (gobject_class, PROP_USE_BUFFERING,
      g_param_spec_boolean ("use-buffering", "Use Buffering",
          "Perform buffering on demuxed/parsed media",
          DEFAULT_USE_BUFFERING, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstURIDecodeBin3::ring-buffer-max-size
   *
   * The maximum size of the ring buffer in kilobytes. If set to 0, the ring
   * buffer is disabled. Default is 0.
   */
  g_object_class_install_property (gobject_class, PROP_RING_BUFFER_MAX_SIZE,
      g_param_spec_uint64 ("ring-buffer-max-size",
          "Max. ring buffer size (bytes)",
          "Max. amount of data in the ring buffer (bytes, 0 = ring buffer disabled)",
          0, G_MAXUINT, DEFAULT_RING_BUFFER_MAX_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_CAPS,
      g_param_spec_boxed ("caps", "Caps",
          "The caps on which to stop decoding. (NULL = default)",
          GST_TYPE_CAPS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstURIDecodebin3::select-stream
   * @decodebin: a #GstURIDecodebin3
   * @collection: a #GstStreamCollection
   * @stream: a #GstStream
   *
   * This signal is emitted whenever @decodebin needs to decide whether
   * to expose a @stream of a given @collection.
   *
   * Returns: 1 if the stream should be selected, 0 if it shouldn't be selected.
   * A value of -1 (default) lets @decodebin decide what to do with the stream.
   * */
  gst_uri_decode_bin3_signals[SIGNAL_SELECT_STREAM] =
      g_signal_new ("select-stream", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GstURIDecodeBin3Class, select_stream),
      _gst_int_accumulator, NULL, g_cclosure_marshal_generic,
      G_TYPE_INT, 2, GST_TYPE_STREAM_COLLECTION, GST_TYPE_STREAM);

  /**
   * GstURIDecodeBin3::source-setup:
   * @bin: the uridecodebin.
   * @source: source element
   *
   * This signal is emitted after a source element has been created, so
   * it can be configured by setting additional properties (e.g. set a
   * proxy server for an http source, or set the device and read speed for
   * an audio cd source).
   */
  gst_uri_decode_bin3_signals[SIGNAL_SOURCE_SETUP] =
      g_signal_new ("source-setup", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL,
      g_cclosure_marshal_generic, G_TYPE_NONE, 1, GST_TYPE_ELEMENT);
  /**
   * GstURIDecodeBin3::about-to-finish:
   *
   * This signal is emitted when the data for the selected URI is
   * entirely buffered and it is safe to specify anothe URI.
   */
  gst_uri_decode_bin3_signals[SIGNAL_ABOUT_TO_FINISH] =
      g_signal_new ("about-to-finish", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE,
      0, G_TYPE_NONE);


  gst_element_class_add_static_pad_template (gstelement_class,
      &video_src_template);
  gst_element_class_add_static_pad_template (gstelement_class,
      &audio_src_template);
  gst_element_class_add_static_pad_template (gstelement_class,
      &text_src_template);
  gst_element_class_add_static_pad_template (gstelement_class, &src_template);
  gst_element_class_set_static_metadata (gstelement_class,
      "URI Decoder", "Generic/Bin/Decoder",
      "Autoplug and decode an URI to raw media",
      "Edward Hervey <edward@centricular.com>, Jan Schmidt <jan@centricular.com>");

  gstelement_class->change_state = gst_uri_decode_bin3_change_state;

}

static GstPadProbeReturn
db_src_probe (GstPad * pad, GstPadProbeInfo * info, OutputPad * output)
{
  /* FIXME : IMPLEMENT */

  /* EOS : Mark pad as EOS */

  /* STREAM_START : Store group_id and check if currently active
   *  PlayEntry changed */

  return GST_PAD_PROBE_OK;
}

static OutputPad *
add_output_pad (GstURIDecodeBin3 * dec, GstPad * target_pad)
{
  OutputPad *output;
  gchar *pad_name;

  output = g_slice_new0 (OutputPad);

  GST_LOG_OBJECT (dec, "Created output %p", output);

  output->uridecodebin = dec;
  output->target_pad = target_pad;
  output->current_group_id = (guint) - 1;
  pad_name = gst_pad_get_name (target_pad);
  output->ghost_pad = gst_ghost_pad_new (pad_name, target_pad);
  g_free (pad_name);

  gst_pad_set_active (output->ghost_pad, TRUE);
  gst_element_add_pad (GST_ELEMENT (dec), output->ghost_pad);

  output->probe_id =
      gst_pad_add_probe (output->target_pad,
      GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, (GstPadProbeCallback) db_src_probe,
      output, NULL);

  /* FIXME: LOCK TO PROTECT PAD LIST */
  dec->output_pads = g_list_append (dec->output_pads, output);

  return output;
}

static void
db_pad_added_cb (GstElement * element, GstPad * pad, GstURIDecodeBin3 * dec)
{
  GST_DEBUG_OBJECT (dec, "Wrapping new pad %s:%s", GST_DEBUG_PAD_NAME (pad));

  if (GST_PAD_IS_SRC (pad))
    add_output_pad (dec, pad);
}

static void
db_pad_removed_cb (GstElement * element, GstPad * pad, GstURIDecodeBin3 * dec)
{
  GList *tmp;
  OutputPad *output = NULL;

  if (!GST_PAD_IS_SRC (pad))
    return;

  GST_DEBUG_OBJECT (dec, "pad %s:%s", GST_DEBUG_PAD_NAME (pad));
  /* FIXME: LOCK for list access */

  for (tmp = dec->output_pads; tmp; tmp = tmp->next) {
    OutputPad *cand = (OutputPad *) tmp->data;

    if (cand->target_pad == pad) {
      output = cand;
      dec->output_pads = g_list_delete_link (dec->output_pads, tmp);
      break;
    }
  }

  if (output) {
    GST_LOG_OBJECT (element, "Removing output %p", output);
    /* Remove source ghost pad */
    gst_ghost_pad_set_target ((GstGhostPad *) output->ghost_pad, NULL);
    gst_element_remove_pad ((GstElement *) dec, output->ghost_pad);

    /* FIXME : Update global/current PlayEntry group_id (did we switch ?) */

    /* Remove event probe */
    gst_pad_remove_probe (output->target_pad, output->probe_id);

    g_slice_free (OutputPad, output);
  }
}

static gint
db_select_stream_cb (GstElement * decodebin,
    GstStreamCollection * collection, GstStream * stream,
    GstURIDecodeBin3 * uridecodebin)
{
  gint response = -1;

  g_signal_emit (uridecodebin,
      gst_uri_decode_bin3_signals[SIGNAL_SELECT_STREAM], 0, collection, stream,
      &response);
  return response;
}

static void
db_about_to_finish_cb (GstElement * decodebin, GstURIDecodeBin3 * uridecodebin)
{
  if (!uridecodebin->posted_about_to_finish) {
    uridecodebin->posted_about_to_finish = TRUE;
    g_signal_emit (uridecodebin,
        gst_uri_decode_bin3_signals[SIGNAL_ABOUT_TO_FINISH], 0, NULL);
  }
}

static void
gst_uri_decode_bin3_init (GstURIDecodeBin3 * dec)
{
  g_mutex_init (&dec->lock);

  dec->caps = gst_static_caps_get (&default_raw_caps);

  dec->decodebin = gst_element_factory_make ("decodebin3", NULL);
  gst_bin_add (GST_BIN_CAST (dec), dec->decodebin);
  dec->db_pad_added_id =
      g_signal_connect (dec->decodebin, "pad-added",
      G_CALLBACK (db_pad_added_cb), dec);
  dec->db_pad_removed_id =
      g_signal_connect (dec->decodebin, "pad-removed",
      G_CALLBACK (db_pad_removed_cb), dec);
  dec->db_select_stream_id =
      g_signal_connect (dec->decodebin, "select-stream",
      G_CALLBACK (db_select_stream_cb), dec);
  dec->db_about_to_finish_id =
      g_signal_connect (dec->decodebin, "about-to-finish",
      G_CALLBACK (db_about_to_finish_cb), dec);

  GST_OBJECT_FLAG_SET (dec, GST_ELEMENT_FLAG_SOURCE);
  gst_bin_set_suppressed_flags (GST_BIN (dec),
      GST_ELEMENT_FLAG_SOURCE | GST_ELEMENT_FLAG_SINK);
}

static void
gst_uri_decode_bin3_finalize (GObject * obj)
{
  GstURIDecodeBin3 *dec = GST_URI_DECODE_BIN3 (obj);

  g_mutex_clear (&dec->lock);
  g_free (dec->uri);
  g_free (dec->suburi);

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

static GstStateChangeReturn
activate_source_item (GstSourceItem * item)
{
  GstSourceHandler *handler = item->handler;

  if (handler == NULL) {
    GST_WARNING ("Can't activate item without a handler");
    return GST_STATE_CHANGE_FAILURE;
  }

  g_object_set (handler->urisourcebin, "uri", item->uri, NULL);
  if (!handler->active) {
    gst_bin_add ((GstBin *) handler->uridecodebin, handler->urisourcebin);
    /* if (!gst_element_sync_state_with_parent (handler->urisourcebin)) */
    /*   return GST_STATE_CHANGE_FAILURE; */
    handler->active = TRUE;
  }

  return GST_STATE_CHANGE_SUCCESS;
}

static void
src_pad_added_cb (GstElement * element, GstPad * pad,
    GstSourceHandler * handler)
{
  GstURIDecodeBin3 *uridecodebin;
  GstPad *sinkpad = NULL;
  GstPadLinkReturn res;

  uridecodebin = handler->uridecodebin;

  GST_DEBUG_OBJECT (uridecodebin,
      "New pad %" GST_PTR_FORMAT " from source %" GST_PTR_FORMAT, pad, element);

  /* FIXME: Add probe to unify group_id and detect EOS */

  /* Try to link to main sink pad only if it's from a main handler */
  if (handler->is_main_source) {
    sinkpad = gst_element_get_static_pad (uridecodebin->decodebin, "sink");
    if (gst_pad_is_linked (sinkpad)) {
      gst_object_unref (sinkpad);
      sinkpad = NULL;
    }
  }

  if (sinkpad == NULL)
    sinkpad = gst_element_get_request_pad (uridecodebin->decodebin, "sink_%u");

  if (sinkpad) {
    GST_DEBUG_OBJECT (uridecodebin,
        "Linking %" GST_PTR_FORMAT " to %" GST_PTR_FORMAT, pad, sinkpad);
    res = gst_pad_link (pad, sinkpad);
    gst_object_unref (sinkpad);
    if (GST_PAD_LINK_FAILED (res))
      goto link_failed;
  }
  return;

link_failed:
  {
    GST_ERROR_OBJECT (uridecodebin,
        "failed to link pad %s:%s to decodebin, reason %s (%d)",
        GST_DEBUG_PAD_NAME (pad), gst_pad_link_get_name (res), res);
    return;
  }
}

static void
src_pad_removed_cb (GstElement * element, GstPad * pad,
    GstSourceHandler * handler)
{
  /* FIXME : IMPLEMENT */
}

static void
src_source_setup_cb (GstElement * element, GstElement * source,
    GstSourceHandler * handler)
{
  g_signal_emit (handler->uridecodebin,
      gst_uri_decode_bin3_signals[SIGNAL_SOURCE_SETUP], 0, source, NULL);
}

static void
src_about_to_finish_cb (GstElement * element, GstSourceHandler * handler)
{
  /* FIXME : check if all sources are done */
  if (!handler->uridecodebin->posted_about_to_finish) {
    handler->uridecodebin->posted_about_to_finish = TRUE;
    g_signal_emit (handler->uridecodebin,
        gst_uri_decode_bin3_signals[SIGNAL_ABOUT_TO_FINISH], 0, NULL);
  }
}

static GstSourceHandler *
new_source_handler (GstURIDecodeBin3 * uridecodebin, gboolean is_main)
{
  GstSourceHandler *handler;

  handler = g_slice_new0 (GstSourceHandler);

  handler->uridecodebin = uridecodebin;
  handler->is_main_source = is_main;
  handler->urisourcebin = gst_element_factory_make ("urisourcebin", NULL);
  /* Set pending properties */
  g_object_set (handler->urisourcebin,
      "connection-speed", uridecodebin->connection_speed / 1000,
      "download", uridecodebin->download,
      "use-buffering", uridecodebin->use_buffering,
      "buffer-duration", uridecodebin->buffer_duration,
      "buffer-size", uridecodebin->buffer_size,
      "ring-buffer-max-size", uridecodebin->ring_buffer_max_size, NULL);

  handler->pad_added_id =
      g_signal_connect (handler->urisourcebin, "pad-added",
      (GCallback) src_pad_added_cb, handler);
  handler->pad_removed_id =
      g_signal_connect (handler->urisourcebin, "pad-removed",
      (GCallback) src_pad_removed_cb, handler);
  handler->source_setup_id =
      g_signal_connect (handler->urisourcebin, "source-setup",
      (GCallback) src_source_setup_cb, handler);
  handler->about_to_finish_id =
      g_signal_connect (handler->urisourcebin, "about-to-finish",
      (GCallback) src_about_to_finish_cb, handler);

  uridecodebin->source_handlers =
      g_list_append (uridecodebin->source_handlers, handler);

  return handler;
}

static void
gst_uri_decode_bin3_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstURIDecodeBin3 *dec = GST_URI_DECODE_BIN3 (object);

  switch (prop_id) {
    case PROP_URI:
      if (dec->uri)
        g_free (dec->uri);
      dec->uri = g_value_dup_string (value);
      break;
    case PROP_SUBURI:
      if (dec->suburi)
        g_free (dec->suburi);
      dec->suburi = g_value_dup_string (value);
      break;
    case PROP_CONNECTION_SPEED:
      GST_URI_DECODE_BIN3_LOCK (dec);
      dec->connection_speed = g_value_get_uint64 (value) * 1000;
      GST_URI_DECODE_BIN3_UNLOCK (dec);
      break;
    case PROP_BUFFER_SIZE:
      dec->buffer_size = g_value_get_int (value);
      break;
    case PROP_BUFFER_DURATION:
      dec->buffer_duration = g_value_get_int64 (value);
      break;
    case PROP_DOWNLOAD:
      dec->download = g_value_get_boolean (value);
      break;
    case PROP_USE_BUFFERING:
      dec->use_buffering = g_value_get_boolean (value);
      break;
    case PROP_RING_BUFFER_MAX_SIZE:
      dec->ring_buffer_max_size = g_value_get_uint64 (value);
      break;
    case PROP_CAPS:
      GST_OBJECT_LOCK (dec);
      if (dec->caps)
        gst_caps_unref (dec->caps);
      dec->caps = g_value_dup_boxed (value);
      GST_OBJECT_UNLOCK (dec);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_uri_decode_bin3_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstURIDecodeBin3 *dec = GST_URI_DECODE_BIN3 (object);

  switch (prop_id) {
    case PROP_URI:
    {
      g_value_set_string (value, dec->uri);
      break;
    }
    case PROP_CURRENT_URI:
    {
      g_value_set_string (value, dec->suburi);
      break;
    }
    case PROP_SUBURI:
    {
      /* FIXME : Return current uri */
      g_value_set_string (value, dec->uri);
      break;
    }
    case PROP_CURRENT_SUBURI:
    {
      /* FIXME : Return current suburi */
      g_value_set_string (value, dec->suburi);
      break;
    }
    case PROP_SOURCE:
    {
      GST_OBJECT_LOCK (dec);
      g_value_set_object (value, dec->source);
      GST_OBJECT_UNLOCK (dec);
      break;
    }
    case PROP_CONNECTION_SPEED:
      GST_URI_DECODE_BIN3_LOCK (dec);
      g_value_set_uint64 (value, dec->connection_speed / 1000);
      GST_URI_DECODE_BIN3_UNLOCK (dec);
      break;
    case PROP_BUFFER_SIZE:
      GST_OBJECT_LOCK (dec);
      g_value_set_int (value, dec->buffer_size);
      GST_OBJECT_UNLOCK (dec);
      break;
    case PROP_BUFFER_DURATION:
      GST_OBJECT_LOCK (dec);
      g_value_set_int64 (value, dec->buffer_duration);
      GST_OBJECT_UNLOCK (dec);
      break;
    case PROP_DOWNLOAD:
      g_value_set_boolean (value, dec->download);
      break;
    case PROP_USE_BUFFERING:
      g_value_set_boolean (value, dec->use_buffering);
      break;
    case PROP_RING_BUFFER_MAX_SIZE:
      g_value_set_uint64 (value, dec->ring_buffer_max_size);
      break;
    case PROP_CAPS:
      GST_OBJECT_LOCK (dec);
      g_value_set_boxed (value, dec->caps);
      GST_OBJECT_UNLOCK (dec);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
free_source_handler (GstURIDecodeBin3 * uridecodebin,
    GstSourceHandler * handler)
{
  GST_LOG_OBJECT (uridecodebin, "source handler %p", handler);
  if (handler->active) {
    GST_LOG_OBJECT (uridecodebin, "Removing %" GST_PTR_FORMAT,
        handler->urisourcebin);
    gst_element_set_state (handler->urisourcebin, GST_STATE_NULL);
    gst_bin_remove ((GstBin *) uridecodebin, handler->urisourcebin);
  }
  uridecodebin->source_handlers =
      g_list_remove (uridecodebin->source_handlers, handler);
  g_slice_free (GstSourceHandler, handler);
}

static GstSourceItem *
new_source_item (GstURIDecodeBin3 * dec, GstPlayItem * item, gchar * uri)
{
  GstSourceItem *sourceitem = g_slice_new0 (GstSourceItem);

  sourceitem->play_item = item;
  sourceitem->uri = uri;

  return sourceitem;
}

static void
free_source_item (GstURIDecodeBin3 * uridecodebin, GstSourceItem * item)
{
  GST_LOG_OBJECT (uridecodebin, "source item %p", item);
  if (item->handler)
    free_source_handler (uridecodebin, item->handler);
  g_slice_free (GstSourceItem, item);
}

static GstPlayItem *
new_play_item (GstURIDecodeBin3 * dec, gchar * uri, gchar * suburi)
{
  GstPlayItem *item = g_slice_new0 (GstPlayItem);

  item->uridecodebin = dec;
  item->main_item = new_source_item (dec, item, uri);
  if (suburi)
    item->sub_item = new_source_item (dec, item, suburi);

  return item;
}

static void
free_play_item (GstURIDecodeBin3 * dec, GstPlayItem * item)
{
  GST_LOG_OBJECT (dec, "play item %p", item);
  if (item->main_item)
    free_source_item (dec, item->main_item);
  if (item->sub_item)
    free_source_item (dec, item->sub_item);

  g_slice_free (GstPlayItem, item);
}

/* Sync source handlers for the given play item. Might require creating/removing some
 * and/or configure the handlers accordingly */
static GstStateChangeReturn
assign_handlers_to_item (GstURIDecodeBin3 * dec, GstPlayItem * item)
{
  GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;

  /* FIXME : Go over existing handlers to see if we can assign some to the
   * given item */

  /* Create missing handlers */
  if (item->main_item->handler == NULL) {
    item->main_item->handler = new_source_handler (dec, TRUE);
    ret = activate_source_item (item->main_item);
    if (ret == GST_STATE_CHANGE_FAILURE)
      return ret;
  }

  if (item->sub_item && item->sub_item->handler) {
    item->sub_item->handler = new_source_handler (dec, FALSE);
    ret = activate_source_item (item->sub_item);
  }

  return ret;
}

/* Called to activate the next play item */
static GstStateChangeReturn
activate_next_play_item (GstURIDecodeBin3 * dec)
{
  GstPlayItem *item;
  GstStateChangeReturn ret;

  /* If there is no current play entry, create one from the uri/suburi
   * FIXME : Use a playlist API in the future */
  item = new_play_item (dec, dec->uri, dec->suburi);

  ret = assign_handlers_to_item (dec, item);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    free_play_item (dec, item);
    return ret;
  }

  dec->play_items = g_list_append (dec->play_items, item);

  return ret;
}

static void
free_play_items (GstURIDecodeBin3 * dec)
{
  GList *tmp;

  for (tmp = dec->play_items; tmp; tmp = tmp->next) {
    GstPlayItem *item = (GstPlayItem *) tmp->data;
    free_play_item (dec, item);
  }

  g_list_free (dec->play_items);
  dec->play_items = NULL;
}

static GstStateChangeReturn
gst_uri_decode_bin3_change_state (GstElement * element,
    GstStateChange transition)
{
  GstStateChangeReturn ret;
  GstURIDecodeBin3 *uridecodebin = (GstURIDecodeBin3 *) element;

  switch (transition) {
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      ret = activate_next_play_item (uridecodebin);
      if (ret == GST_STATE_CHANGE_FAILURE)
        goto failure;
    default:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
  if (ret == GST_STATE_CHANGE_FAILURE)
    goto failure;

  switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      /* FIXME: Cleanup everything */
      free_play_items (uridecodebin);
      /* Free play item */
      uridecodebin->posted_about_to_finish = FALSE;
      break;
    default:
      break;
  }

  return ret;

  /* ERRORS */
failure:
  {
    if (transition == GST_STATE_CHANGE_READY_TO_PAUSED)
      free_play_items (uridecodebin);
    return ret;
  }
}


gboolean
gst_uri_decode_bin3_plugin_init (GstPlugin * plugin)
{
  GST_DEBUG_CATEGORY_INIT (gst_uri_decode_bin3_debug, "uridecodebin3", 0,
      "URI decoder element 3");

  return gst_element_register (plugin, "uridecodebin3", GST_RANK_NONE,
      GST_TYPE_URI_DECODE_BIN3);
}
