/*
 * This file is part of GNAC - Gnome Audio Converter
 *
 * Copyright (C) 2007 - 2012 Gnac
 *    
 *    - DUPASQUIER  Benoit    <bdupasqu@src.gnome.org>
 *    - JOAQUIM     David     <djoaquim@src.gnome.org>
 *    - ROUX        Alexandre <alexroux@src.gnome.org>
 *
 * GNAC is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * GNAC 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GNAC; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, 
 * Boston, MA  02110-1301  USA
 */

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

#include <gdk/gdk.h>
#include <glib/gi18n.h>
#include <gst/gst.h>
#include <gst/pbutils/pbutils.h>
#include <gst/tag/tag.h>
#include <stdarg.h>

#include "libgnac-error.h"
#include "libgnac-gst-utils.h"
#include "libgnac-metadata.h"


struct LibgnacMetadataPrivate
{
  GHashTable        *tags_table;
  GstDiscovererInfo *info;
  LibgnacTags       *metadata;
};

G_DEFINE_TYPE(LibgnacMetadata, libgnac_metadata, G_TYPE_OBJECT);

#define LIBGNAC_METADATA_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), \
    LIBGNAC_TYPE_METADATA, LibgnacMetadataPrivate))


static void
libgnac_metadata_reset(LibgnacMetadata *md)
{
  /* must be freed by the user */
  if (md->priv->metadata) {
    md->priv->metadata = NULL;
  }
}


static void
libgnac_metadata_dispose(GObject *object)
{
  LibgnacMetadata *self = LIBGNAC_METADATA(object);

  if (self->priv->tags_table) {
    g_hash_table_destroy(self->priv->tags_table);
    self->priv->tags_table = NULL;
  }

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


static void
libgnac_metadata_finalize(GObject *object)
{
  LibgnacMetadata *md = LIBGNAC_METADATA(object);

  libgnac_metadata_reset(md);
  
  G_OBJECT_CLASS(libgnac_metadata_parent_class)->finalize(object);
}


static void
libgnac_metadata_class_init(LibgnacMetadataClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS(klass);

  g_type_class_add_private(klass, sizeof(LibgnacMetadataPrivate));

  object_class->finalize = libgnac_metadata_finalize;
  object_class->dispose = libgnac_metadata_dispose;
}


static void
libgnac_metadata_init(LibgnacMetadata *md)
{
  LibgnacMetadataPrivate *priv = md->priv = LIBGNAC_METADATA_GET_PRIVATE(md);

  priv->tags_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) libgnac_metadata_tags_free);
}


LibgnacMetadata *
libgnac_metadata_new(void)
{
  return LIBGNAC_METADATA(g_object_new(LIBGNAC_TYPE_METADATA, NULL, NULL));
}


static void
libgnac_metadata_free_gvalue(GValue *value) 
{
  if (!value) return;

  g_value_unset(value);
  g_free(value);
}


static void
libgnac_metadata_add_boolean_value(LibgnacMetadata *md,
                                   gchar           *tag_name,
                                   gboolean         value)
{
  GValue *val = g_new0(GValue, 1);
  g_value_init(val, G_TYPE_BOOLEAN);
  g_value_set_boolean(val, value);
  g_hash_table_insert(md->priv->metadata, tag_name, val);
}


static void
libgnac_metadata_add_fraction_value(LibgnacMetadata *md,
                                    gchar           *tag_name,
                                    guint            numerator,
                                    guint            denominator)
{
  if (numerator == 0 || denominator == 0) return;

  GValue *value = g_new0(GValue, 1);
  g_value_init(value, G_TYPE_FLOAT);
  g_value_set_float(value, (gfloat) numerator / denominator);
  g_hash_table_insert(md->priv->metadata, tag_name, value);
}


static void
libgnac_metadata_add_object_value(LibgnacMetadata *md,
                                  gchar           *tag_name,
                                  GObject         *value)
{
  if (!value) return;

  GValue *val = g_new0(GValue, 1);
  g_value_init(val, G_TYPE_OBJECT);
  g_value_set_object(val, value);
  g_hash_table_insert(md->priv->metadata, tag_name, val);
}


