/* GStreamer
 *
 * Copyright (C) 2006 Lutz Mueller <lutz@topfrose.de>
 * 
 * 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 "gsttarmux.h"

#include <string.h>

GST_DEBUG_CATEGORY_STATIC (tar_mux_debug);
#define GST_CAT_DEFAULT tar_mux_debug

typedef struct _GstTarMuxPadSink GstTarMuxPadSink;

static GstFlowReturn gst_tar_mux_pad_sink_get_header (GstTarMuxPadSink *snk, GstBuffer **buf);
static GstFlowReturn gst_tar_mux_pad_sink_get_footer (GstTarMuxPadSink *snk, GstBuffer **buf);

/******************************************************************************
 * Source pad
 */

#define GST_TYPE_TAR_MUX_PAD_SRC         (gst_tar_mux_pad_src_get_type())
#define GST_TAR_MUX_PAD_SRC(obj)         (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAR_MUX_PAD_SRC,GstTarMuxPadSrc))
#define GST_TAR_MUX_PAD_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAR_MUX_PAD_SRC,GstTarMuxPadSrcClass))

typedef struct {
  GstPad parent;
} GstTarMuxPadSrc;

typedef struct {
  GstPad parent_class;
} GstTarMuxPadSrcClass;

static GType gst_tar_mux_pad_src_get_type (void);

static const GstQueryType *
gst_tar_mux_pad_src_query_type (GstPad *pad)
{
  static const const GstQueryType query_types[] = {
    GST_QUERY_DURATION,
    GST_QUERY_FORMATS,
    0
  };

  return query_types;
}

static gboolean
gst_tar_mux_pad_src_query (GstPad *pad, GstQuery *query)
{
  GstTarMuxPadSrc *src = GST_TAR_MUX_PAD_SRC (pad);
  GstIterator *it;
  gint64 duration = 0, dur;
  GstIteratorResult ir;
  GstPad *snk;
  GstFormat format;

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_DURATION:
      gst_query_parse_duration (query, &format, NULL);
      it = gst_element_iterate_sink_pads (GST_PAD_PARENT (pad));
      while (TRUE) {
        ir = gst_iterator_next (it, (gpointer) &snk);
        switch (ir) {
          case GST_ITERATOR_DONE:
            gst_iterator_free (it);
            return TRUE;
          case GST_ITERATOR_OK:
            if (format == GST_FORMAT_BYTES) {
              g_object_get (G_OBJECT (snk), "duration", &dur, NULL);
              dur += 512 + (dur % 512);
            } else
              if (!gst_pad_query_peer_duration (snk, &format, &dur)) {
                gst_iterator_free (it);
                return FALSE;
              }
            duration += dur;
            break;
          default:
            gst_iterator_free (it);
            GST_DEBUG_OBJECT (src, "Could not iterate on sink pads (%i).", ir);
            return FALSE;
        }
      }
      return FALSE;
    case GST_QUERY_FORMATS:

      /*
       * If we cannot query in bytes, we fail. We don't care about
       * the other formats
       */
      gst_query_set_formats (query, 1, GST_FORMAT_BYTES);
      return TRUE;
    default:
      return gst_pad_query_default (pad, query);
  }
}

static GstFlowReturn
gst_tar_mux_pad_src_getrange (GstPad * pad, guint64 offset, guint length,
    GstBuffer ** buf)
{
  GstIterator *it;
  gboolean done = FALSE;
  GstIteratorResult ir;
  GstPad *snk = NULL;
  gint64 off = 0, dur = 0;
  gboolean last;

  it = gst_element_iterate_src_pads (GST_PAD_PARENT (pad));
  while (!done) {
    ir = gst_iterator_next (it, (gpointer) &snk);
    switch (ir) {
      case GST_ITERATOR_DONE:
        gst_iterator_free (it);
        return GST_FLOW_UNEXPECTED;
      case GST_ITERATOR_OK:
        g_object_get (G_OBJECT (snk), "offset", &off,
            "duration", &dur, "last", &last, NULL);
        if ((off <= offset) &&
            (off + 512 + dur + (dur % 512) + (last ? 2 * 512 : 0) > offset))
          done = TRUE;
        break;
      default:
        gst_iterator_free (it);
        return GST_FLOW_ERROR;
    }
  }
  gst_iterator_free (it);

  offset -= off;
  if (offset < 512)
    return gst_tar_mux_pad_sink_get_header ((GstTarMuxPadSink *) snk, buf);

  offset -= 512;
  if (offset > dur)
    return gst_tar_mux_pad_sink_get_footer ((GstTarMuxPadSink *) snk, buf);

  length = MIN (length, dur - offset);
  return gst_pad_get_range (GST_PAD_PEER (snk), offset, length, buf);
}

