/* 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 "gstsmildec.h"

#include <libxml/parser.h>

#include <string.h>

GST_DEBUG_CATEGORY_STATIC (smil_dec_debug);
#define GST_CAT_DEFAULT smil_dec_debug

static GstElementDetails smil_dec_details = GST_ELEMENT_DETAILS (
    "SMIL decoder", "Decoder/SMIL",
    "Decode SMIL data", "Lutz Mueller <lutz@topfrose.de>");

struct _GstSmilDec
{
  GstBin parent;

  xmlParserCtxtPtr ctxt;
};

struct _GstSmilDecClass
{
  GstBinClass parent_class;
};

GST_BOILERPLATE (GstSmilDec, gst_smil_dec, GstBin, GST_TYPE_BIN);

static GstStaticPadTemplate tsink =
GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/smil"));

static GstStaticPadTemplate tsrc =
GST_STATIC_PAD_TEMPLATE ("src_%d", GST_PAD_SRC, GST_PAD_SOMETIMES,
    GST_STATIC_CAPS_ANY);

static void gst_smil_dec_add_pad (GstSmilDec * dec, GstPad * pad);

static gboolean
gst_smil_dec_event (GstPad * pad, GstEvent * event)
{
  GstSmilDec *dec = GST_SMIL_DEC (GST_PAD_PARENT (pad));

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_EOS:
      GST_DEBUG_OBJECT (dec, "Got EOS.");
      if (!GST_BIN (dec)->numchildren)
        gst_element_no_more_pads (GST_ELEMENT (dec));
      gst_event_unref (event);
      return TRUE;
    default:
      return gst_pad_event_default (pad, event);
  }
}

static void
foreach_remove (gpointer data, gpointer user_data)
{
  gst_bin_remove (GST_BIN (user_data), GST_ELEMENT (data));
}

static void
smil_startDocument (GstSmilDec * dec)
{
  GstIterator *it;

  it = gst_bin_iterate_elements (GST_BIN (dec));
  gst_iterator_foreach (it, foreach_remove, dec);
  gst_iterator_free (it);
}

static void
foreach_add (gpointer data, gpointer user_data)
{
  gst_smil_dec_add_pad (GST_SMIL_DEC (user_data), GST_PAD (data));
}

static void
pad_added_cb (GstElement * element, GstPad * pad, GstSmilDec * dec)
{
  gst_smil_dec_add_pad (dec, pad);
}

static void
gst_smil_dec_add_pad (GstSmilDec * dec, GstPad * pad)
{
  GstCaps *caps = gst_pad_get_caps (pad);
  GstPad *ghost;
  GstElement *d;

  /*
   * We need to manually add a decodebin element after ANY caps - I have
   * not been able to convince GStreamer developers to implement that
   * logic in decodebin itself (i.e. decodebin creating another decodebin
   * if it encounters ANY caps).
   */
  if (gst_caps_is_any (caps)) {
    GstElement *d = gst_element_factory_make ("decodebin", NULL);
    GstPadLinkReturn ret;

    gst_bin_add (GST_BIN (dec), d);
    g_signal_connect (G_OBJECT (d), "pad-added",
        G_CALLBACK (pad_added_cb), dec);
    ret = gst_pad_link (pad, gst_element_get_pad (d, "sink"));
    if (ret != GST_PAD_LINK_OK)
      GST_DEBUG_OBJECT (dec, "Could not link pad to decodebin (%i).", ret);
    if (!gst_element_sync_state_with_parent (d))
      GST_DEBUG_OBJECT (dec, "Could not synchronize state of new element.");
  } else {
    GstPad *ghost = gst_ghost_pad_new (NULL, pad);
    
    gst_pad_set_active (ghost, TRUE);
    gst_element_add_pad (GST_ELEMENT (dec), ghost);
  }
  gst_caps_unref (caps);
}

static gint64
smil_get_time (GstSmilDec * dec, const gchar * s)
{
  if (!strncmp (s, "npt=", 4)) {
    s += 4;
    if (strlen (s) != 11)
      goto could_not_parse;
    if (s[0] < '0' || s[0] > '9' ||
        s[1] < '0' || s[1] > '9' ||
        s[2] != ':' ||
        s[3] < '0' || s[3] > '9' ||
        s[4] < '0' || s[4] > '9' ||
        s[5] != ':' ||
        s[6] < '0' || s[6] > '9' ||
        s[7] < '0' || s[7] > '9' ||
        s[8] != '.' ||
        s[9] < '0' || s[9] > '9' ||
        s[10] < '0' || s[10] > '9')
      goto could_not_parse;
    return (((((s[0] - 48) * 10 + s[1]) * 60 + s[3] * 10 + s[4]) * 60 +
      s[6] * 10) * 60 + s[7]) * GST_SECOND +
      (s[9] * 100 + s[10] * 10) * GST_MSECOND;
  }

  /* Errors */
could_not_parse:
  {
    GST_DEBUG_OBJECT (dec, "Could not parse time '%s'.", s);
    return GST_CLOCK_TIME_NONE;
  }
}