static void
libgnac_metadata_add_string_value(LibgnacMetadata *md,
                                  gchar           *tag_name,
                                  const gchar     *value)
{
  if (!value) return;

  GValue *val = g_new0(GValue, 1);
  g_value_init(val, G_TYPE_STRING);
  g_value_set_string(val, value);
  g_hash_table_insert(md->priv->metadata, tag_name, val);
}


static void
libgnac_metadata_add_uint_value(LibgnacMetadata *md,
                                gchar           *tag_name,
                                guint            value)
{
  if (value == 0) return;

  GValue *val = g_new0(GValue, 1);
  g_value_init(val, G_TYPE_UINT);
  g_value_set_uint(val, value);
  g_hash_table_insert(md->priv->metadata, tag_name, val);
}


static void
libgnac_metadata_process_tag_image(const GstTagList *taglist, 
                                   const gchar      *tag_name,
                                   gint              count,
                                   LibgnacMetadata  *md)
{
  gint i;
  for (i = 0; i < count; i++) {
    const GValue *val = gst_tag_list_get_value_index(taglist, tag_name, i);
      
    GstBuffer *buffer = gst_value_get_buffer(val);
    g_return_if_fail(buffer);
      
    GstSample *sample = gst_value_get_sample(val);
    g_return_if_fail(sample);

    gint image_type = GST_TAG_IMAGE_TYPE_NONE;

    /* GST_TAG_PREVIEW_IMAGE does not have an "image-type" field */
    if (g_str_equal(tag_name, GST_TAG_IMAGE)) {
      GstCaps *caps = gst_sample_get_caps(sample);
      g_return_if_fail(caps);

      GstStructure *structure = gst_caps_get_structure(caps, 0);
      g_return_if_fail(structure);

      gst_structure_get_enum(structure, "image-type",
          GST_TYPE_TAG_IMAGE_TYPE, &image_type);
    }

    /* we are only interested in the front cover (but if there is only
     * one image that has an undefined type, we display it anyway) */
    if (g_str_equal(tag_name, GST_TAG_PREVIEW_IMAGE) ||
          (image_type == GST_TAG_IMAGE_TYPE_FRONT_COVER) ||
          (image_type == GST_TAG_IMAGE_TYPE_UNDEFINED && count == 1))
    {
      GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
      GError *error = NULL;
      GstMapInfo *info = NULL;
      gst_buffer_map(buffer, info, GST_MAP_READ);
      guint8 *data = info->data;
      guint size = info->size;
      gst_buffer_unmap(buffer, info);

      if (!gdk_pixbuf_loader_write(loader, data, size, &error)) {
        g_debug("Error writing data to pixbuf: %s", error->message);
        gdk_pixbuf_loader_close(loader, NULL);
        g_object_unref(loader);
        g_clear_error(&error);
        return;
      }

      if (!gdk_pixbuf_loader_close(loader, &error)) {
        g_debug("Error closing pixbuf loader: %s", error->message);
        g_object_unref(loader);
        g_clear_error(&error);
        return;
      }

      GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
      libgnac_metadata_add_object_value(md, GST_TAG_IMAGE, G_OBJECT(pixbuf));

      g_object_unref(loader);
    }
  }
}


