/*
 * gstcmmlparser.c - GStreamer CMML document parser
 * Copyright (C) 2005 Alessandro Decina
 * 
 * Authors:
 *   Alessandro Decina <alessandro@nnva.org>
 *
 * 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 <string.h>
#include <stdarg.h>
#include <gst/gst.h>

#include "gstcmmlparser.h"
#include "gstannodex.h"
#include "gstcmmlutils.h"

GST_DEBUG_CATEGORY_STATIC (cmmlparser);
#define GST_CAT_DEFAULT cmmlparser

static void gst_cmml_parser_generic_error (void *ctx, const char *msg, ...);
static xmlNodePtr gst_cmml_parser_new_node (GstCmmlParser * parser,
    const gchar * name, ...);
static void
gst_cmml_parser_parse_start_element_ns (xmlParserCtxt * ctxt,
    const xmlChar * name, const xmlChar * prefix, const xmlChar * URI,
    int nb_preferences, const xmlChar ** namespaces,
    int nb_attributes, int nb_defaulted, const xmlChar ** attributes);
static void gst_cmml_parser_parse_end_element_ns (xmlParserCtxt * ctxt,
    const xmlChar * name, const xmlChar * prefix, const xmlChar * URI);
static void gst_cmml_parser_parse_processing_instruction (xmlParserCtxtPtr ctxt,
    const xmlChar * target, const xmlChar * data);
static void gst_cmml_parser_meta_to_string (GstCmmlParser * parser,
    xmlNodePtr parent, GValueArray * meta);

/* initialize the parser */
void
gst_cmml_parser_init (void)
{
  GST_DEBUG_CATEGORY_INIT (cmmlparser, "cmmlparser", 0, "annodex CMML parser");

  xmlGenericError = gst_cmml_parser_generic_error;
}

/* create a new CMML parser
 */
GstCmmlParser *
gst_cmml_parser_new (GstCmmlParserMode mode)
{
  GstCmmlParser *parser = g_malloc (sizeof (GstCmmlParser));

  parser->mode = mode;
  parser->context = xmlCreatePushParserCtxt (NULL, NULL,
      NULL, 0, "cmml-bitstream");
  xmlCtxtUseOptions (parser->context, XML_PARSE_NONET | XML_PARSE_NOERROR);
  parser->context->_private = parser;
  parser->context->sax->startElementNs =
      (startElementNsSAX2Func) gst_cmml_parser_parse_start_element_ns;
  parser->context->sax->endElementNs =
      (endElementNsSAX2Func) gst_cmml_parser_parse_end_element_ns;
  parser->context->sax->processingInstruction = (processingInstructionSAXFunc)
      gst_cmml_parser_parse_processing_instruction;
  parser->preamble_callback = NULL;
  parser->cmml_end_callback = NULL;
  parser->stream_callback = NULL;
  parser->head_callback = NULL;
  parser->clip_callback = NULL;
  parser->user_data = NULL;

  return parser;
}

/* free a CMML parser instance
 */
void
gst_cmml_parser_free (GstCmmlParser * parser)
{
  if (parser) {
    xmlFreeDoc (parser->context->myDoc);
    xmlFreeParserCtxt (parser->context);
    g_free (parser);
  }
}

/* parse an xml chunk
 *
 * returns false if the xml is invalid
 */
gboolean
gst_cmml_parser_parse_chunk (GstCmmlParser * parser,
    const gchar * data, guint size, GError ** err)
{
  gint xmlres;

  xmlres = xmlParseChunk (parser->context, data, size, 0);
  if (xmlres != XML_ERR_OK) {
    xmlErrorPtr xml_error = xmlCtxtGetLastError (parser->context);

    GST_DEBUG ("Error occurred decoding chunk %s", data);
    g_set_error (err,
        GST_LIBRARY_ERROR, GST_LIBRARY_ERROR_FAILED, "%s", xml_error->message);
    return FALSE;
  }

  return TRUE;
}

/* convert an xmlNodePtr to a string
 */
guchar *
gst_cmml_parser_node_to_string (GstCmmlParser * parser, xmlNodePtr node)
{
  xmlBufferPtr xml_buffer;
  xmlDocPtr doc;
  guchar *str;

  if (parser)
    doc = parser->context->myDoc;
  else
    doc = NULL;

  xml_buffer = xmlBufferCreate ();
  xmlNodeDump (xml_buffer, doc, node, 0, 0);
  str = xmlStrndup (xml_buffer->content, xml_buffer->use);
  xmlBufferFree (xml_buffer);

  return str;
}