static void
smil_startElement (GstSmilDec * dec, const xmlChar * name, 
    const xmlChar ** attrs)
{
  if (!strcmp (name, "smil")) {
    /* Nothing for now. */
  } else if (!strcmp (name, "head")) {
    /* Nothing for now. */
  } else if (!strcmp (name, "body")) {
    /* Nothing for now. */
  } else if (!strcmp (name, "seq")) {
    GST_DEBUG_OBJECT (dec, "Sequential playback not yet implemented. "
        "Parallel playback will be used instead.");
  } else if (!strcmp (name, "par")) {
    /* Nothing for now. */
  } else if (!strcmp (name, "meta")) {
    const gchar *content, *name;

    for ( ; *attrs; attrs++) {
      if (!strcmp (*attrs, "name"))
        name = *(++attrs);
      else if (!strcmp (*attrs, "content"))
        content = *(++attrs);
      else {
        GST_DEBUG_OBJECT (dec, "Unknown attribute '%s' for 'meta' element.");
      }
    }
    if (name && !strcmp (name, "copyright")) {
      GstTagList *list = gst_tag_list_new ();

      gst_tag_list_add (list, GST_TAG_MERGE_APPEND,
          GST_TAG_COPYRIGHT, content, NULL);
      gst_element_found_tags (GST_ELEMENT (dec), list);
    }
  } else if (!strcmp (name, "video")) {
    const xmlChar **p;
    const gchar *src, *clip_begin, *clip_end;

    for ( ; *attrs; attrs++) {
      if (!strcmp (*attrs, "src"))
        src = *(++attrs);
      else if (!strcmp (*attrs, "clip-begin"))
        clip_begin = *(++attrs);
      else if (!strcmp (*attrs, "clip-end"))
        clip_end = *(++attrs);
      else
        GST_DEBUG_OBJECT (dec, "Skipping attribute %s.", *(attrs++));
    }
    if (src) {
      GstElement *e;
      GstPad *pad;
      GstIterator *it;

      if (!(e = gst_element_make_from_uri (GST_URI_SRC, src, NULL))) {
        GST_DEBUG_OBJECT (dec, "Could not create element for URI '%s'.", src);
        return;
      }
      gst_bin_add (GST_BIN (dec), e);
      it = gst_element_iterate_src_pads (e);
      gst_iterator_foreach (it, foreach_add, dec);
      g_signal_connect (G_OBJECT (e), "pad-added",
          G_CALLBACK (pad_added_cb), dec);
      if (!gst_element_sync_state_with_parent (e))
        GST_DEBUG_OBJECT (dec, "Could not synchronize state of new element.");

      /* Unfortunately, not all elements do typefinding themselves. */
      if (g_object_class_find_property (G_OBJECT_GET_CLASS (e), "typefind"))
        g_object_set (G_OBJECT (e), "typefind", TRUE, NULL);

      if (clip_begin || clip_end) {
        gint64 start = GST_CLOCK_TIME_NONE, stop = GST_CLOCK_TIME_NONE;
        GstSeekType start_type = GST_SEEK_TYPE_NONE;
        GstSeekType stop_type = GST_SEEK_TYPE_NONE;

        if (clip_begin)
          if ((start = smil_get_time (dec, clip_begin)) != GST_CLOCK_TIME_NONE)
            start_type = GST_SEEK_TYPE_CUR;
        if (clip_end)
          if ((stop = smil_get_time (dec, clip_end)) != GST_CLOCK_TIME_NONE)
            stop_type = GST_SEEK_TYPE_CUR;
        if (!gst_element_seek (e, 1.0, GST_FORMAT_TIME,
              GST_SEEK_FLAG_FLUSH, start_type, start, stop_type, stop))
          GST_DEBUG_OBJECT (dec, "Seek failed.");
      }
    }
  } else
    GST_DEBUG_OBJECT (dec, "Skipping element %s.", name);
}