static gboolean
gst_tar_mux_pad_src_activate (GstPad * pad)
{
  return gst_pad_activate_pull (pad, TRUE);
}

static void
gst_tar_mux_pad_src_init (GTypeInstance *instance, gpointer g_class)
{
  GstPad *pad = GST_PAD (instance);

  GST_PAD_DIRECTION (pad) = GST_PAD_SRC;
  GST_PAD_CAPS (pad) = gst_caps_new_simple ("application/x-tar", NULL);
  gst_pad_set_query_function (pad, gst_tar_mux_pad_src_query);
  gst_pad_set_query_type_function (pad, gst_tar_mux_pad_src_query_type);
  gst_pad_set_getrange_function (pad, gst_tar_mux_pad_src_getrange);
  gst_pad_set_activate_function (pad, gst_tar_mux_pad_src_activate);
}

static GType
gst_tar_mux_pad_src_get_type (void)
{
  static GType object_type = 0;

  if (G_UNLIKELY (!object_type)) {
    static const GTypeInfo object_info = {
      sizeof (GstTarMuxPadSrcClass), NULL, NULL,
      NULL, NULL, NULL,
      sizeof (GstTarMuxPadSrc), 0,
      gst_tar_mux_pad_src_init
    };

    object_type = g_type_register_static (GST_TYPE_PAD, "GstTarMuxPadSrc",
        &object_info, (GTypeFlags) 0);
  }
  return object_type;
}

/******************************************************************************
 * Sink pad
 */

#define GST_TYPE_TAR_MUX_PAD_SINK         (gst_tar_mux_pad_sink_get_type())
#define GST_TAR_MUX_PAD_SINK(obj)         (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAR_MUX_PAD_SINK,GstTarMuxPadSink))
#define GST_TAR_MUX_PAD_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAR_MUX_PAD_SINK,GstTarMuxPadSinkClass))

struct _GstTarMuxPadSink {
  GstPad parent;

  gint64 duration;
  gint64 offset;
  gboolean last;
  GstPad *src;

  gchar *title;
  GDate date;
};

typedef struct {
  GstPadClass parent_class;
} GstTarMuxPadSinkClass;

enum {
  SINK_PROP_0,
  SINK_PROP_OFFSET,
  SINK_PROP_DURATION,
  SINK_PROP_SRC,
  SINK_PROP_LAST
};

static GstPadClass *pad_sink_parent_class = NULL;

static GType gst_tar_mux_pad_sink_get_type (void);

static gboolean
gst_tar_mux_pad_sink_ensure_duration (GstTarMuxPadSink * snk)
{
  GstPad *peer = GST_PAD_PEER (GST_PAD (snk));
  GstFormat format = GST_FORMAT_BYTES;

  if (snk->duration >= 0)
    return TRUE;

  /* We need to know the duration. */
  if (!peer || !gst_pad_query_duration (peer, &format, &snk->duration)) {
    GST_DEBUG_OBJECT (snk, "Could not query duration.");
    return FALSE;
  }
  if (snk->duration > 0) {
    g_object_notify (G_OBJECT (snk), "duration");
    return TRUE;
  } else
    return FALSE;
}