guchar *
gst_cmml_parser_tag_stream_to_string (GstCmmlParser * parser,
    GstCmmlTagStream * stream)
{
  xmlNodePtr node;
  xmlNodePtr import;
  guchar *ret;

  node = gst_cmml_parser_new_node (parser, "stream", NULL);
  if (stream->timebase)
    xmlSetProp (node, (xmlChar *) "timebase", stream->timebase);

  if (stream->utc)
    xmlSetProp (node, (xmlChar *) "utc", stream->utc);

  if (stream->imports) {
    gint i;
    GValue *val;

    for (i = 0; i < stream->imports->n_values; ++i) {
      val = g_value_array_get_nth (stream->imports, i);
      import = gst_cmml_parser_new_node (parser, "import",
          "src", g_value_get_string (val), NULL);
      xmlAddChild (node, import);
    }
  }

  ret = gst_cmml_parser_node_to_string (parser, node);

  xmlUnlinkNode (node);
  xmlFreeNode (node);

  return ret;
}

/* convert a GstCmmlTagHead to its string representation
 */
guchar *
gst_cmml_parser_tag_head_to_string (GstCmmlParser * parser,
    GstCmmlTagHead * head)
{
  xmlNodePtr node;
  xmlNodePtr tmp;
  guchar *ret;

  node = gst_cmml_parser_new_node (parser, "head", NULL);
  if (head->title) {
    tmp = gst_cmml_parser_new_node (parser, "title", NULL);
    xmlNodeSetContent (tmp, head->title);
    xmlAddChild (node, tmp);
  }

  if (head->base) {
    tmp = gst_cmml_parser_new_node (parser, "base", "uri", head->base, NULL);
    xmlAddChild (node, tmp);
  }

  if (head->meta)
    gst_cmml_parser_meta_to_string (parser, node, head->meta);

  ret = gst_cmml_parser_node_to_string (parser, node);

  xmlUnlinkNode (node);
  xmlFreeNode (node);

  return ret;
}

/* convert a GstCmmlTagClip to its string representation
 */
guchar *
gst_cmml_parser_tag_clip_to_string (GstCmmlParser * parser,
    GstCmmlTagClip * clip)
{
  xmlNodePtr node;
  xmlNodePtr tmp;
  guchar *ret;

  node = gst_cmml_parser_new_node (parser, "clip",
      "id", clip->id, "track", clip->track, NULL);
  /* add the anchor element */
  if (clip->anchor_href) {
    tmp = gst_cmml_parser_new_node (parser, "a",
        "href", clip->anchor_href, NULL);
    if (clip->anchor_text)
      xmlNodeSetContent (tmp, clip->anchor_text);

    xmlAddChild (node, tmp);
  }
  /* add the img element */
  if (clip->img_src) {
    tmp = gst_cmml_parser_new_node (parser, "img",
        "src", clip->img_src, "alt", clip->img_alt, NULL);

    xmlAddChild (node, tmp);
  }
  /* add the desc element */
  if (clip->desc_text) {
    tmp = gst_cmml_parser_new_node (parser, "desc", NULL);
    xmlNodeSetContent (tmp, clip->desc_text);

    xmlAddChild (node, tmp);
  }
  /* add the meta elements */
  if (clip->meta)
    gst_cmml_parser_meta_to_string (parser, node, clip->meta);

  if (parser->mode == GST_CMML_PARSER_DECODE) {
    gchar *time_str;

    time_str = gst_cmml_clock_time_to_npt (clip->start_time);
    if (time_str == NULL)
      goto fail;

    xmlSetProp (node, (xmlChar *) "start", (xmlChar *) time_str);
    g_free (time_str);

    if (clip->end_time != GST_CLOCK_TIME_NONE) {
      time_str = gst_cmml_clock_time_to_npt (clip->end_time);
      if (time_str == NULL)
        goto fail;

      xmlSetProp (node, (xmlChar *) "end", (xmlChar *) time_str);
      g_free (time_str);
    }
  }

  ret = gst_cmml_parser_node_to_string (parser, node);

  xmlUnlinkNode (node);
  xmlFreeNode (node);

  return ret;
fail:
  xmlUnlinkNode (node);
  xmlFreeNode (node);
  return NULL;
}

