/* GStreamer
 * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
 * Copyright (C) <2003> David A. Schleef <ds@schleef.org>
 * Copyright (C) <2006> Wim Taymans <wim@fluendo.com>
 * Copyright (C) <2007> Julien Moutte <julien@fluendo.com>
 * Copyright (C) <2009> Tim-Philipp Müller <tim centricular net>
 * Copyright (C) <2009> STEricsson <benjamin.gaignard@stericsson.com>
 * Copyright (C) <2013> Sreerenj Balachandran <sreerenj.balachandran@intel.com>
 * Copyright (C) <2013> Intel Corporation
 * Copyright (C) <2014> Centricular Ltd
 * Copyright (C) <2015> YouView TV Ltd.
 * Copyright (C) <2016> British Broadcasting Corporation
 *
 * 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/* Parsing functions for various MP4 standard extension atom groups */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <gst/base/gstbytereader.h>
#include <gst/tag/tag.h>

#include "qtdemux_tags.h"
#include "qtdemux_tree.h"
#include "qtdemux_types.h"
#include "qtdemux_debug.h"
#include "fourcc.h"

#define GST_CAT_DEFAULT qtdemux_debug

static GstBuffer *
_gst_buffer_new_wrapped (gpointer mem, gsize size, GFreeFunc free_func)
{
  return gst_buffer_new_wrapped_full (free_func ? 0 : GST_MEMORY_FLAG_READONLY,
      mem, size, 0, size, mem, free_func);
}

/* check if major or compatible brand is 3GP */
static inline gboolean
qtdemux_is_brand_3gp (GstQTDemux * qtdemux, gboolean major)
{
  if (major) {
    return ((qtdemux->major_brand & GST_MAKE_FOURCC (255, 255, 0, 0)) ==
        FOURCC_3g__);
  } else if (qtdemux->comp_brands != NULL) {
    GstMapInfo map;
    guint8 *data;
    gsize size;
    gboolean res = FALSE;

    gst_buffer_map (qtdemux->comp_brands, &map, GST_MAP_READ);
    data = map.data;
    size = map.size;
    while (size >= 4) {
      res = res || ((QT_FOURCC (data) & GST_MAKE_FOURCC (255, 255, 0, 0)) ==
          FOURCC_3g__);
      data += 4;
      size -= 4;
    }
    gst_buffer_unmap (qtdemux->comp_brands, &map);
    return res;
  } else {
    return FALSE;
  }
}

/* check if tag is a spec'ed 3GP tag keyword storing a string */
static inline gboolean
qtdemux_is_string_tag_3gp (GstQTDemux * qtdemux, guint32 fourcc)
{
  return fourcc == FOURCC_cprt || fourcc == FOURCC_gnre || fourcc == FOURCC_titl
      || fourcc == FOURCC_dscp || fourcc == FOURCC_perf || fourcc == FOURCC_auth
      || fourcc == FOURCC_albm;
}

static void
qtdemux_tag_add_location (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
  int offset;
  char *name;
  gchar *data;
  gdouble longitude, latitude, altitude;
  gint len;

  len = QT_UINT32 (node->data);
  if (len <= 14)
    goto short_read;

  data = node->data;
  offset = 14;

  /* TODO: language code skipped */

  name = gst_tag_freeform_string_to_utf8 (data + offset, -1, env_vars);

  if (!name) {
    /* do not alarm in trivial case, but bail out otherwise */
    if (*(data + offset) != 0) {
      GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8, "
          "giving up", tag);
    }
  } else {
    gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE,
        GST_TAG_GEO_LOCATION_NAME, name, NULL);
    offset += strlen (name);
    g_free (name);
  }

  if (len < offset + 2 + 4 + 4 + 4)
    goto short_read;

  /* +1 +1 = skip null-terminator and location role byte */
  offset += 1 + 1;
  /* table in spec says unsigned, semantics say negative has meaning ... */
  longitude = QT_SFP32 (data + offset);

  offset += 4;
  latitude = QT_SFP32 (data + offset);

  offset += 4;
  altitude = QT_SFP32 (data + offset);

  /* one invalid means all are invalid */
  if (longitude >= -180.0 && longitude <= 180.0 &&
      latitude >= -90.0 && latitude <= 90.0) {
    gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE,
        GST_TAG_GEO_LOCATION_LATITUDE, latitude,
        GST_TAG_GEO_LOCATION_LONGITUDE, longitude,
        GST_TAG_GEO_LOCATION_ELEVATION, altitude, NULL);
  }

  /* TODO: no GST_TAG_, so astronomical body and additional notes skipped */

  return;

  /* ERRORS */
short_read:
  {
    GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP location");
    return;
  }
}