static GstFlowReturn
gst_tar_mux_pad_sink_get_header (GstTarMuxPadSink * snk, GstBuffer ** buf)
{
  GstFlowReturn r;
  guint i, c;
  time_t t = 0;
  const gchar *ext = "";
  GstStructure *st;

  if (!gst_tar_mux_pad_sink_ensure_duration (snk))
    return GST_FLOW_ERROR;

  if ((r = gst_pad_alloc_buffer (snk->src, snk->offset, 512,
          GST_PAD_CAPS (snk->src), buf)) != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (snk, "Could not create buffer for header (%s).",
        gst_flow_get_name (r));
    return r;
  }
  memset (GST_BUFFER_DATA (*buf), 0, 512);

  /* FIXME: How to get the correct extension for a given caps? */
  if (GST_PAD_CAPS (GST_PAD_PEER (snk)) &&
      GST_PAD_CAPS (GST_PAD_PEER (snk))->structs &&
      GST_PAD_CAPS (GST_PAD_PEER (snk))->structs->len) {
    st = gst_caps_get_structure (GST_PAD_CAPS (GST_PAD_PEER (snk)), 0);
    if (gst_structure_has_name (st, "application/ogg"))
      ext = ".ogg";
  }

  /* File name */
  snprintf ((char *) GST_BUFFER_DATA (*buf), 100, "%s%s",
      snk->title ? snk->title : GST_PAD_NAME (snk), ext);

  /* File size */
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[124], 12, "%011o",
      (int) snk->duration);

  /* Date */
  if (g_date_valid (&snk->date)) {
    struct tm tm;

    g_date_to_struct_tm (&snk->date, &tm);
    t = mktime (&tm);
  } else
    t = time (NULL);
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[136], 12, "%011o", (guint) t);

  /* Dummy data */
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[100], 9, "0000644");
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[108], 9, "0001750");
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[116], 9, "0001750");
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[329], 8, "0000000");
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[337], 8, "0000000");

  /* Checksum */
  c = 8 * ((guint) ' ');
  for (i = 0; i < 512; i++)
    c += *((char *) &GST_BUFFER_DATA (*buf)[i]);
  snprintf ((char *) &GST_BUFFER_DATA (*buf)[148], 7, "%06o", c);
  *((char *) &GST_BUFFER_DATA (*buf)[148 + 7]) = ' ';

  return GST_FLOW_OK;
}

static GstFlowReturn
gst_tar_mux_pad_sink_get_footer (GstTarMuxPadSink * snk, GstBuffer ** buf)
{
  GstFlowReturn r;

  if (!gst_tar_mux_pad_sink_ensure_duration (snk))
    return GST_FLOW_ERROR;

  if ((r = gst_pad_alloc_buffer (snk->src, snk->offset + 512 + snk->duration,
          512 * (snk->last ? 3 : 1) - (snk->duration % 512),
          GST_PAD_CAPS (snk->src), buf)) != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (snk, "Could not create buffer for footer (%s).",
        gst_flow_get_name (r));
    return r;
  }
  memset (GST_BUFFER_DATA (*buf), 0, GST_BUFFER_SIZE (*buf));
  return GST_FLOW_OK;
}

static gboolean
gst_tar_mux_pad_sink_event (GstPad * pad, GstEvent * event)
{
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (pad);
  GstTagList *taglist;
  gchar *title;
  GDate *d;
  GstBuffer *buf;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_TAG:
      gst_event_parse_tag (event, &taglist);
      if (gst_tag_list_get_string (taglist, GST_TAG_TITLE, &title)) {
        if (snk->title)
          g_free (snk->title);
        snk->title = title;
      }
      if (gst_tag_list_get_date (taglist, GST_TAG_DATE, &d)) {
        memcpy (&snk->date, d, sizeof (snk->date));
        g_date_free (d);
      }
      gst_event_unref (event);
      return TRUE;
    case GST_EVENT_EOS:
      if ((gst_tar_mux_pad_sink_get_footer (snk, &buf) != GST_FLOW_OK) ||
          (gst_pad_push (snk->src, buf) != GST_FLOW_OK)) {
        gst_event_unref (event);
        return FALSE;
      }
      return gst_pad_event_default (pad, event);
    default:
      return gst_pad_event_default (pad, event);
  }
}

static GstFlowReturn
gst_tar_mux_pad_sink_chain (GstPad * pad, GstBuffer * buf)
{
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (pad);
  GstFlowReturn r;
  GstBuffer *header;

  if (!GST_BUFFER_OFFSET (buf)) {
    GST_DEBUG_OBJECT (snk, "Writing header for pad %s:%s...",
        GST_DEBUG_PAD_NAME (pad));
    if ((r = gst_tar_mux_pad_sink_get_header (snk, &header)) != GST_FLOW_OK) {
      gst_buffer_unref (buf);
      return r;
    }
    if ((r = gst_pad_push (snk->src, header)) != GST_FLOW_OK) {
      GST_DEBUG_OBJECT (snk, "Could not write header (%s).",
          gst_flow_get_name (r));
      gst_buffer_unref (header);
      return r;
    }
  }
  GST_BUFFER_OFFSET (buf) += snk->offset + 512;
  gst_buffer_set_caps (buf, GST_PAD_CAPS (snk->src));
  return gst_pad_push (snk->src, buf);
}