guchar *
gst_cmml_parser_tag_object_to_string (GstCmmlParser * parser, GObject * tag)
{
  guchar *tag_string = NULL;
  GType tag_type = G_OBJECT_TYPE (tag);

  if (tag_type == GST_TYPE_CMML_TAG_STREAM)
    tag_string = gst_cmml_parser_tag_stream_to_string (parser,
        GST_CMML_TAG_STREAM (tag));
  else if (tag_type == GST_TYPE_CMML_TAG_HEAD)
    tag_string = gst_cmml_parser_tag_head_to_string (parser,
        GST_CMML_TAG_HEAD (tag));
  else if (tag_type == GST_TYPE_CMML_TAG_CLIP)
    tag_string = gst_cmml_parser_tag_clip_to_string (parser,
        GST_CMML_TAG_CLIP (tag));
  else
    g_warning ("could not convert object to cmml");

  return tag_string;
}

/*** private section ***/

/* create a new node
 *
 * helper to create a node and set its attributes
 */
static xmlNodePtr
gst_cmml_parser_new_node (GstCmmlParser * parser, const gchar * name, ...)
{
  va_list args;
  xmlNodePtr node;
  xmlChar *prop_name, *prop_value;

  node = xmlNewNode (NULL, (xmlChar *) name);

  va_start (args, name);

  prop_name = va_arg (args, xmlChar *);
  while (prop_name != NULL) {
    prop_value = va_arg (args, xmlChar *);
    if (prop_value != NULL)
      xmlSetProp (node, prop_name, prop_value);

    prop_name = va_arg (args, xmlChar *);
  }
  va_end (args);

  return node;
}

/* get the last node of the stream
 *
 * returns the last node at depth 1 (if any) or the root node
 */
static xmlNodePtr
gst_cmml_parser_get_last_element (GstCmmlParser * parser)
{
  xmlNodePtr node;

  node = xmlDocGetRootElement (parser->context->myDoc);
  if (!node) {
    g_warning ("no last cmml element");
    return NULL;
  }

  if (node->children)
    node = xmlGetLastChild (node);

  return node;
}

static void
gst_cmml_parser_parse_preamble (GstCmmlParser * parser,
    const guchar * attributes)
{
  gchar *preamble;
  gchar *element;
  const gchar *version;
  const gchar *encoding;
  const gchar *standalone;
  xmlDocPtr doc;

  doc = parser->context->myDoc;

  version = doc->version ? (gchar *) doc->version : "1.0";
  encoding = doc->encoding ? (gchar *) doc->encoding : "UTF-8";
  standalone = doc->standalone ? "yes" : "no";

  preamble = g_strdup_printf ("<?xml version=\"%s\""
      " encoding=\"%s\" standalone=\"%s\"?>\n"
      "<!DOCTYPE cmml SYSTEM \"cmml.dtd\">\n", version, encoding, standalone);

  if (attributes == NULL)
    attributes = (guchar *) "";

  if (parser->mode == GST_CMML_PARSER_ENCODE)
    element = g_strdup_printf ("<?cmml %s?>", attributes);
  else
    element = g_strdup_printf ("<cmml %s>", attributes);

  parser->preamble_callback (parser->user_data,
      (guchar *) preamble, (guchar *) element);

  g_free (preamble);
  g_free (element);
}

/* parse the cmml stream tag */
static void
gst_cmml_parser_parse_stream (GstCmmlParser * parser, xmlNodePtr stream)
{
  GstCmmlTagStream *stream_tag;
  GValue str_val = { 0 };
  xmlNodePtr walk;
  guchar *timebase;

  g_value_init (&str_val, G_TYPE_STRING);

  /* read the timebase and utc attributes */
  timebase = xmlGetProp (stream, (xmlChar *) "timebase");
  if (timebase == NULL)
    timebase = (guchar *) g_strdup ("0");

  stream_tag = g_object_new (GST_TYPE_CMML_TAG_STREAM,
      "timebase", timebase, NULL);
  g_free (timebase);

  stream_tag->utc = xmlGetProp (stream, (xmlChar *) "utc");

  /* walk the children nodes */
  for (walk = stream->children; walk; walk = walk->next) {
    /* for every import tag add its src attribute to stream_tag->imports */
    if (!xmlStrcmp (walk->name, (xmlChar *) "import")) {
      g_value_take_string (&str_val,
          (gchar *) xmlGetProp (walk, (xmlChar *) "src"));

      if (stream_tag->imports == NULL)
        stream_tag->imports = g_value_array_new (0);

      g_value_array_append (stream_tag->imports, &str_val);
    }
  }
  g_value_unset (&str_val);

  parser->stream_callback (parser->user_data, stream_tag);
  g_object_unref (stream_tag);
}