static void
libgnac_metadata_process_tag(const GstTagList *taglist,
                             gchar            *tag_name,
                             LibgnacMetadata  *md)
{
  gint count = gst_tag_list_get_tag_size(taglist, tag_name);
  if (count < 1) return;

  if (g_str_equal(tag_name, GST_TAG_IMAGE) ||
      g_str_equal(tag_name, GST_TAG_PREVIEW_IMAGE)) /* used by oga files */
  {
    libgnac_metadata_process_tag_image(taglist, tag_name, count, md);
    return;
  }

  const GValue *val = gst_tag_list_get_value_index(taglist, tag_name, 0);

  GType type = gst_tag_get_type(tag_name);
  GValue *newval = g_new0(GValue, 1);
  g_value_init(newval, type);
  if (!g_value_transform(val, newval)) {
    libgnac_metadata_free_gvalue(newval);
    return;
  }

  if (type == G_TYPE_STRING) {
    gchar *str = g_value_dup_string(newval);
    if (!g_utf8_validate(str, -1, NULL)) {
      g_info("%s: %s", _("Invalid UTF-8 tag"), tag_name);
      g_free(str);
      libgnac_metadata_free_gvalue(newval);
      return;
    }
    str = g_strstrip(str);
    g_value_take_string(newval, str);
  }

  if (g_str_equal(tag_name, GST_TAG_DATE))
  {
    g_return_if_fail(G_VALUE_HOLDS(newval,G_TYPE_DATE));
    const GDate *date = g_value_get_boxed(newval);
    if (!date) return;
    GDateYear year = g_date_get_year(date);
    g_value_unset(newval);
    g_value_init(newval, G_TYPE_UINT);
    g_value_set_uint(newval, year);
  }
  else if (g_str_equal(tag_name, GST_TAG_DURATION))
  {
    guint64 duration = g_value_get_uint64(newval);
    /* convert the duration from ns to seconds */
    g_value_set_uint64(newval, duration / GST_SECOND);
  }

  g_hash_table_insert(md->priv->metadata, tag_name, newval);
}


static void
libgnac_metadata_get_file_info(GFile *uri, LibgnacMetadata *md)
{
  GError *error = NULL;

  GFileInfo *info = g_file_query_info(uri,
      G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
      G_FILE_ATTRIBUTE_STANDARD_ICON ","
      G_FILE_ATTRIBUTE_STANDARD_SIZE ","
      G_FILE_ATTRIBUTE_THUMBNAIL_PATH,
      G_FILE_QUERY_INFO_NONE, NULL, &error);

  if (error) {
    g_clear_error(&error);
    return;
  }

  /* file size */
  GValue *file_size_value = g_new0(GValue, 1);
  g_value_init(file_size_value, G_TYPE_INT64);
  g_value_set_int64(file_size_value, g_file_info_get_size(info));
  g_hash_table_insert(md->priv->metadata, GNAC_TAG_FILE_SIZE, file_size_value);

  /* location */
  gchar *path = g_file_get_path(uri);
  libgnac_metadata_add_string_value(md, GST_TAG_LOCATION, path);
  g_free(path);

  /* filename */
  const gchar *filename = g_file_info_get_display_name(info);
  libgnac_metadata_add_string_value(md, GNAC_TAG_FILENAME, filename);

  /* image */
  const gchar *thumbnail_path = g_file_info_get_attribute_byte_string(info,
      G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
  if (thumbnail_path) {
    GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(thumbnail_path, NULL);
    libgnac_metadata_add_object_value(md, GST_TAG_IMAGE, G_OBJECT(pixbuf));
    g_object_unref(pixbuf);
  }

  g_object_unref(info);
}


static void
libgnac_metadata_process_audio_info(LibgnacMetadata *md)
{
  GList *audio_info = gst_discoverer_info_get_audio_streams(md->priv->info);
  if (!audio_info) return;

  libgnac_metadata_add_boolean_value(md, GNAC_TAG_HAS_AUDIO, TRUE);

  GList *first = g_list_first(audio_info);
  GstDiscovererAudioInfo *audio = (GstDiscovererAudioInfo *) first->data;

  guint channels = gst_discoverer_audio_info_get_channels(audio);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_CHANNELS, channels);

  guint sample_rate = gst_discoverer_audio_info_get_sample_rate(audio);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_RATE, sample_rate);

  guint depth = gst_discoverer_audio_info_get_depth(audio);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_AUDIO_DEPTH, depth);

  //guint bitrate = gst_discoverer_audio_info_get_bitrate(audio);
  //if (gst_discoverer_audio_info_is_vbr(audio)) { // XXX not implemented yet
  //  libgnac_metadata_add_uint_value(md, GNAC_TAG_VBR, bitrate);
  //}

  gst_discoverer_stream_info_list_free(audio_info);
}


