/* GStreamer
 *
 * Copyright (C) 2005 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,
 * tarut 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 "gsttardemux.h"

#include <gst/base/gsttypefindhelper.h>

#include <string.h>

GST_DEBUG_CATEGORY_STATIC (tar_demux_debug);
#define GST_CAT_DEFAULT tar_demux_debug

#define GST_TYPE_TAR_DEMUX_PAD_SRC         (gst_tar_demux_pad_src_get_type())
#define GST_TAR_DEMUX_PAD_SRC(obj)         (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAR_DEMUX_PAD_SRC,GstTarDemuxPadSrc))
#define GST_TAR_DEMUX_PAD_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAR_DEMUX_PAD_SRC,GstTarDemuxPadSrcClass))

typedef struct {
  GstPad parent;

  gchar h[512];

  guint64 offset;
  guint64 duration, trailer;
} GstTarDemuxPadSrc;

typedef struct {
  GstPadClass parent_class;
} GstTarDemuxPadSrcClass;

static GType gst_tar_demux_pad_src_get_type (void);

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

  return query_types;
}

static gboolean
gst_tar_demux_pad_src_query (GstPad *pad, GstQuery *query)
{
  GstTarDemuxPadSrc *src = GST_TAR_DEMUX_PAD_SRC (pad);
  GstFormat format;
  gboolean ret;
  gint64 pos;

  GST_DEBUG_OBJECT (src, "Query received of type %s...",
    gst_query_type_get_name (GST_QUERY_TYPE (query)));
  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_DURATION:
      gst_query_parse_duration (query, &format, NULL);
      switch (format) {
        case GST_FORMAT_BYTES:
          GST_DEBUG_OBJECT (src, "Announcing duration %" G_GUINT64_FORMAT "...",
            src->duration);
          gst_query_set_duration (query, format, src->duration);
          return TRUE;
        default:
          return gst_pad_query_default (pad, query);
      }
    case GST_QUERY_POSITION:
      gst_query_parse_position (query, &format, NULL);
      if (!(ret = gst_pad_query_default (pad, query)))
        return ret;
      switch (format) {
        case GST_FORMAT_BYTES:
          gst_query_parse_position (query, NULL, &pos);
          gst_query_set_position (query, format, pos - src->offset);
          return TRUE;
        default:
          return TRUE;
      }
    case GST_QUERY_FORMATS:
      gst_query_set_formats (query, 1, GST_FORMAT_BYTES);
      return TRUE;
    case GST_QUERY_SEEKING:
      gst_query_parse_seeking (query, &format, NULL, NULL, NULL);
      switch (format) {
        case GST_FORMAT_BYTES:
          gst_query_set_seeking (query, format, TRUE, 0, src->duration);
          return TRUE;
        default:
          return FALSE;
      }
    default:
      return gst_pad_query_default (pad, query);
  }
}

static GstFlowReturn
gst_tar_demux_pad_src_getrange (GstPad * pad, guint64 offset, guint length,
    GstBuffer ** buf)
{
  GstTarDemuxPadSrc *src = GST_TAR_DEMUX_PAD_SRC (pad);

  return gst_pad_get_range (GST_PAD_PEER (gst_element_get_pad (
    GST_PAD_PARENT (pad), "sink")), src->offset + 512 + offset,
    MIN (length, src->duration - offset), buf);
}

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

static gboolean
gst_tar_demux_pad_src_checkgetrange (GstPad *pad)
{
  return gst_pad_check_pull_range (
      gst_element_get_pad (GST_PAD_PARENT (pad), "sink"));
}

static void
gst_tar_demux_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_any ();
  gst_pad_set_query_function (pad, gst_tar_demux_pad_src_query);
  gst_pad_set_query_type_function (pad, gst_tar_demux_pad_src_query_type);
  gst_pad_set_checkgetrange_function (pad, gst_tar_demux_pad_src_checkgetrange);
  gst_pad_set_getrange_function (pad, gst_tar_demux_pad_src_getrange);
  gst_pad_set_activate_function (pad, gst_tar_demux_pad_src_activate);
}

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

  if (G_UNLIKELY (!object_type)) {
    static const GTypeInfo object_info = {
      sizeof (GstTarDemuxPadSrcClass), NULL, NULL, NULL, NULL, NULL,
      sizeof (GstTarDemuxPadSrc), 0,
      gst_tar_demux_pad_src_init
    };

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

#define GST_TYPE_TAR_DEMUX_PAD_SINK         (gst_tar_demux_pad_sink_get_type())
#define GST_TAR_DEMUX_PAD_SINK(obj)         (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAR_DEMUX_PAD_SINK,GstTarDemuxPadSink))
#define GST_TAR_DEMUX_PAD_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAR_DEMUX_PAD_SINK,GstTarDemuxPadSinkClass))

typedef struct {
  GstPad parent;

  gint64 position;

} GstTarDemuxPadSink;

typedef struct {
  GstPadClass parent_class;
} GstTarDemuxPadSinkClass;

static GstPadClass *pad_sink_parent_class = NULL;

static GType gst_tar_demux_pad_sink_get_type (void);

static void
send_eos_func (gpointer data, gpointer user_data)
{
  GstTarDemuxPadSrc *src = GST_TAR_DEMUX_PAD_SRC (data);

  gst_pad_push_event (GST_PAD (src), gst_event_new_eos ());
}

static gboolean
gst_tar_demux_pad_sink_event (GstPad * snk, GstEvent * event)
{
  GstTarDemux *tar = GST_TAR_DEMUX (GST_PAD_PARENT (snk));
  GstIterator *it;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_EOS:
      GST_DEBUG_OBJECT (snk, "Sending EOS to all source pads.");
      it = gst_element_iterate_src_pads (GST_ELEMENT (tar));
      gst_iterator_foreach (it, send_eos_func, NULL);
      gst_iterator_free (it);
      gst_event_unref (event);
      return TRUE;
    default:
      return gst_pad_event_default (snk, event);
  }
}

static GstTarDemuxPadSrc *
gst_tar_demux_add_src (GstElement * tar, guint64 offset)
{
  gchar *name = g_strdup_printf ("src_%d", tar->numsrcpads);
  GstTarDemuxPadSrc *src = g_object_new (GST_TYPE_TAR_DEMUX_PAD_SRC,
    "direction", GST_PAD_SRC, "name", name, NULL);

  g_free (name);
  src->offset = offset;
  gst_pad_set_active (GST_PAD (src), TRUE);
  gst_element_add_pad (tar, GST_PAD (src));
  return src;
}

static gint
get_src_func (gconstpointer a, gconstpointer b)
{
  GstTarDemuxPadSrc *src = GST_TAR_DEMUX_PAD_SRC (a);
  GstTarDemuxPadSink *snk = GST_TAR_DEMUX_PAD_SINK (b);

  if (snk->position < src->offset)
    return -1;
  if (snk->position >= src->offset + src->duration + src->trailer)
    return 1;
  return 0;
}

static GstTarDemuxPadSrc *
gst_tar_demux_pad_sink_get_src (GstTarDemuxPadSink * snk)
{
  GstTarDemux *tar = GST_TAR_DEMUX (GST_PAD_PARENT (snk));
  GstIterator *it = gst_element_iterate_src_pads (GST_ELEMENT (tar));
  GstTarDemuxPadSrc *src;

  src = gst_iterator_find_custom (it, get_src_func, snk);
  gst_iterator_free (it);
  return src;
}

static gint
is_at_beginning_of_file_func (gconstpointer a, gconstpointer b)
{
  GstTarDemuxPadSrc *src = GST_TAR_DEMUX_PAD_SRC (a);
  GstTarDemuxPadSink *snk = GST_TAR_DEMUX_PAD_SINK (b);

  if (snk->position < src->offset + src->duration + src->trailer)
    return -1;
  if (snk->position > src->offset + src->duration + src->trailer)
    return 1;
  return 0;
}

static gboolean
gst_tar_demux_pad_sink_is_at_beginning_of_file (GstTarDemuxPadSink * snk)
{
  GstIterator *it;
  GstTarDemuxPadSrc *src;

  if (!snk->position)
    return TRUE;

  it = gst_element_iterate_src_pads (GST_PAD_PARENT (snk));
  src = gst_iterator_find_custom (it, is_at_beginning_of_file_func, snk);
  gst_iterator_free (it);
  return (src != NULL);
}

static GstFlowReturn gst_tar_demux_pad_sink_chain_data (GstTarDemuxPadSink *, GstBuffer *, GstTarDemuxPadSrc *);

static GstFlowReturn
gst_tar_demux_pad_sink_chain_header (GstTarDemuxPadSink * snk, GstBuffer * in,
  GstTarDemuxPadSrc * src)
{
  GstBuffer *out;
  guint64 out_size;
  GstTagList *list;
  guint i, c1, c2;
  GDate d;
  GTime t;

  /* Skip data if header is empty */
  if (GST_BUFFER_SIZE (in) >= 512) {
    for (i = 0; i < 512 && !GST_BUFFER_DATA (in)[i]; i++);
    if (i == 512) {
      if (GST_BUFFER_SIZE (in) > 512) {
        out = gst_buffer_create_sub (in, 512, GST_BUFFER_SIZE (in) - 512);
        gst_buffer_unref (in);
        snk->position += 512;
        return gst_tar_demux_pad_sink_chain_header (snk, out, src);
      } else {
        gst_buffer_unref (in);
        return GST_FLOW_OK;
      }
    }
  }

  if (!src)
    src = gst_tar_demux_add_src (GST_PAD_PARENT (GST_PAD (snk)), snk->position);
  out_size = MIN (GST_BUFFER_SIZE (in), src->offset + 512 - snk->position);
  memcpy (src->h + snk->position - src->offset, GST_BUFFER_DATA (in), out_size);
  snk->position += out_size;
  if (snk->position + out_size < src->offset + 512) {
    gst_buffer_unref (in);
    return GST_FLOW_OK;
  }

  /* Check the checksum */
  for (c1 = i = 0; i < 512; i++)
    c1 += ((i < 148 || i >= 148 + 8) ? (guchar) src->h[i] : 32);
  for (c2 = 0, i = 148; (i < 148 + 8) && src->h[i]; i++)
    c2 = (8 * c2 + (guchar) src->h[i] - 48);
  if (c1 != c2) {
    GST_DEBUG_OBJECT (src, "Checksum mismatch.");
    return GST_FLOW_ERROR;
  }

  src->duration = 0;
  src->trailer = 0;
  for (i = 124; src->h[i] != '\0'; i++)
    src->duration = src->duration * 8 + src->h[i] - 48;
  if (src->duration % 512)
    src->trailer = 512 - src->duration % 512;

  /* Post information */
  g_date_clear (&d, 1);
  for (t = 0, i = 136; i < 136 + 12; i++)
    t = t * 10 + src->h[i] - 48;
  g_date_set_time (&d, t);
  list = gst_tag_list_new ();
  gst_tag_list_add (list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TITLE, src->h,
      GST_TAG_DATE, &d, NULL);
  gst_pad_push_event (GST_PAD (src), gst_event_new_tag (list));

  /* Handle remaining data (if any). */
  if (GST_BUFFER_SIZE (in) > out_size) {
    out = gst_buffer_create_sub (in, out_size, GST_BUFFER_SIZE (in) - out_size);
    gst_buffer_unref (in);
    return gst_tar_demux_pad_sink_chain_data (snk, out, src);
  } else {
    gst_buffer_unref (in);
    return GST_FLOW_OK;
  }
}