/* parse the cmml head tag */
static void
gst_cmml_parser_parse_head (GstCmmlParser * parser, xmlNodePtr head)
{
  GstCmmlTagHead *head_tag;
  xmlNodePtr walk;
  GValue str_val = { 0 };

  head_tag = g_object_new (GST_TYPE_CMML_TAG_HEAD, NULL);

  g_value_init (&str_val, G_TYPE_STRING);

  /* Parse the content of the node and setup the GST_TAG_CMML_HEAD tag.
   * Create a GST_TAG_TITLE when we find the title element.
   */
  for (walk = head->children; walk; walk = walk->next) {
    if (!xmlStrcmp (walk->name, (xmlChar *) "title")) {
      head_tag->title = xmlNodeGetContent (walk);
    } else if (!xmlStrcmp (walk->name, (xmlChar *) "base")) {
      head_tag->base = xmlGetProp (walk, (xmlChar *) "uri");
    } else if (!xmlStrcmp (walk->name, (xmlChar *) "meta")) {
      if (head_tag->meta == NULL)
        head_tag->meta = g_value_array_new (0);
      /* add a pair name, content to the meta value array */
      g_value_take_string (&str_val,
          (gchar *) xmlGetProp (walk, (xmlChar *) "name"));
      g_value_array_append (head_tag->meta, &str_val);
      g_value_take_string (&str_val,
          (gchar *) xmlGetProp (walk, (xmlChar *) "content"));
      g_value_array_append (head_tag->meta, &str_val);
    }
  }
  g_value_unset (&str_val);

  parser->head_callback (parser->user_data, head_tag);
  g_object_unref (head_tag);
}

/* parse a cmml clip tag */
static void
gst_cmml_parser_parse_clip (GstCmmlParser * parser, xmlNodePtr clip)
{
  GstCmmlTagClip *clip_tag;
  GValue str_val = { 0 };
  guchar *id, *track, *start, *end;
  xmlNodePtr walk;
  GstClockTime start_time = GST_CLOCK_TIME_NONE;
  GstClockTime end_time = GST_CLOCK_TIME_NONE;

  start = xmlGetProp (clip, (xmlChar *) "start");
  if (parser->mode == GST_CMML_PARSER_ENCODE && start == NULL)
    /* XXX: validate the document */
    return;

  id = xmlGetProp (clip, (xmlChar *) "id");
  track = xmlGetProp (clip, (xmlChar *) "track");
  end = xmlGetProp (clip, (xmlChar *) "end");

  if (track == NULL)
    track = (guchar *) g_strdup ("default");

  if (start) {
    if (!strncmp ((gchar *) start, "smpte", 5))
      start_time = gst_cmml_clock_time_from_smpte ((gchar *) start);
    else
      start_time = gst_cmml_clock_time_from_npt ((gchar *) start);
  }

  if (end) {
    if (!strncmp ((gchar *) end, "smpte", 5))
      start_time = gst_cmml_clock_time_from_smpte ((gchar *) end);
    else
      end_time = gst_cmml_clock_time_from_npt ((gchar *) end);
  }

  clip_tag = g_object_new (GST_TYPE_CMML_TAG_CLIP, "id", id,
      "track", track, "start-time", start_time, "end-time", end_time, NULL);

  g_free (id);
  g_free (track);
  g_free (start);
  g_free (end);

  g_value_init (&str_val, G_TYPE_STRING);

  /* parse the children */
  for (walk = clip->children; walk; walk = walk->next) {
    /* the clip is not empty */
    clip_tag->empty = FALSE;

    if (!xmlStrcmp (walk->name, (xmlChar *) "a")) {
      clip_tag->anchor_href = xmlGetProp (walk, (xmlChar *) "href");
      clip_tag->anchor_text = xmlNodeGetContent (walk);
    } else if (!xmlStrcmp (walk->name, (xmlChar *) "img")) {
      clip_tag->img_src = xmlGetProp (walk, (xmlChar *) "src");
      clip_tag->img_alt = xmlGetProp (walk, (xmlChar *) "alt");
    } else if (!xmlStrcmp (walk->name, (xmlChar *) "desc")) {
      clip_tag->desc_text = xmlNodeGetContent (walk);
    } else if (!xmlStrcmp (walk->name, (xmlChar *) "meta")) {
      if (clip_tag->meta == NULL)
        clip_tag->meta = g_value_array_new (0);
      /* add a pair name, content to the meta value array */
      g_value_take_string (&str_val,
          (char *) xmlGetProp (walk, (xmlChar *) "name"));
      g_value_array_append (clip_tag->meta, &str_val);
      g_value_take_string (&str_val,
          (char *) xmlGetProp (walk, (xmlChar *) "content"));
      g_value_array_append (clip_tag->meta, &str_val);
    }
  }
  g_value_unset (&str_val);

  parser->clip_callback (parser->user_data, clip_tag);
  g_object_unref (clip_tag);
}