static void
qtdemux_tag_add_year (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  guint16 y;
  GDate *date;
  gint len;

  len = QT_UINT32 (node->data);
  if (len < 14)
    return;

  y = QT_UINT16 ((guint8 *) node->data + 12);
  if (y == 0) {
    GST_DEBUG_OBJECT (qtdemux, "year: %u is not a valid year", y);
    return;
  }
  GST_DEBUG_OBJECT (qtdemux, "year: %u", y);

  date = g_date_new_dmy (1, 1, y);
  gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, date, NULL);
  g_date_free (date);
}

static void
qtdemux_tag_add_classification (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  int offset;
  char *tag_str = NULL;
  guint8 *entity;
  guint16 table;
  gint len;

  len = QT_UINT32 (node->data);
  if (len <= 20)
    goto short_read;

  offset = 12;
  entity = (guint8 *) node->data + offset;
  if (entity[0] == 0 || entity[1] == 0 || entity[2] == 0 || entity[3] == 0) {
    GST_DEBUG_OBJECT (qtdemux,
        "classification info: %c%c%c%c invalid classification entity",
        entity[0], entity[1], entity[2], entity[3]);
    return;
  }

  offset += 4;
  table = QT_UINT16 ((guint8 *) node->data + offset);

  /* Language code skipped */

  offset += 4;

  /* Tag format: "XXXX://Y[YYYY]/classification info string"
   * XXXX: classification entity, fixed length 4 chars.
   * Y[YYYY]: classification table, max 5 chars.
   */
  tag_str = g_strdup_printf ("----://%u/%s",
      table, (char *) node->data + offset);

  /* memcpy To be sure we're preserving byte order */
  memcpy (tag_str, entity, 4);
  GST_DEBUG_OBJECT (qtdemux, "classification info: %s", tag_str);

  gst_tag_list_add (taglist, GST_TAG_MERGE_APPEND, tag, tag_str, NULL);

  g_free (tag_str);

  return;

  /* ERRORS */
short_read:
  {
    GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP classification");
    return;
  }
}

static gboolean
qtdemux_tag_add_str_full (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
  GNode *data;
  char *s;
  int len;
  guint32 type;
  int offset;
  gboolean ret = TRUE;
  const gchar *charset = NULL;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    if (type == 0x00000001 && len > 16) {
      s = gst_tag_freeform_string_to_utf8 ((char *) data->data + 16, len - 16,
          env_vars);
      if (s) {
        GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (s));
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, s, NULL);
        g_free (s);
      } else {
        GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8", tag);
      }
    }
  } else {
    len = QT_UINT32 (node->data);
    type = QT_UINT32 ((guint8 *) node->data + 4);
    if ((type >> 24) == 0xa9 && len > 8 + 4) {
      gint str_len;
      gint lang_code;

      /* Type starts with the (C) symbol, so the next data is a list
       * of (string size(16), language code(16), string) */

      str_len = QT_UINT16 ((guint8 *) node->data + 8);
      lang_code = QT_UINT16 ((guint8 *) node->data + 10);

      /* the string + fourcc + size + 2 16bit fields,
       * means that there are more tags in this atom */
      if (len > str_len + 8 + 4) {
        /* TODO how to represent the same tag in different languages? */
        GST_WARNING_OBJECT (qtdemux, "Ignoring metadata entry with multiple "
            "text alternatives, reading only first one");
      }

      offset = 12;
      len = MIN (len, str_len + 8 + 4); /* remove trailing strings that we don't use */
      GST_DEBUG_OBJECT (qtdemux, "found international text tag");

      if (lang_code < 0x800) {  /* MAC encoded string */
        charset = "mac";
      }
    } else if (len > 14 && qtdemux_is_string_tag_3gp (qtdemux,
            QT_FOURCC ((guint8 *) node->data + 4))) {
      guint32 type = QT_UINT32 ((guint8 *) node->data + 8);

      /* we go for 3GP style encoding if major brands claims so,
       * or if no hope for data be ok UTF-8, and compatible 3GP brand present */
      if (qtdemux_is_brand_3gp (qtdemux, TRUE) ||
          (qtdemux_is_brand_3gp (qtdemux, FALSE) &&
              ((type & 0x00FFFFFF) == 0x0) && (type >> 24 <= 0xF))) {
        offset = 14;
        /* 16-bit Language code is ignored here as well */
        GST_DEBUG_OBJECT (qtdemux, "found 3gpp text tag");
      } else {
        goto normal;
      }
    } else {
    normal:
      offset = 8;
      GST_DEBUG_OBJECT (qtdemux, "found normal text tag");
      ret = FALSE;              /* may have to fallback */
    }
    if (charset) {
      GError *err = NULL;

      s = g_convert ((gchar *) node->data + offset, len - offset, "utf8",
          charset, NULL, NULL, &err);
      if (err) {
        GST_DEBUG_OBJECT (qtdemux, "Failed to convert string from charset %s:"
            " %s(%d): %s", charset, g_quark_to_string (err->domain), err->code,
            err->message);
        g_error_free (err);
      }
    } else {
      s = gst_tag_freeform_string_to_utf8 ((char *) node->data + offset,
          len - offset, env_vars);
    }
    if (s) {
      GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (s));
      gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, s, NULL);
      g_free (s);
      ret = TRUE;
    } else {
      GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8", tag);
    }
  }
  return ret;
}