static GstFlowReturn
gst_tar_demux_pad_sink_chain_trailer (GstTarDemuxPadSink * snk, GstBuffer * in,
  GstTarDemuxPadSrc * src)
{
  GstBuffer *out;
  guint64 out_size;

  out_size = MIN (GST_BUFFER_SIZE (in),
    src->offset + 512 + src->duration + src->trailer - snk->position);
  snk->position += out_size;
  if (GST_BUFFER_SIZE (in) > out_size) {
    out = gst_buffer_create_sub (in, out_size, GST_BUFFER_SIZE (in) - out_size);
    gst_buffer_unref (in);
    return gst_tar_demux_pad_sink_chain_header (snk, out, src);
  } else {
    gst_buffer_unref (in);
    return GST_FLOW_OK;
  }
}

static GstFlowReturn
gst_tar_demux_pad_sink_chain_data (GstTarDemuxPadSink * snk, GstBuffer * in,
  GstTarDemuxPadSrc * src)
{
  guint64 out_size;
  GstBuffer *out;
  GstFlowReturn r;
  GstCaps *caps;

  /* If at position 0, figure out the caps. If at the end, send EOS. */
  out_size = MIN (GST_BUFFER_SIZE (in),
    src->offset + 512 + src->duration - snk->position);
  out = gst_buffer_create_sub (in, 0, out_size);
  out = gst_buffer_make_metadata_writable (out);
  if (!(GST_BUFFER_OFFSET (out) = snk->position - src->offset - 512)) {
    GST_DEBUG_OBJECT (src, "Determining caps...");
    caps = gst_type_find_helper_for_buffer (GST_OBJECT (snk), out, NULL);
    if (caps) {
      gst_pad_set_caps (GST_PAD (src), caps);
      gst_caps_unref (caps);
    }
  }
  gst_buffer_set_caps (out, GST_PAD_CAPS (src));
  GST_DEBUG_OBJECT (src, "Pushing buffer...");
  if ((r = gst_pad_push (GST_PAD (src), out)) != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (src, "Failed to push buffer for offset %"
      G_GUINT64_FORMAT ": %s", snk->position - src->offset - 512,
      gst_flow_get_name (r));
    gst_buffer_unref (in);
    return r;
  }
  snk->position += out_size;
  if (snk->position >= src->offset + 512 + src->duration)
    gst_pad_push_event (GST_PAD (src), gst_event_new_eos ());

  /* Handle remaining data (if any). */
  if (GST_BUFFER_SIZE (in) > out_size) {
    out = gst_buffer_create_sub (in, out_size, GST_BUFFER_SIZE (in) - out_size);
    gst_buffer_unref (in);
    if (snk->position < src->offset + 512 + src->duration + src->trailer)
      return gst_tar_demux_pad_sink_chain_trailer (snk, out, src);
    else
      return gst_tar_demux_pad_sink_chain_header (snk, out, src);
  } else {
    gst_buffer_unref (in);
    return GST_FLOW_OK;
  }
}

