/* -*- Mode: C; tab-width: 2; indent-tabs-mode: t; c-basic-offset: 2 -*- */
/* Copyright 2005 Jan Schmidt <thaytan@mad.scientist.com>
 *           2006 Michael Smith <msmith@fluendo.com>
 * Copyright (C) 2003-2004 Benjamin Otte <otte@gnome.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.
 */

/**
 * SECTION:element-icydemux
 * @short_description: reads tag information from an Icy (Icecast/Shoutcast) 
 * stream, outputting them as tag messages, and forwarding the enclosed data.
 *
 * <refsect2>
 * <para>
 * icydemux accepts data streams with ICY metadata at known intervals, as transmitted from an upstream element (usually read as response headers from an HTTP stream). The mime type of the data between the tag blocks is detected using typefind functions, and the appropriate output mime type set on outgoing buffers. 
 * </para>
 * <title>Example launch line</title>
 * <para>
 * <programlisting>
 * gst-launch gnomevfssrc location=http://some.server/ ! icydemux ! fakesink -t
 * </programlisting>
 * This pipeline should read any available ICY tag information and output it. The contents of the stream should be detected, and the appropriate mime type set on buffers produced from icydemux.
 * </para>
 * </refsect2>
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gst/gst.h>
#include <gst/gst-i18n-plugin.h>

#include "gsticydemux.h"

#include <string.h>

static GstElementDetails gst_icydemux_details =
GST_ELEMENT_DETAILS ("ICY tag demuxer",
    "Codec/Demuxer/Metadata",
    "Read and output ICY tags while demuxing the contents",
    "Jan Schmidt <thaytan@mad.scientist.com>\n"
    "Michael Smith <msmith@fluendo.com>");

#define ICY_TYPE_FIND_MAX_SIZE (40*1024)

GST_DEBUG_CATEGORY (icydemux_debug);
#define GST_CAT_DEFAULT (icydemux_debug)

static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/x-icy, metadata-interval = (int)[0, MAX]")
    );

static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_SOMETIMES,
    GST_STATIC_CAPS ("ANY")
    );

static void gst_icydemux_class_init (GstICYDemuxClass * klass);
static void gst_icydemux_base_init (GstICYDemuxClass * klass);
static void gst_icydemux_init (GstICYDemux * icydemux);
static void gst_icydemux_dispose (GObject * object);

static GstFlowReturn gst_icydemux_chain (GstPad * pad, GstBuffer * buf);

static gboolean gst_icydemux_add_srcpad (GstICYDemux * icydemux,
    GstCaps * new_caps);
static gboolean gst_icydemux_remove_srcpad (GstICYDemux * icydemux);

static GstStateChangeReturn gst_icydemux_change_state (GstElement * element,
    GstStateChange transition);
static gboolean gst_icydemux_sink_setcaps (GstPad * pad, GstCaps *caps);

static void gst_icydemux_send_tag_event (GstICYDemux * icydemux, 
        GstTagList *taglist);

static GstElementClass *parent_class = NULL;

GType
gst_icydemux_get_type (void)
{
  static GType plugin_type = 0;

  if (!plugin_type) {
    static const GTypeInfo plugin_info = {
      sizeof (GstICYDemuxClass),
      (GBaseInitFunc) gst_icydemux_base_init,
      NULL,
      (GClassInitFunc) gst_icydemux_class_init,
      NULL,
      NULL,
      sizeof (GstICYDemux),
      0,
      (GInstanceInitFunc) gst_icydemux_init,
    };
    plugin_type = g_type_register_static (GST_TYPE_ELEMENT,
        "GstICYDemux", &plugin_info, 0);
  }
  return plugin_type;
}

static void
gst_icydemux_base_init (GstICYDemuxClass * klass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&src_factory));
  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sink_factory));
  gst_element_class_set_details (element_class, &gst_icydemux_details);
}

static void
gst_icydemux_class_init (GstICYDemuxClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = (GObjectClass *) klass;
  gstelement_class = (GstElementClass *) klass;

  parent_class = g_type_class_ref (GST_TYPE_ELEMENT);

  gobject_class->dispose = gst_icydemux_dispose;

  gstelement_class->change_state = gst_icydemux_change_state;

}

static void
gst_icydemux_reset (GstICYDemux * icydemux)
{
  /* Unknown at the moment (this is a fatal error if don't have a value by the
   * time we get to our chain function)
   */
  icydemux->meta_interval = -1;
  icydemux->remaining = 0;

  icydemux->typefinding = TRUE;

  gst_caps_replace (&(icydemux->src_caps), NULL);

  gst_icydemux_remove_srcpad (icydemux);

  if (icydemux->cached_tags) {
    gst_tag_list_free (icydemux->cached_tags);
    icydemux->cached_tags = NULL;
  }

  if (icydemux->meta_adapter) {
    gst_adapter_clear (icydemux->meta_adapter);
    g_object_unref (icydemux->meta_adapter);
    icydemux->meta_adapter = NULL;
  }

  if (icydemux->typefind_adapter) {
    gst_adapter_clear (icydemux->typefind_adapter);
    g_object_unref (icydemux->typefind_adapter);
    icydemux->typefind_adapter = NULL;
  }
}