static void
qtdemux_tag_add_str (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  qtdemux_tag_add_str_full (qtdemux, taglist, tag, dummy, node);
}

static void
qtdemux_tag_add_keywords (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
  guint8 *data;
  char *s, *t, *k = NULL;
  int len;
  int offset;
  int count;

  /* first try normal string tag if major brand not 3GP */
  if (!qtdemux_is_brand_3gp (qtdemux, TRUE)) {
    if (!qtdemux_tag_add_str_full (qtdemux, taglist, tag, dummy, node)) {
      /* hm, that did not work, maybe 3gpp storage in non-3gpp major brand;
       * let's try it 3gpp way after minor safety check */
      data = node->data;
      if (QT_UINT32 (data) < 15 || !qtdemux_is_brand_3gp (qtdemux, FALSE))
        return;
    } else
      return;
  }

  GST_DEBUG_OBJECT (qtdemux, "found 3gpp keyword tag");

  data = node->data;

  len = QT_UINT32 (data);
  if (len < 15)
    goto short_read;

  count = QT_UINT8 (data + 14);
  offset = 15;
  for (; count; count--) {
    gint slen;

    if (offset + 1 > len)
      goto short_read;
    slen = QT_UINT8 (data + offset);
    offset += 1;
    if (offset + slen > len)
      goto short_read;
    s = gst_tag_freeform_string_to_utf8 ((char *) node->data + offset,
        slen, env_vars);
    if (s) {
      GST_DEBUG_OBJECT (qtdemux, "adding keyword %s", GST_STR_NULL (s));
      if (k) {
        t = g_strjoin (",", k, s, NULL);
        g_free (s);
        g_free (k);
        k = t;
      } else {
        k = s;
      }
    } else {
      GST_DEBUG_OBJECT (qtdemux, "failed to convert keyword to UTF-8");
    }
    offset += slen;
  }

done:
  if (k) {
    GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (k));
    gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, k, NULL);
  }
  g_free (k);

  return;

  /* ERRORS */
short_read:
  {
    GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP keywords");
    goto done;
  }
}

static void
qtdemux_tag_add_num (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag1, const char *tag2, GNode * node)
{
  GNode *data;
  int len;
  int type;
  int n1, n2;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    if (type == 0x00000000 && len >= 22) {
      n1 = QT_UINT16 ((guint8 *) data->data + 18);
      n2 = QT_UINT16 ((guint8 *) data->data + 20);
      if (n1 > 0) {
        GST_DEBUG_OBJECT (qtdemux, "adding tag %s=%d", tag1, n1);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, n1, NULL);
      }
      if (n2 > 0) {
        GST_DEBUG_OBJECT (qtdemux, "adding tag %s=%d", tag2, n2);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag2, n2, NULL);
      }
    }
  }
}

static void
qtdemux_tag_add_tmpo (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag1, const char *dummy, GNode * node)
{
  GNode *data;
  int len;
  int type;
  int n1;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    GST_DEBUG_OBJECT (qtdemux, "have tempo tag, type=%d,len=%d", type, len);
    /* some files wrongly have a type 0x0f=15, but it should be 0x15 */
    if ((type == 0x00000015 || type == 0x0000000f) && len >= 18) {
      n1 = QT_UINT16 ((guint8 *) data->data + 16);
      if (n1) {
        /* do not add bpm=0 */
        GST_DEBUG_OBJECT (qtdemux, "adding tag %d", n1);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, (gdouble) n1,
            NULL);
      }
    }
  }
}

static void
qtdemux_tag_add_uint32 (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag1, const char *dummy, GNode * node)
{
  GNode *data;
  int len;
  int type;
  guint32 num;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    GST_DEBUG_OBJECT (qtdemux, "have %s tag, type=%d,len=%d", tag1, type, len);
    /* some files wrongly have a type 0x0f=15, but it should be 0x15 */
    if ((type == 0x00000015 || type == 0x0000000f) && len >= 20) {
      num = QT_UINT32 ((guint8 *) data->data + 16);
      if (num) {
        /* do not add num=0 */
        GST_DEBUG_OBJECT (qtdemux, "adding tag %d", num);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, num, NULL);
      }
    }
  }
}