static void
gst_tar_mux_pad_sink_init (GTypeInstance *instance, gpointer g_class)
{
  GstPad *pad = GST_PAD (instance);
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (pad);

  g_date_clear (&snk->date, 1);
  snk->duration = -1;

  GST_PAD_DIRECTION (pad) = GST_PAD_SINK;
  GST_PAD_CAPS (pad) = gst_caps_new_any ();
  gst_pad_set_chain_function (pad, gst_tar_mux_pad_sink_chain);
  gst_pad_set_event_function (pad, gst_tar_mux_pad_sink_event);
}

static void
gst_tar_mux_pad_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (object);

  switch (prop_id) {
    case SINK_PROP_OFFSET:
      g_value_set_int64 (value, snk->offset);
      break;
    case SINK_PROP_SRC:
      g_value_set_object (value, snk->src);
      break;
    case SINK_PROP_LAST:
      g_value_set_boolean (value, snk->last);
      break;
    case SINK_PROP_DURATION:
      g_value_set_int64 (value, snk->duration);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
}

static void
gst_tar_mux_pad_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (object);

  switch (prop_id) {
    case SINK_PROP_OFFSET:
      snk->offset = g_value_get_int64 (value);
      break;
    case SINK_PROP_SRC:
      if (snk->src)
        gst_object_unref (GST_OBJECT (snk->src));
      snk->src = g_value_get_object (value);
      if (snk->src)
        gst_object_ref (GST_OBJECT (snk->src));
      break;
    case SINK_PROP_LAST:
      snk->last = g_value_get_boolean (value);
      break;
    case SINK_PROP_DURATION:
      snk->duration = g_value_get_int64 (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
}

static void
gst_tar_mux_pad_sink_finalize (GObject *object)
{
  GstTarMuxPadSink *snk = GST_TAR_MUX_PAD_SINK (object);

  if (snk->title) {
    g_free (snk->title);
    snk->title = NULL;
  }

  G_OBJECT_CLASS (pad_sink_parent_class)->finalize (object);
}

static void
gst_tar_mux_pad_sink_class_init (gpointer g_class, gpointer data)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (g_class);

  gobject_class->finalize     = gst_tar_mux_pad_sink_finalize;
  gobject_class->set_property = gst_tar_mux_pad_sink_set_property;
  gobject_class->get_property = gst_tar_mux_pad_sink_get_property;

  g_object_class_install_property (gobject_class,
      SINK_PROP_OFFSET, g_param_spec_int64 ("offset",
        "Offset", "Offset", 0, G_MAXINT64, 0, G_PARAM_READWRITE));
  g_object_class_install_property (gobject_class,
      SINK_PROP_SRC, g_param_spec_object ("source",
        "Source", "Source", GST_TYPE_TAR_MUX_PAD_SRC, G_PARAM_READWRITE));
  g_object_class_install_property (gobject_class,
      SINK_PROP_LAST, g_param_spec_boolean ("last",
        "Last stream", "Last stream", FALSE, G_PARAM_READWRITE));
  g_object_class_install_property (gobject_class,
      SINK_PROP_DURATION, g_param_spec_int64 ("duration",
        "Duration", "Duration", -1, G_MAXINT64, -1, G_PARAM_READWRITE));

  pad_sink_parent_class = GST_PAD_CLASS (g_type_class_peek_parent (g_class));
}

static GType
gst_tar_mux_pad_sink_get_type (void)
{
  static GType object_type = 0;

  if (G_UNLIKELY (!object_type)) {
    static const GTypeInfo object_info = {
      sizeof (GstTarMuxPadSinkClass), NULL, NULL,
      gst_tar_mux_pad_sink_class_init, NULL, NULL,
      sizeof (GstTarMuxPadSink), 0,
      gst_tar_mux_pad_sink_init
    };

    object_type = g_type_register_static (GST_TYPE_PAD, "GstTarMuxPadSink",
        &object_info, (GTypeFlags) 0);
  }
  return object_type;
}