void
gst_cmml_parser_meta_to_string (GstCmmlParser * parser,
    xmlNodePtr parent, GValueArray * array)
{
  gint i;
  xmlNodePtr node;
  GValue *name, *content;

  for (i = 0; i < array->n_values - 1; i += 2) {
    name = g_value_array_get_nth (array, i);
    content = g_value_array_get_nth (array, i + 1);
    node = gst_cmml_parser_new_node (parser, "meta",
        "name", g_value_get_string (name),
        "content", g_value_get_string (content), NULL);
    xmlAddChild (parent, node);
  }
}

static void
gst_cmml_parser_generic_error (void *ctx, const char *msg, ...)
{
#ifndef GST_DISABLE_GST_DEBUG
  va_list varargs;

  va_start (varargs, msg);
  gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_WARNING,
      "", "", 0, NULL, msg, varargs);
  va_end (varargs);
#endif /* GST_DISABLE_GST_DEBUG */
}

/* sax handler called when an element start tag is found
 * this is used to parse the cmml start tag
 */
static void
gst_cmml_parser_parse_start_element_ns (xmlParserCtxt * ctxt,
    const xmlChar * name, const xmlChar * prefix, const xmlChar * URI,
    int nb_preferences, const xmlChar ** namespaces,
    int nb_attributes, int nb_defaulted, const xmlChar ** attributes)
{
  GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;

  xmlSAX2StartElementNs (ctxt, name, prefix, URI, nb_preferences, namespaces,
      nb_attributes, nb_defaulted, attributes);

  if (parser->mode == GST_CMML_PARSER_ENCODE)
    if (!xmlStrcmp (name, (xmlChar *) "cmml"))
      if (parser->preamble_callback)
        /* FIXME: parse attributes */
        gst_cmml_parser_parse_preamble (parser, NULL);
}

/* sax processing instruction handler
 * used to parse the cmml processing instruction
 */
static void
gst_cmml_parser_parse_processing_instruction (xmlParserCtxtPtr ctxt,
    const xmlChar * target, const xmlChar * data)
{
  GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;

  xmlSAX2ProcessingInstruction (ctxt, target, data);

  if (parser->mode == GST_CMML_PARSER_DECODE)
    if (!xmlStrcmp (target, (xmlChar *) "cmml"))
      if (parser->preamble_callback)
        gst_cmml_parser_parse_preamble (parser, data);
}

/* sax handler called when an xml end tag is found
 * used to parse the stream, head and clip nodes
 */
static void
gst_cmml_parser_parse_end_element_ns (xmlParserCtxt * ctxt,
    const xmlChar * name, const xmlChar * prefix, const xmlChar * URI)
{
  xmlNodePtr node;
  GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;

  xmlSAX2EndElementNs (ctxt, name, prefix, URI);

  if (!xmlStrcmp (name, (xmlChar *) "clip")) {
    if (parser->clip_callback) {
      node = gst_cmml_parser_get_last_element (parser);
      gst_cmml_parser_parse_clip (parser, node);
    }
  } else if (!xmlStrcmp (name, (xmlChar *) "cmml")) {
    if (parser->cmml_end_callback)
      parser->cmml_end_callback (parser->user_data);
  } else if (!xmlStrcmp (name, (xmlChar *) "stream")) {
    if (parser->stream_callback) {
      node = gst_cmml_parser_get_last_element (parser);
      gst_cmml_parser_parse_stream (parser, node);
    }
  } else if (!xmlStrcmp (name, (xmlChar *) "head")) {
    if (parser->head_callback) {
      node = gst_cmml_parser_get_last_element (parser);
      gst_cmml_parser_parse_head (parser, node);
    }
  }
}