static void
qtdemux_tag_add_covr (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag1, const char *dummy, GNode * node)
{
  GNode *data;
  int len;
  int type;
  GstSample *sample;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    GST_DEBUG_OBJECT (qtdemux, "have covr tag, type=%d,len=%d", type, len);
    if ((type == 0x0000000d || type == 0x0000000e) && len > 16) {
      GstTagImageType image_type;

      if (gst_tag_list_get_tag_size (taglist, GST_TAG_IMAGE) == 0)
        image_type = GST_TAG_IMAGE_TYPE_FRONT_COVER;
      else
        image_type = GST_TAG_IMAGE_TYPE_NONE;

      if ((sample =
              gst_tag_image_data_to_image_sample ((guint8 *) data->data + 16,
                  len - 16, image_type))) {
        GST_DEBUG_OBJECT (qtdemux, "adding tag size %d", len - 16);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, sample, NULL);
        gst_sample_unref (sample);
      }
    }
  }
}

static void
qtdemux_tag_add_date (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  GNode *data;
  GstDateTime *datetime = NULL;
  char *s;
  int len;
  int type;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (data) {
    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    if (type == 0x00000001 && len > 16) {
      guint y, m = 1, d = 1;
      gint ret;

      s = g_strndup ((char *) data->data + 16, len - 16);
      GST_DEBUG_OBJECT (qtdemux, "adding date '%s'", s);
      datetime = gst_date_time_new_from_iso8601_string (s);
      if (datetime != NULL) {
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_DATE_TIME,
            datetime, NULL);
        gst_date_time_unref (datetime);
      }

      ret = sscanf (s, "%u-%u-%u", &y, &m, &d);
      if (ret >= 1 && y > 1500 && y < 3000) {
        GDate *date;

        date = g_date_new_dmy (d, m, y);
        gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, date, NULL);
        g_date_free (date);
      } else {
        GST_DEBUG_OBJECT (qtdemux, "could not parse date string '%s'", s);
      }
      g_free (s);
    }
  }
}

static void
qtdemux_tag_add_gnre (GstQTDemux * qtdemux, GstTagList * taglist,
    const char *tag, const char *dummy, GNode * node)
{
  GNode *data;

  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);

  /* re-route to normal string tag if major brand says so
   * or no data atom and compatible brand suggests so */
  if (qtdemux_is_brand_3gp (qtdemux, TRUE) ||
      (qtdemux_is_brand_3gp (qtdemux, FALSE) && !data)) {
    qtdemux_tag_add_str (qtdemux, taglist, tag, dummy, node);
    return;
  }

  if (data) {
    guint len, type, n;

    len = QT_UINT32 (data->data);
    type = QT_UINT32 ((guint8 *) data->data + 8);
    if (type == 0x00000000 && len >= 18) {
      n = QT_UINT16 ((guint8 *) data->data + 16);
      if (n > 0) {
        const gchar *genre;

        genre = gst_tag_id3_genre_get (n - 1);
        if (genre != NULL) {
          GST_DEBUG_OBJECT (qtdemux, "adding %d [%s]", n, genre);
          gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, genre, NULL);
        }
      }
    }
  }
}

static void
qtdemux_add_double_tag_from_str (GstQTDemux * demux, GstTagList * taglist,
    const gchar * tag, guint8 * data, guint32 datasize)
{
  gdouble value;
  gchar *datacopy;

  /* make a copy to have \0 at the end */
  datacopy = g_strndup ((gchar *) data, datasize);

  /* convert the str to double */
  if (sscanf (datacopy, "%lf", &value) == 1) {
    GST_DEBUG_OBJECT (demux, "adding tag: %s [%s]", tag, datacopy);
    gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, value, NULL);
  } else {
    GST_WARNING_OBJECT (demux, "Failed to parse double from string: %s",
        datacopy);
  }
  g_free (datacopy);
}