static GstFlowReturn
gst_tar_demux_pad_sink_chain (GstPad * pad, GstBuffer * in)
{
  GstTarDemuxPadSink *snk = GST_TAR_DEMUX_PAD_SINK (pad);
  GstTarDemuxPadSrc *src;

  /* If we already have the source pad, use it. */
  if ((src = gst_tar_demux_pad_sink_get_src (snk))) {
    if (snk->position < src->offset + 512)
      return gst_tar_demux_pad_sink_chain_header (snk, in, src);
    if (snk->position < src->offset + 512 + src->duration)
      return gst_tar_demux_pad_sink_chain_data (snk, in, src);
    return gst_tar_demux_pad_sink_chain_trailer (snk, in, src);
  }

  /* We might be at the beginning of a new file. */
  if (gst_tar_demux_pad_sink_is_at_beginning_of_file (snk))
    return gst_tar_demux_pad_sink_chain_header (snk, in, NULL);

  /* We have no idea where in the file we are. Fail. */
  GST_DEBUG_OBJECT (snk, "Don't have sufficient information to decode at "
    "offset %" G_GUINT64_FORMAT "...", snk->position);
  gst_buffer_unref (in);
  return GST_FLOW_ERROR;
}

static void
gst_tar_demux_pad_sink_loop (void *data)
{
  GstTarDemuxPadSink *snk = GST_TAR_DEMUX_PAD_SINK (data);
  GstFlowReturn r;
  GstBuffer *buf;

  r = gst_pad_pull_range (GST_PAD (snk), snk->position, 2048, &buf);
  if (r != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (snk, "Pull failed at offset %" G_GUINT64_FORMAT 
      ": %s.", snk->position, gst_flow_get_name (r));
    gst_pad_pause_task (GST_PAD (snk));
  }
  r = gst_tar_demux_pad_sink_chain (GST_PAD (snk), buf);
  if (r != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (snk, "Chain failed: %s.", gst_flow_get_name (r));
    gst_pad_pause_task (GST_PAD (snk));
  }
}