static void
libgnac_metadata_process_video_info(LibgnacMetadata *md)
{
  GList *video_info = gst_discoverer_info_get_video_streams(md->priv->info);
  if (!video_info) return;

  libgnac_metadata_add_boolean_value(md, GNAC_TAG_HAS_VIDEO, TRUE);

  GList *first = g_list_first(video_info);
  GstDiscovererVideoInfo *video = (GstDiscovererVideoInfo *) first->data;

  guint height = gst_discoverer_video_info_get_height(video);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_HEIGHT, height);

  guint width = gst_discoverer_video_info_get_width(video);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_WIDTH, width);

  guint depth = gst_discoverer_video_info_get_depth(video);
  libgnac_metadata_add_uint_value(md, GNAC_TAG_VIDEO_DEPTH, depth);

  guint rate_num = gst_discoverer_video_info_get_framerate_num(video);
  guint rate_denom = gst_discoverer_video_info_get_framerate_denom(video);
  libgnac_metadata_add_fraction_value(md, GNAC_TAG_FRAMERATE,
      rate_num, rate_denom);

  if (gst_discoverer_video_info_is_interlaced(video)) {
    libgnac_metadata_add_boolean_value(md, GNAC_TAG_INTERLACED, TRUE);
  }

  guint par_num = gst_discoverer_video_info_get_par_num(video);
  guint par_denom = gst_discoverer_video_info_get_par_denom(video);
  libgnac_metadata_add_fraction_value(md, GNAC_TAG_PAR, par_num, par_denom);

  gst_discoverer_stream_info_list_free(video_info);
}


gboolean
libgnac_metadata_remove(LibgnacMetadata  *md,
                        GFile            *uri)
{
  gchar *string_uri = g_file_get_uri(uri);
  gboolean ret = g_hash_table_remove(md->priv->tags_table, string_uri);
  g_free(string_uri);
  return ret;
}


void
libgnac_metadata_remove_all(LibgnacMetadata *md)
{
  g_hash_table_remove_all(md->priv->tags_table);
}


LibgnacTags *
libgnac_metadata_extract(LibgnacMetadata  *md,
                         GFile            *uri,
                         GError          **error)
{
  g_return_val_if_fail(LIBGNAC_IS_METADATA(md), NULL);
  g_return_val_if_fail(uri, NULL);

  gchar *string_uri = g_file_get_uri(uri);
  LibgnacTags *tags = g_hash_table_lookup(md->priv->tags_table, string_uri);

  if (!tags) {
    /* make sure we start with a clean base */
    libgnac_metadata_reset(md);

    md->priv->metadata = g_hash_table_new_full(g_str_hash, g_str_equal,
        NULL, (GDestroyNotify) libgnac_metadata_free_gvalue);

    /* store file-size, location and filename */
    libgnac_metadata_get_file_info(uri, md);

    GError *err = NULL;
    GstDiscoverer *discoverer = gst_discoverer_new(5 * GST_SECOND, &err);
    if (G_UNLIKELY(!discoverer)) {
      g_debug("Failed to create discoverer: %s", err->message);
      g_propagate_error(error, err);
      return NULL;
    }

    GstDiscovererInfo *info = gst_discoverer_discover_uri(discoverer,
        string_uri, &err);
    if (G_UNLIKELY(!info)) {
      g_debug("Failed to discover: %s", err->message);
      g_propagate_error(error, err);
      return NULL;
    }

    gst_object_unref(discoverer);

    md->priv->info = info;

    GstClockTime duration = gst_discoverer_info_get_duration(info);
    GValue *val = g_new0(GValue, 1);
    g_value_init(val, G_TYPE_UINT64);
    /* convert the duration from ns to seconds */
    g_value_set_uint64(val, duration / GST_SECOND);
    g_hash_table_insert(md->priv->metadata, GST_TAG_DURATION, val);

    const GstTagList *taglist = gst_discoverer_info_get_tags(info);
    if (taglist) {
      gst_tag_list_foreach(taglist,
          (GstTagForeachFunc) libgnac_metadata_process_tag, md);
    }

    libgnac_metadata_process_audio_info(md);
    libgnac_metadata_process_video_info(md);

    gst_discoverer_info_unref(info);

    g_hash_table_insert(md->priv->tags_table, g_strdup(string_uri),
        md->priv->metadata);
    tags = md->priv->metadata;
  }

  g_free(string_uri);

  return tags;
}