static void
gst_icydemux_init (GstICYDemux * icydemux)
{
  GstElementClass *klass = GST_ELEMENT_GET_CLASS (icydemux);

  icydemux->sinkpad =
      gst_pad_new_from_template (gst_element_class_get_pad_template (klass,
          "sink"), "sink");
  gst_pad_set_chain_function (icydemux->sinkpad,
      GST_DEBUG_FUNCPTR (gst_icydemux_chain));
  gst_pad_set_setcaps_function (icydemux->sinkpad,
      GST_DEBUG_FUNCPTR (gst_icydemux_sink_setcaps));
  gst_element_add_pad (GST_ELEMENT (icydemux), icydemux->sinkpad);

  gst_icydemux_reset (icydemux);
}

static gboolean gst_icydemux_sink_setcaps (GstPad * pad, GstCaps *caps)
{
  GstICYDemux *icydemux = GST_ICYDEMUX (GST_PAD_PARENT (pad));
  GstStructure *structure = gst_caps_get_structure (caps, 0);

  if (!gst_structure_get_int (structure, "metadata-interval", 
              &icydemux->meta_interval))
    return FALSE;
  else {
    /* We have a meta interval, so initialise the rest */
    icydemux->remaining = icydemux->meta_interval;
    icydemux->meta_remaining = 0;
    return TRUE;
  }
}