static gboolean
gst_tar_demux_pad_sink_activate_pull (GstPad * snk, gboolean active)
{
  if (active)
    return gst_pad_start_task (snk, gst_tar_demux_pad_sink_loop, snk);
  else
    return gst_pad_stop_task (snk);
}

static gboolean
gst_tar_demux_pad_sink_activate (GstPad * pad)
{
  if (gst_pad_check_pull_range (pad)) {
    return gst_pad_activate_pull (pad, TRUE);
  } else
    return gst_pad_activate_push (pad, TRUE);
}

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

  GST_PAD_DIRECTION (pad) = GST_PAD_SINK;
  caps = gst_caps_new_simple ("application/x-tar", NULL);
  gst_pad_set_caps (pad, caps);
  gst_caps_unref (caps);

  gst_pad_set_chain_function (pad, gst_tar_demux_pad_sink_chain);
  gst_pad_set_event_function (pad, gst_tar_demux_pad_sink_event);
  gst_pad_set_activate_function (pad, gst_tar_demux_pad_sink_activate);
  gst_pad_set_activatepull_function (pad, gst_tar_demux_pad_sink_activate_pull);
}

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

  if (G_UNLIKELY (!object_type)) {
    static const GTypeInfo object_info = {
      sizeof (GstTarDemuxPadSinkClass), NULL, NULL,
      NULL, NULL, NULL,
      sizeof (GstTarDemuxPadSink), 0,
      gst_tar_demux_pad_sink_init
    };

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

static GstElementDetails tar_demux_details = GST_ELEMENT_DETAILS ("TAR demuxer",
    "Codec/Demuxer", "Demuxer for TAR streams",
    "Lutz Mueller <lutz@topfrose.de>");

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

struct _GstTarDemux
{
  GstElement parent;
};

struct _GstTarDemuxClass
{
  GstElementClass parent_class;
};

GST_BOILERPLATE (GstTarDemux, gst_tar_demux, GstElement, GST_TYPE_ELEMENT);

static void
gst_tar_demux_init (GstTarDemux * tar, GstTarDemuxClass * klass)
{
  gst_element_add_pad (GST_ELEMENT (tar), g_object_new (
    GST_TYPE_TAR_DEMUX_PAD_SINK, "direction",
    GST_PAD_SINK, "name", "sink", NULL));
}

static void
gst_tar_demux_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_demux_details);
}