static void
qtdemux_tag_add_revdns (GstQTDemux * demux, GstTagList * taglist,
    const char *tag, const char *tag_bis, GNode * node)
{
  GNode *mean;
  GNode *name;
  GNode *data;
  guint32 meansize;
  guint32 namesize;
  guint32 datatype;
  guint32 datasize;
  const gchar *meanstr;
  const gchar *namestr;

  /* checking the whole ---- atom size for consistency */
  if (QT_UINT32 (node->data) <= 4 + 12 + 12 + 16) {
    GST_WARNING_OBJECT (demux, "Tag ---- atom is too small, ignoring");
    return;
  }

  mean = qtdemux_tree_get_child_by_type (node, FOURCC_mean);
  if (!mean) {
    GST_WARNING_OBJECT (demux, "No 'mean' atom found");
    return;
  }

  meansize = QT_UINT32 (mean->data);
  if (meansize <= 12) {
    GST_WARNING_OBJECT (demux, "Small mean atom, ignoring the whole tag");
    return;
  }
  meanstr = ((gchar *) mean->data) + 12;
  meansize -= 12;

  name = qtdemux_tree_get_child_by_type (node, FOURCC_name);
  if (!name) {
    GST_WARNING_OBJECT (demux, "'name' atom not found, ignoring tag");
    return;
  }

  namesize = QT_UINT32 (name->data);
  if (namesize <= 12) {
    GST_WARNING_OBJECT (demux, "'name' atom is too small, ignoring tag");
    return;
  }
  namestr = ((gchar *) name->data) + 12;
  namesize -= 12;

  /*
   * Data atom is:
   * uint32 - size
   * uint32 - name
   * uint8  - version
   * uint24 - data type
   * uint32 - all 0
   * rest   - the data
   */
  data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
  if (!data) {
    GST_WARNING_OBJECT (demux, "No data atom in this tag");
    return;
  }
  datasize = QT_UINT32 (data->data);
  if (datasize <= 16) {
    GST_WARNING_OBJECT (demux, "Data atom too small");
    return;
  }
  datatype = QT_UINT32 (((gchar *) data->data) + 8) & 0xFFFFFF;

  if ((strncmp (meanstr, "com.apple.iTunes", meansize) == 0) ||
      (strncmp (meanstr, "org.hydrogenaudio.replaygain", meansize) == 0)) {
    static const struct
    {
      const gchar name[28];
      const gchar tag[28];
    } tags[] = {
      {
          "replaygain_track_gain", GST_TAG_TRACK_GAIN}, {
          "replaygain_track_peak", GST_TAG_TRACK_PEAK}, {
          "replaygain_album_gain", GST_TAG_ALBUM_GAIN}, {
          "replaygain_album_peak", GST_TAG_ALBUM_PEAK}, {
          "MusicBrainz Track Id", GST_TAG_MUSICBRAINZ_TRACKID}, {
          "MusicBrainz Artist Id", GST_TAG_MUSICBRAINZ_ARTISTID}, {
          "MusicBrainz Album Id", GST_TAG_MUSICBRAINZ_ALBUMID}, {
          "MusicBrainz Album Artist Id", GST_TAG_MUSICBRAINZ_ALBUMARTISTID}
    };
    int i;

    for (i = 0; i < G_N_ELEMENTS (tags); ++i) {
      if (!g_ascii_strncasecmp (tags[i].name, namestr, namesize)) {
        switch (gst_tag_get_type (tags[i].tag)) {
          case G_TYPE_DOUBLE:
            qtdemux_add_double_tag_from_str (demux, taglist, tags[i].tag,
                ((guint8 *) data->data) + 16, datasize - 16);
            break;
          case G_TYPE_STRING:
            qtdemux_tag_add_str (demux, taglist, tags[i].tag, NULL, node);
            break;
          default:
            /* not reached */
            break;
        }
        break;
      }
    }

    /* Some tags might not actually be used for metadata about the media,
     * but for other purposes. One such tag is iTunSMPB, which contains
     * padding information for gapless playback. Scan these separately. */
    if (i == G_N_ELEMENTS (tags)) {
      if (!g_ascii_strncasecmp ("iTunSMPB", namestr, 8)) {
        /* iTunSMPB tag format goes as follows:
         *
         * " 00000000 xxxxxxxx yyyyyyyy zzzzzzzzzzzzzzzz 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000"
         *
         * The data is actually an ASCII string containing these hex fields.
         * The description above is _not_ a description of a binary format!
         * These need to be parsed with g_ascii_strtoull() and base 16.
         *
         * (The quotes are not part of it; they just emphasize the
         * whitespace at the beginning of the string).
         *
         * Only the fields marked with x/y/z are of interest here.
         *
         * The x field is the priming, in samples.
         * These are the padding samples at the beginning of the stream.
         *
         * The y field is the remainder, in samples.
         * These are the padding samples at the end of the stream.
         *
         * The z field is the number of valid PCM frames, excluding the
         * priming and remainder. (In other words, the number of PCM
         * frames that make up the actual audio, without the padding.)
         *
         * The data starts at offset 16. All access to it must therefore skip
         * the first 16 bytes.
         */

        const gsize start_offset = 16;
        const gsize priming_offset = start_offset + 10;
        const gsize remainder_offset = start_offset + 19;
        const gsize num_valid_pcm_frames_offset = start_offset + 28;
        const gsize total_length = 44;
        const gchar *str;
        guint64 priming;
        guint64 remainder;
        guint64 num_valid_pcm_frames;
        /* Temporary buffer for g_ascii_strtoull() calls.
         * Add extra +1 space for nullbyte. */
        gchar tmp[16 + 1];

        /* Use the iTunSMPB info if no other info has been found yet. */
        if (demux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) {
          GST_DEBUG_OBJECT (demux, "iTunSMPB information found, "
              "but other gapless audio info was already read");
          goto finish;
        }

        if (G_UNLIKELY (datasize < (start_offset + total_length))) {
          GST_WARNING_OBJECT (demux,
              "iTunSMPB tag data size too small - not parsing");
          goto finish;
        }

        str = (gchar *) ((guint8 *) data->data);

#define PARSE_ITUNSMPB_FIELD(FIELD_NAME, NUM_DIGITS) \
        G_STMT_START \
        { \
          gint str_idx; \
\
          for (str_idx = 0; str_idx < (NUM_DIGITS); ++str_idx) { \
            gchar ch = str[FIELD_NAME ## _offset + str_idx]; \
            if (!g_ascii_isxdigit (ch)) { \
              GST_WARNING_OBJECT (demux, #FIELD_NAME " field in iTunSMPB " \
                  "tag data has invalid character '%c'", ch); \
              goto finish; \
            } \
            tmp[str_idx] = ch; \
          } \
          tmp[NUM_DIGITS] = 0; \
\
          FIELD_NAME = g_ascii_strtoull (tmp, NULL, 16); \
        } \
        G_STMT_END

        PARSE_ITUNSMPB_FIELD (priming, 8);
        PARSE_ITUNSMPB_FIELD (remainder, 8);
        PARSE_ITUNSMPB_FIELD (num_valid_pcm_frames, 16);

#undef PARSE_ITUNSMPB_FIELD

        GST_DEBUG_OBJECT (demux, "iTunSMPB information: priming %"
            G_GUINT64_FORMAT " remainder %" G_GUINT64_FORMAT
            " num valid PCM frames %" G_GUINT64_FORMAT, priming, remainder,
            num_valid_pcm_frames);

        demux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_ITUNES;
        demux->gapless_audio_info.num_start_padding_pcm_frames = priming;
        demux->gapless_audio_info.num_end_padding_pcm_frames = remainder;
        demux->gapless_audio_info.num_valid_pcm_frames = num_valid_pcm_frames;
      } else {
        goto unknown_tag;
      }
    }
  } else {
    goto unknown_tag;
  }

finish:
  return;

/* errors */
unknown_tag:
#ifndef GST_DISABLE_GST_DEBUG
  {
    gchar *namestr_dbg;
    gchar *meanstr_dbg;

    meanstr_dbg = g_strndup (meanstr, meansize);
    namestr_dbg = g_strndup (namestr, namesize);

    GST_WARNING_OBJECT (demux, "This tag %s:%s type:%u is not mapped, "
        "file a bug at %s", meanstr_dbg, namestr_dbg, datatype,
        PACKAGE_BUGREPORT);

    g_free (namestr_dbg);
    g_free (meanstr_dbg);
  }
#endif
  return;
}

static void
qtdemux_tag_add_id32 (GstQTDemux * demux, GstTagList * taglist, const char *tag,
    const char *tag_bis, GNode * node)
{
  guint8 *data;
  GstBuffer *buf;
  guint len;
  GstTagList *id32_taglist = NULL;

  GST_LOG_OBJECT (demux, "parsing ID32");

  data = node->data;
  len = GST_READ_UINT32_BE (data);

  /* need at least full box and language tag */
  if (len < 12 + 2)
    return;

  buf = gst_buffer_new_allocate (NULL, len - 14, NULL);
  gst_buffer_fill (buf, 0, data + 14, len - 14);

  id32_taglist = gst_tag_list_from_id3v2_tag (buf);
  if (id32_taglist) {
    GST_LOG_OBJECT (demux, "parsing ok");
    gst_tag_list_insert (taglist, id32_taglist, GST_TAG_MERGE_KEEP);
    gst_tag_list_unref (id32_taglist);
  } else {
    GST_LOG_OBJECT (demux, "parsing failed");
  }

  gst_buffer_unref (buf);
}

typedef void (*GstQTDemuxAddTagFunc) (GstQTDemux * demux, GstTagList * taglist,
    const char *tag, const char *tag_bis, GNode * node);

/* unmapped tags
FOURCC_pcst -> if media is a podcast -> bool
FOURCC_cpil -> if media is part of a compilation -> bool
FOURCC_pgap -> if media is part of a gapless context -> bool
FOURCC_tven -> the tv episode id e.g. S01E23 -> str
*/

static const struct
{
  guint32 fourcc;
  const gchar *gst_tag;
  const gchar *gst_tag_bis;
  const GstQTDemuxAddTagFunc func;
} add_funcs[] = {
  {
      FOURCC__nam, GST_TAG_TITLE, NULL, qtdemux_tag_add_str}, {
      FOURCC_titl, GST_TAG_TITLE, NULL, qtdemux_tag_add_str}, {
      FOURCC__grp, GST_TAG_GROUPING, NULL, qtdemux_tag_add_str}, {
      FOURCC__wrt, GST_TAG_COMPOSER, NULL, qtdemux_tag_add_str}, {
      FOURCC__ART, GST_TAG_ARTIST, NULL, qtdemux_tag_add_str}, {
      FOURCC_aART, GST_TAG_ALBUM_ARTIST, NULL, qtdemux_tag_add_str}, {
      FOURCC_perf, GST_TAG_ARTIST, NULL, qtdemux_tag_add_str}, {
      FOURCC_auth, GST_TAG_COMPOSER, NULL, qtdemux_tag_add_str}, {
      FOURCC__alb, GST_TAG_ALBUM, NULL, qtdemux_tag_add_str}, {
      FOURCC_albm, GST_TAG_ALBUM, NULL, qtdemux_tag_add_str}, {
      FOURCC_cprt, GST_TAG_COPYRIGHT, NULL, qtdemux_tag_add_str}, {
      FOURCC__cpy, GST_TAG_COPYRIGHT, NULL, qtdemux_tag_add_str}, {
      FOURCC__cmt, GST_TAG_COMMENT, NULL, qtdemux_tag_add_str}, {
      FOURCC__des, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
      FOURCC_desc, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
      FOURCC_dscp, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
      FOURCC__lyr, GST_TAG_LYRICS, NULL, qtdemux_tag_add_str}, {
      FOURCC__day, GST_TAG_DATE, NULL, qtdemux_tag_add_date}, {
      FOURCC_yrrc, GST_TAG_DATE, NULL, qtdemux_tag_add_year}, {
      FOURCC__too, GST_TAG_ENCODER, NULL, qtdemux_tag_add_str}, {
      FOURCC__inf, GST_TAG_COMMENT, NULL, qtdemux_tag_add_str}, {
        FOURCC_trkn, GST_TAG_TRACK_NUMBER, GST_TAG_TRACK_COUNT,
      qtdemux_tag_add_num}, {
        FOURCC_disk, GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT,
      qtdemux_tag_add_num}, {
        FOURCC_disc, GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT,
      qtdemux_tag_add_num}, {
      FOURCC__gen, GST_TAG_GENRE, NULL, qtdemux_tag_add_str}, {
      FOURCC_gnre, GST_TAG_GENRE, NULL, qtdemux_tag_add_gnre}, {
      FOURCC_tmpo, GST_TAG_BEATS_PER_MINUTE, NULL, qtdemux_tag_add_tmpo}, {
      FOURCC_covr, GST_TAG_IMAGE, NULL, qtdemux_tag_add_covr}, {
      FOURCC_sonm, GST_TAG_TITLE_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_soal, GST_TAG_ALBUM_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_soar, GST_TAG_ARTIST_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_soaa, GST_TAG_ALBUM_ARTIST_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_soco, GST_TAG_COMPOSER_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_sosn, GST_TAG_SHOW_SORTNAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_tvsh, GST_TAG_SHOW_NAME, NULL, qtdemux_tag_add_str}, {
      FOURCC_tvsn, GST_TAG_SHOW_SEASON_NUMBER, NULL, qtdemux_tag_add_uint32}, {
      FOURCC_tves, GST_TAG_SHOW_EPISODE_NUMBER, NULL, qtdemux_tag_add_uint32}, {
      FOURCC_kywd, GST_TAG_KEYWORDS, NULL, qtdemux_tag_add_keywords}, {
      FOURCC_keyw, GST_TAG_KEYWORDS, NULL, qtdemux_tag_add_str}, {
      FOURCC__enc, GST_TAG_ENCODER, NULL, qtdemux_tag_add_str}, {
      FOURCC_loci, GST_TAG_GEO_LOCATION_NAME, NULL, qtdemux_tag_add_location}, {
        FOURCC_clsf, GST_QT_DEMUX_CLASSIFICATION_TAG, NULL,
      qtdemux_tag_add_classification}, {
      FOURCC__mak, GST_TAG_DEVICE_MANUFACTURER, NULL, qtdemux_tag_add_str}, {
      FOURCC__mod, GST_TAG_DEVICE_MODEL, NULL, qtdemux_tag_add_str}, {
      FOURCC__swr, GST_TAG_APPLICATION_NAME, NULL, qtdemux_tag_add_str}, {

        /* This is a special case, some tags are stored in this
         * 'reverse dns naming', according to:
         * http://atomicparsley.sourceforge.net/mpeg-4files.html and
         * bug #614471
         */
      FOURCC_____, "", NULL, qtdemux_tag_add_revdns}, {
        /* see http://www.mp4ra.org/specs.html for ID32 in meta box */
      FOURCC_ID32, "", NULL, qtdemux_tag_add_id32}
};

struct _GstQtDemuxTagList
{
  GstQTDemux *demux;
  GstTagList *taglist;
};
typedef struct _GstQtDemuxTagList GstQtDemuxTagList;

static void
qtdemux_tag_add_blob (GNode * node, GstQtDemuxTagList * qtdemuxtaglist)
{
  gint len;
  guint8 *data;
  GstBuffer *buf;
  gchar *media_type;
  const gchar *style;
  GstSample *sample;
  GstStructure *s;
  guint i;
  guint8 ndata[4];
  GstQTDemux *demux = qtdemuxtaglist->demux;
  GstTagList *taglist = qtdemuxtaglist->taglist;

  data = node->data;
  len = QT_UINT32 (data);
  buf = gst_buffer_new_and_alloc (len);
  gst_buffer_fill (buf, 0, data, len);

  /* heuristic to determine style of tag */
  if (QT_FOURCC (data + 4) == FOURCC_____ ||
      (len > 8 + 12 && QT_FOURCC (data + 12) == FOURCC_data))
    style = "itunes";
  else if (demux->major_brand == FOURCC_qt__)
    style = "quicktime";
  /* fall back to assuming iso/3gp tag style */
  else
    style = "iso";

  /* sanitize the name for the caps. */
  for (i = 0; i < 4; i++) {
    guint8 d = data[4 + i];
    if (g_ascii_isalnum (d))
      ndata[i] = g_ascii_tolower (d);
    else
      ndata[i] = '_';
  }

  media_type = g_strdup_printf ("application/x-gst-qt-%c%c%c%c-tag",
      ndata[0], ndata[1], ndata[2], ndata[3]);
  GST_DEBUG_OBJECT (demux, "media type %s", media_type);

  s = gst_structure_new (media_type, "style", G_TYPE_STRING, style, NULL);
  sample = gst_sample_new (buf, NULL, NULL, s);
  gst_buffer_unref (buf);
  g_free (media_type);

  GST_DEBUG_OBJECT (demux, "adding private tag; size %d, info %" GST_PTR_FORMAT,
      len, s);

  gst_tag_list_add (taglist, GST_TAG_MERGE_APPEND,
      GST_QT_DEMUX_PRIVATE_TAG, sample, NULL);

  gst_sample_unref (sample);
}

void
qtdemux_parse_udta (GstQTDemux * qtdemux, GstTagList * taglist, GNode * udta)
{
  GNode *meta;
  GNode *ilst;
  GNode *xmp_;
  GNode *node;
  gint i;
  GstQtDemuxTagList demuxtaglist;

  demuxtaglist.demux = qtdemux;
  demuxtaglist.taglist = taglist;

  meta = qtdemux_tree_get_child_by_type (udta, FOURCC_meta);
  if (meta != NULL) {
    ilst = qtdemux_tree_get_child_by_type (meta, FOURCC_ilst);
    if (ilst == NULL) {
      GST_LOG_OBJECT (qtdemux, "no ilst");
      return;
    }
  } else {
    ilst = udta;
    GST_LOG_OBJECT (qtdemux, "no meta so using udta itself");
  }

  i = 0;
  while (i < G_N_ELEMENTS (add_funcs)) {
    node = qtdemux_tree_get_child_by_type (ilst, add_funcs[i].fourcc);
    if (node) {
      gint len;

      len = QT_UINT32 (node->data);
      if (len < 12) {
        GST_DEBUG_OBJECT (qtdemux, "too small tag atom %" GST_FOURCC_FORMAT,
            GST_FOURCC_ARGS (add_funcs[i].fourcc));
      } else {
        add_funcs[i].func (qtdemux, taglist, add_funcs[i].gst_tag,
            add_funcs[i].gst_tag_bis, node);
      }
      g_node_destroy (node);
    } else {
      i++;
    }
  }

  /* parsed nodes have been removed, pass along remainder as blob */
  g_node_children_foreach (ilst, G_TRAVERSE_ALL,
      (GNodeForeachFunc) qtdemux_tag_add_blob, &demuxtaglist);

  /* parse up XMP_ node if existing */
  xmp_ = qtdemux_tree_get_child_by_type (udta, FOURCC_XMP_);
  if (xmp_ != NULL) {
    GstBuffer *buf;
    GstTagList *xmptaglist;

    buf = _gst_buffer_new_wrapped (((guint8 *) xmp_->data) + 8,
        QT_UINT32 ((guint8 *) xmp_->data) - 8, NULL);
    xmptaglist = gst_tag_list_from_xmp_buffer (buf);
    gst_buffer_unref (buf);

    qtdemux_handle_xmp_taglist (qtdemux, taglist, xmptaglist);
  } else {
    GST_DEBUG_OBJECT (qtdemux, "No XMP_ node found");
  }
}

void
qtdemux_handle_xmp_taglist (GstQTDemux * qtdemux, GstTagList * taglist,
    GstTagList * xmptaglist)
{
  /* Strip out bogus fields */
  if (xmptaglist) {
    if (gst_tag_list_get_scope (taglist) == GST_TAG_SCOPE_GLOBAL) {
      gst_tag_list_remove_tag (xmptaglist, GST_TAG_VIDEO_CODEC);
      gst_tag_list_remove_tag (xmptaglist, GST_TAG_AUDIO_CODEC);
    } else {
      gst_tag_list_remove_tag (xmptaglist, GST_TAG_CONTAINER_FORMAT);
    }

    GST_DEBUG_OBJECT (qtdemux, "Found XMP tags %" GST_PTR_FORMAT, xmptaglist);

    /* prioritize native tags using _KEEP mode */
    gst_tag_list_insert (taglist, xmptaglist, GST_TAG_MERGE_KEEP);
    gst_tag_list_unref (xmptaglist);
  }
}