static void
gst_icydemux_dispose (GObject * object)
{
  GstICYDemux *icydemux = GST_ICYDEMUX (object);

  gst_icydemux_reset (icydemux);

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

static gboolean
gst_icydemux_add_srcpad (GstICYDemux * icydemux, GstCaps * new_caps)
{
  GstPad *srcpad = NULL;

  if (icydemux->src_caps == NULL ||
      !gst_caps_is_equal (new_caps, icydemux->src_caps)) 
  {
    gst_caps_replace (&(icydemux->src_caps), new_caps);
    if (icydemux->srcpad != NULL) {
      GST_DEBUG_OBJECT (icydemux, "Changing src pad caps to %" GST_PTR_FORMAT,
          icydemux->src_caps);

      gst_pad_set_caps (icydemux->srcpad, icydemux->src_caps);
    }
  } else {
    /* Caps never changed */
    gst_caps_unref (new_caps);
  }

  if (icydemux->srcpad == NULL) {
    srcpad = icydemux->srcpad =
        gst_pad_new_from_template (gst_element_class_get_pad_template
        (GST_ELEMENT_GET_CLASS (icydemux), "src"), "src");
    g_return_val_if_fail (icydemux->srcpad != NULL, FALSE);

    gst_pad_use_fixed_caps (icydemux->srcpad);

    if (icydemux->src_caps)
      gst_pad_set_caps (icydemux->srcpad, icydemux->src_caps);

    GST_DEBUG_OBJECT (icydemux, "Adding src pad with caps %" GST_PTR_FORMAT,
        icydemux->src_caps);

    gst_object_ref (icydemux->srcpad);
    if (!(gst_element_add_pad (GST_ELEMENT (icydemux), icydemux->srcpad)))
      return FALSE;
    gst_element_no_more_pads (GST_ELEMENT (icydemux));
  }

  return TRUE;
}

static gboolean
gst_icydemux_remove_srcpad (GstICYDemux * icydemux)
{
  gboolean res = TRUE;

  if (icydemux->srcpad != NULL) {
    GST_DEBUG_OBJECT (icydemux, "Removing src pad");
    res = gst_element_remove_pad (GST_ELEMENT (icydemux), icydemux->srcpad);
    g_return_val_if_fail (res != FALSE, FALSE);
    gst_object_unref (icydemux->srcpad);
    icydemux->srcpad = NULL;
  }

  return res;
};

/* The following two charset mangling functions were copied from gnomevfssrc.
 * Preserve them under the unverified assumption that they do something vaguely
 * worthwhile.
 */
static char *
unicodify (const char *str, int len, ...)
{
  char *ret = NULL, *cset;
  va_list args;
  gsize bytes_read, bytes_written;

  if (g_utf8_validate (str, len, NULL))
    return g_strndup (str, len >= 0 ? len : strlen (str));

  va_start (args, len);
  while ((cset = va_arg (args, char *)) != NULL)
  {
    if (!strcmp (cset, "locale"))
      ret = g_locale_to_utf8 (str, len, &bytes_read, &bytes_written, NULL);
    else
      ret = g_convert (str, len, "UTF-8", cset,
          &bytes_read, &bytes_written, NULL);
    if (ret)
      break;
  }
  va_end (args);

  return ret;
}

static char *
gst_icydemux_unicodify (const char *str)
{
  return unicodify (str, -1, "locale", "ISO-8859-1", NULL);
}

static void
gst_icydemux_parse_and_send_tags (GstICYDemux *icydemux)
{
  GstTagList *tags = gst_tag_list_new();
  const guint8 *data;
  int length, i;
  gchar *buffer;
  gchar **strings;
  gboolean found_tag = FALSE;

  length = gst_adapter_available (icydemux->meta_adapter);

  data = gst_adapter_peek (icydemux->meta_adapter, length);

  /* Now, copy this to a buffer where we can NULL-terminate it to make things
   * a bit easier, then do that parsing. */
  buffer = g_malloc (length+1);
  memcpy (buffer, data, length);
  buffer[length] = 0;

  strings = g_strsplit (buffer, "';", 0);

  for (i = 0; strings[i]; i++) {
    if (!g_ascii_strncasecmp (strings[i], "StreamTitle=", 12)) {
      char *title = gst_icydemux_unicodify (strings[i] + 13);
      if (title && *title) {
        gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_TITLE, 
                title, NULL);
        g_free (title);
        found_tag = TRUE;
      }
    }
    else if (!g_ascii_strncasecmp (strings[i], "StreamUrl=", 10)) {
      char *url = gst_icydemux_unicodify (strings[i] + 11);
      if (url) {
        /* 
        gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_URL, 
              url, NULL); 
        found_tag = TRUE;
        */
        g_free (url);
      }
    }
  }

  g_strfreev (strings);
  g_free (buffer);
  gst_adapter_clear (icydemux->meta_adapter);

  if (found_tag) {
    if (icydemux->srcpad) {
      gst_icydemux_send_tag_event (icydemux, tags);
    }
    else {
      if (!icydemux->cached_tags) {
        icydemux->cached_tags = gst_tag_list_new ();
      }

      gst_tag_list_insert (icydemux->cached_tags, tags, 
              GST_TAG_MERGE_REPLACE_ALL);
    }
  }
}