static void
smil_warning (GstSmilDec * dec, const char * msg, ...)
{
  va_list args;

  va_start (args, msg);
  gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_DEBUG, __FILE__,
      GST_FUNCTION, __LINE__, G_OBJECT (dec), msg, args);
  va_end (args);
}

static void
smil_error (GstSmilDec * dec, const gchar * msg, ...)
{
  va_list args;

  va_start (args, msg);
  gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_DEBUG, __FILE__,
      GST_FUNCTION, __LINE__, G_OBJECT (dec), msg, args);
  va_end (args);
}

static void
smil_fatalError (GstSmilDec * dec, const gchar * msg, ...)
{
  va_list args;

  va_start (args, msg);
  gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_DEBUG, __FILE__,
      GST_FUNCTION, __LINE__, G_OBJECT (dec), msg, args);
  va_end (args);
}

static xmlSAXHandler smil_parser = {
  (internalSubsetSAXFunc) NULL,                     /* internalSubset */
  (isStandaloneSAXFunc) NULL,                       /* isStandalone */
  (hasInternalSubsetSAXFunc) NULL,                  /* hasInternalSubset */
  (hasExternalSubsetSAXFunc) NULL,                  /* hasExternalSubset */
  (resolveEntitySAXFunc) NULL,                      /* resolveEntity */
  (getEntitySAXFunc) NULL,                          /* getEntity */
  (entityDeclSAXFunc) NULL,                         /* entityDecl */
  (notationDeclSAXFunc) NULL,                       /* notationDecl */
  (attributeDeclSAXFunc) NULL,                      /* attributeDecl */
  (elementDeclSAXFunc) NULL,                        /* elementDecl */
  (unparsedEntityDeclSAXFunc) NULL,                 /* unparsedEntityDecl */
  (setDocumentLocatorSAXFunc) NULL,                 /* setDocumentLocator */
  (startDocumentSAXFunc) smil_startDocument,        /* startDocument */
  (endDocumentSAXFunc) NULL,                        /* endDocument */
  (startElementSAXFunc) smil_startElement,          /* startElement */
  (endElementSAXFunc) NULL,                         /* endElement */
  (referenceSAXFunc)NULL,                           /* reference */
  (charactersSAXFunc) NULL,                         /* characters */
  (ignorableWhitespaceSAXFunc) NULL,                /* ignorableWhitespace */
  (processingInstructionSAXFunc) NULL,              /* processingInstruction */
  (commentSAXFunc) NULL,                            /* comment */
  (warningSAXFunc) smil_warning,                    /* warning */
  (errorSAXFunc) smil_error,                        /* error */
  (fatalErrorSAXFunc) smil_fatalError,              /* fatalError */
};

static GstFlowReturn
gst_smil_dec_chain (GstPad * pad, GstBuffer * buf)
{
  GstSmilDec *dec = GST_SMIL_DEC (GST_PAD_PARENT (pad));
  int res;

  res = xmlParseChunk (dec->ctxt, (const char *) GST_BUFFER_DATA (buf),
      (int) GST_BUFFER_SIZE (buf), 0);
  gst_buffer_unref (buf);
  if (res) {
    GST_DEBUG_OBJECT (dec, "Could not parse chunk (error %i).", res);
    return GST_FLOW_ERROR;
  }

  return GST_FLOW_OK;
}

static void
gst_smil_dec_init (GstSmilDec * dec, GstSmilDecClass * klass)
{
  GstPad *snk;

  snk = gst_pad_new_from_template (
      gst_static_pad_template_get (&tsink), "sink");
  gst_element_add_pad (GST_ELEMENT (dec), snk);
  gst_pad_set_chain_function (snk, gst_smil_dec_chain);
  gst_pad_set_event_function (snk, gst_smil_dec_event);

  dec->ctxt = xmlCreatePushParserCtxt (&smil_parser, dec, NULL, 0, NULL);
}

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

  gst_element_class_add_pad_template (ec, gst_static_pad_template_get (&tsink));
  gst_element_class_add_pad_template (ec, gst_static_pad_template_get (&tsrc));
  gst_element_class_set_details (ec, &smil_dec_details);
}

static void
gst_smil_dec_finalize (GObject * object)
{
  GstSmilDec *dec = GST_SMIL_DEC (object);

  xmlFreeParserCtxt (dec->ctxt);

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

static void
gst_smil_dec_class_init (GstSmilDecClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->finalize = gst_smil_dec_finalize;

  GST_DEBUG_CATEGORY_INIT (smil_dec_debug, "smildec", 0, "Decoder for SMIL data");
}