static GstStateChangeReturn
gst_tar_demux_change_state (GstElement * element, GstStateChange transition)
{
  GstTarDemux *tar = GST_TAR_DEMUX (element);
  GstTarDemuxPadSink *snk = GST_TAR_DEMUX_PAD_SINK (gst_element_get_pad (
    GST_ELEMENT (tar), "sink"));

  switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      snk->position = 0;
      break;
    default:
      break;
  }
  return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
}

static gboolean
gst_tar_demux_send_event (GstElement * element, GstEvent * event)
{
  GstTarDemux *tar = GST_TAR_DEMUX (element);
  GstSeekFlags flags;
  GstSeekType cur_type, stop_type;
  gint64 cur, stop;
  gdouble rate;
  GstFormat format;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_SEEK:
      gst_event_parse_seek (event, &rate, &format, &flags, &cur_type, &cur,
        &stop_type, &stop);
      switch (format) {
        case GST_FORMAT_BYTES:
          GST_DEBUG_OBJECT (tar, "FIXME: Got seek event!");
          return TRUE;
        default:
          GST_DEBUG_OBJECT (tar, "Cannot seek in format %s.",
            gst_format_get_name (format)); 
      }
      break;
    default:
      GST_DEBUG_OBJECT (tar, "Can't handle event %s.", 
        gst_event_type_get_name (GST_EVENT_TYPE (event)));
  }
  gst_event_unref (event);
  return FALSE;
}

static void
gst_tar_demux_class_init (GstTarDemuxClass * klass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  
  element_class->change_state = gst_tar_demux_change_state;
  element_class->send_event = gst_tar_demux_send_event;

  GST_DEBUG_CATEGORY_INIT (tar_demux_debug, "tardemux", 0, "TAR demuxer");
}