static GstFlowReturn
gst_icydemux_typefind_or_forward (GstICYDemux *icydemux, GstBuffer *buf)
{
  if (icydemux->typefinding) {
    GstBuffer *typefind_buf;
    const guint8 *data;
    GstCaps *caps;
    int size;
    guint prob;

    if (!icydemux->typefind_adapter)
      icydemux->typefind_adapter = gst_adapter_new();

    gst_adapter_push (icydemux->typefind_adapter, buf);

    size = gst_adapter_available (icydemux->typefind_adapter);
    typefind_buf = gst_buffer_new ();
    
    data = gst_adapter_peek (icydemux->typefind_adapter, size);

    GST_BUFFER_DATA (typefind_buf) = (guint8 *)data;
    GST_BUFFER_SIZE (typefind_buf) = size;

    caps = gst_type_find_helper_for_buffer(GST_OBJECT (icydemux), 
            typefind_buf, &prob);

    if (caps == NULL) {
      if (size < ICY_TYPE_FIND_MAX_SIZE) {
        gst_buffer_unref (typefind_buf);
          /* Just break for more data */
        return GST_FLOW_OK;
      }

      /* We failed typefind */
      GST_ELEMENT_ERROR (icydemux, CORE, CAPS,
          ("Could not determine the mime type of the file"),
          ("No caps found for contents within an ICY stream"));
      gst_buffer_unref (typefind_buf);
      gst_adapter_clear (icydemux->typefind_adapter);
      return GST_FLOW_ERROR;
    }

    if (!gst_icydemux_add_srcpad (icydemux, caps)) {
      GST_DEBUG_OBJECT (icydemux, "Failed to add srcpad");
      gst_caps_unref (caps);
      gst_buffer_unref (typefind_buf);
      gst_adapter_clear (icydemux->typefind_adapter);
      return GST_FLOW_ERROR;
    }
    gst_caps_unref (caps);

    if (icydemux->cached_tags) {
      gst_icydemux_send_tag_event (icydemux, icydemux->cached_tags);
      icydemux->cached_tags = NULL;
    }

    /* Move onto streaming: call ourselves recursively with the typefind buffer
     * to get that forwarded. */
    icydemux->typefinding = FALSE;

    data = gst_adapter_take (icydemux->typefind_adapter, size);
    GST_BUFFER_DATA (typefind_buf) = (guint8 *)data;
    GST_BUFFER_MALLOCDATA (typefind_buf) = (guint8 *)data;

    return gst_icydemux_typefind_or_forward (icydemux, typefind_buf);
  }
  else {
    if (G_UNLIKELY (icydemux->srcpad == NULL)) {
      gst_buffer_unref (buf);
      return GST_FLOW_ERROR;
    }

    gst_buffer_set_caps (buf, icydemux->src_caps);

    /* Most things don't care, and it's a pain to track */
    GST_BUFFER_OFFSET (buf) = GST_BUFFER_OFFSET_NONE;

    GST_DEBUG_OBJECT (icydemux, "Pushing buffer %p", buf);
    return gst_pad_push (icydemux->srcpad, buf);
  }
}

static  void
gst_icydemux_add_meta (GstICYDemux *icydemux, GstBuffer *buf)
{
  if (!icydemux->meta_adapter)
    icydemux->meta_adapter = gst_adapter_new();

  gst_adapter_push (icydemux->meta_adapter, buf);
}