/******************************************************************************
 * Element
 */

static GstElementDetails tar_mux_details = GST_ELEMENT_DETAILS ("TAR muxer",
    "Codec/Muxer", "Muxer for TAR streams",
    "Lutz Mueller <lutz@topfrose.de>");

static GstStaticPadTemplate snk_t =
GST_STATIC_PAD_TEMPLATE ("sink_%d", GST_PAD_SINK, GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);
static GstStaticPadTemplate src_t =
GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/x-tar"));

struct _GstTarMux
{
  GstElement parent;

  GstPad *src;
};

struct _GstTarMuxClass
{
  GstElementClass parent_class;
};

GST_BOILERPLATE (GstTarMux, gst_tar_mux, GstElement, GST_TYPE_ELEMENT);

static GstPad *
gst_tar_mux_get_last_pad (GstTarMux * tar)
{
  GstPad *snk_max = NULL, *snk;
  gint64 offset_max = 0, offset;
  gboolean done = FALSE;
  GstIterator *it;
  GstIteratorResult ir;

  it = gst_element_iterate_sink_pads (GST_ELEMENT (tar));
  while (!done) {
    ir = gst_iterator_next (it, (gpointer) &snk);
    switch (ir) {
      case GST_ITERATOR_OK:
        g_object_get (G_OBJECT (snk), "offset", &offset, NULL);
        if (offset >= offset_max)
          snk_max = snk;
        break;
      case GST_ITERATOR_DONE:
        done = TRUE;
        break;
      default:
        GST_DEBUG_OBJECT (tar, "Could not iterate sinks (%i).", ir);
        done = TRUE;
        break;
    }
  }
  gst_iterator_free (it);
  return snk_max;
}

static void
no_more_pads (GstTarMux * tar)
{
  GstPad *snk;

  snk = gst_tar_mux_get_last_pad (tar);
  if (snk)
    g_object_set (G_OBJECT (snk), "last", TRUE, NULL);
  else
    GST_DEBUG_OBJECT (tar, "No sinks but no more pads expected.");
}

static void
gst_tar_mux_init (GstTarMux * tar, GstTarMuxClass * klass)
{
  GstPad *snk;

  g_signal_connect (G_OBJECT (tar), "no-more-pads",
      G_CALLBACK (no_more_pads), NULL);

  /* The source pad */
  tar->src = g_object_new (GST_TYPE_TAR_MUX_PAD_SRC,
      "direction", GST_PAD_SRC, "name", "src", NULL);
  gst_element_add_pad (GST_ELEMENT (tar), tar->src);

  /* The first sink pad */
  snk = gst_element_get_pad (GST_ELEMENT (tar), "sink_%d");
  gst_object_unref (GST_OBJECT (snk));
}

static void
gst_tar_mux_base_init (gpointer g_class)
{
  GstElementClass *ec = GST_ELEMENT_CLASS (g_class);

  gst_element_class_add_pad_template (ec, gst_static_pad_template_get (&snk_t));
  gst_element_class_add_pad_template (ec, gst_static_pad_template_get (&src_t));
  gst_element_class_set_details (ec, &tar_mux_details);
}

static GstPad *
gst_tar_mux_request_new_pad (GstElement * element, GstPadTemplate * template,
    const gchar * name)
{
  GstPad *pad;
  GstPad *snk;
  gint64 off = 0, dur = 0;

  snk = gst_tar_mux_get_last_pad (GST_TAR_MUX (element));
  if (snk) {
    g_object_get (G_OBJECT (snk), "duration", &dur, "offset", &off, NULL);
    g_object_set (G_OBJECT (snk), "last", FALSE, NULL);
    off += 512 + dur + (dur % 512);
  }

  pad = g_object_new (GST_TYPE_TAR_MUX_PAD_SINK, "name", name,
      "source", GST_TAR_MUX (element)->src, "last", TRUE,
      "offset", off, "direction", GST_PAD_SINK, NULL);
  gst_element_add_pad (element, pad);

  return pad;
}

static void
gst_tar_mux_class_init (GstTarMuxClass * klass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  element_class->request_new_pad = gst_tar_mux_request_new_pad;

  GST_DEBUG_CATEGORY_INIT (tar_mux_debug, "tarmux", 0, "TAR muxer");
}