void
libgnac_metadata_tags_free(LibgnacTags *tags)
{
  if (!tags) return;

  g_hash_table_destroy((GHashTable *) tags);
}


gboolean
libgnac_metadata_tag_exists(LibgnacTags *tags,
                            const gchar *name)
{
  return libgnac_metadata_tags_exist(tags, name, NULL);
}


gboolean
libgnac_metadata_tags_exist(LibgnacTags *tags, ...)
{
  if (!tags) return FALSE;

  gboolean missing = FALSE;
  gchar *name;
  va_list names;

  va_start(names, tags);

  while ((name = va_arg(names, gchar *)) && !missing) {
    if (!g_hash_table_lookup(tags, name)) missing = TRUE;
  }

  va_end(names);

  return !missing;
}


LibgnacTags *
libgnac_metadata_get_dummy_tags(void)
{
  static LibgnacTags *dummy_tags = NULL;

  if (!dummy_tags) {
    dummy_tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
        (GDestroyNotify) libgnac_metadata_tags_free);

    gchar *string_tags[][2] = {
      { GST_TAG_ALBUM    , "Today!"                },
      { GST_TAG_ARTIST   , "The Beach Boys"        },
      /* Translators: example filename used in the preview label (do not remove the extension) */
      { GST_TAG_COMMENT  , N_("Converted by Gnac") },
      { GNAC_TAG_FILENAME, N_("filename.oga")      },
      { GST_TAG_GENRE    , "Rock"                  },
      { GST_TAG_TITLE    , "Do You Wanna Dance?"   },
      { NULL, NULL }
    };

    gint i;
    for (i = 0; string_tags[i][0]; i++) {
      GValue *value = g_new0(GValue, 1);
      g_value_init(value, G_TYPE_STRING);
      g_value_set_static_string(value, string_tags[i][1]);
      g_hash_table_insert(dummy_tags, string_tags[i][0], value);
    }

    GValue *value = g_new0(GValue, 1);
    g_value_init(value, G_TYPE_UINT);
    g_value_set_uint(value, 1965);
    g_hash_table_insert(dummy_tags, GST_TAG_DATE, value);

    GValue *track_count = g_new0(GValue, 1);
    g_value_init(track_count, G_TYPE_UINT);
    g_value_set_uint(track_count, 6);
    g_hash_table_insert(dummy_tags, GST_TAG_TRACK_COUNT, track_count);

    GValue *track_number = g_new0(GValue, 1);
    g_value_init(track_number, G_TYPE_UINT);
    g_value_set_uint(track_number, 1);
    g_hash_table_insert(dummy_tags, GST_TAG_TRACK_NUMBER, track_number);

    GValue *disc_number = g_new0(GValue, 1);
    g_value_init(disc_number, G_TYPE_UINT);
    g_value_set_uint(disc_number, 1);
    g_hash_table_insert(dummy_tags, GST_TAG_ALBUM_VOLUME_NUMBER, disc_number);

    GValue *disc_count = g_new0(GValue, 1);
    g_value_init(disc_count, G_TYPE_UINT);
    g_value_set_uint(disc_count, 2);
    g_hash_table_insert(dummy_tags, GST_TAG_ALBUM_VOLUME_COUNT, disc_count);
  }

  return dummy_tags;
}