static GstFlowReturn
gst_icydemux_chain (GstPad * pad, GstBuffer * buf)
{
  GstICYDemux *icydemux;
  guint size, chunk, offset;
  GstBuffer *sub;
  GstFlowReturn ret = GST_FLOW_OK;

  icydemux = GST_ICYDEMUX (GST_PAD_PARENT (pad));
  g_return_val_if_fail (GST_IS_ICYDEMUX (icydemux), GST_FLOW_ERROR);
  g_return_val_if_fail (icydemux->meta_interval >= 0, GST_FLOW_ERROR);

  GST_DEBUG_OBJECT (icydemux, "Chain: size %d, offset %d, remaining %d", (int)(GST_BUFFER_SIZE(buf)), (int)(GST_BUFFER_OFFSET(buf)), icydemux->remaining);

  if (icydemux->meta_interval == 0) {
    ret = gst_icydemux_typefind_or_forward (icydemux, buf);
    goto done;
  }

  /* Go through the buffer, chopping it into appropriate chunks. Forward as
   * tags or buffers, as appropriate
   */
  size = GST_BUFFER_SIZE (buf);
  offset = 0;
  while (size) {
    if (icydemux->remaining) {
      chunk = (size <= icydemux->remaining) ? size : icydemux->remaining;
      sub = gst_buffer_create_sub (buf, offset, chunk);
      offset += chunk;
      icydemux->remaining -= chunk;
      size -= chunk;

      /* This buffer goes onto typefinding, and/or directly pushed out */
      ret = gst_icydemux_typefind_or_forward (icydemux, sub);
      if (ret != GST_FLOW_OK)
        goto done;
    }
    else if (icydemux->meta_remaining) {
      chunk = (size <= icydemux->meta_remaining) ? 
          size : icydemux->meta_remaining;
      sub = gst_buffer_create_sub (buf, offset, chunk);
      gst_icydemux_add_meta (icydemux, sub);

      offset += chunk;
      icydemux->meta_remaining -= chunk;
      size -= chunk;

      if (icydemux->meta_remaining == 0) {
        /* Parse tags from meta_adapter, send off as tag messages */
        gst_icydemux_parse_and_send_tags (icydemux);

        icydemux->remaining = icydemux->meta_interval;
      }
    }
    else {
      /* We need to read a single byte (always safe at this point in the loop)
       * to figure out how many bytes of metadata exist. 
       * The 'spec' tells us to read 16 * (byte_value) bytes of metadata after
       * this (zero is common, and means the metadata hasn't changed).
       */
      icydemux->meta_remaining = 16 * GST_BUFFER_DATA (buf)[offset];
      if (icydemux->meta_remaining == 0)
        icydemux->remaining = icydemux->meta_interval;
      GST_DEBUG_OBJECT (icydemux, "Meta length: %d", icydemux->meta_remaining);

      offset += 1;
      size -= 1;
    }
  }

done:
  gst_buffer_unref (buf);

  return ret;
}

static GstStateChangeReturn
gst_icydemux_change_state (GstElement * element, GstStateChange transition)
{
  GstStateChangeReturn ret;
  GstICYDemux *icydemux = GST_ICYDEMUX (element);

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      gst_icydemux_reset (icydemux);
      break;
    default:
      break;
  }
  return ret;
}

static void
gst_icydemux_send_tag_event (GstICYDemux * icydemux, GstTagList *tags)
{
  GstEvent *event;

  gst_element_post_message (GST_ELEMENT (icydemux), 
          gst_message_new_tag (GST_OBJECT (icydemux), 
              gst_tag_list_copy (tags)));

  event = gst_event_new_tag (tags);
  GST_EVENT_TIMESTAMP (event) = 0;

  GST_DEBUG_OBJECT (icydemux, "Sending tag event on src pad");
  gst_pad_push_event (icydemux->srcpad, event);

}

static gboolean
plugin_init (GstPlugin * plugin)
{
  GST_DEBUG_CATEGORY_INIT (icydemux_debug, "icydemux", 0,
      "GStreamer ICY tag demuxer");

  return gst_element_register (plugin, "icydemux",
      GST_RANK_PRIMARY, GST_TYPE_ICYDEMUX);
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    "icydemux",
    "Demux ICY tags from a stream",
    plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)

