/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
 * GData Client
 * Copyright (C) Philip Withnall 2009–2010 <philip@tecnocode.co.uk>
 *
 * GData Client is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * GData Client 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:gdata-media-thumbnail
 * @short_description: Media RSS thumbnail element
 * @stability: Stable
 * @include: gdata/media/gdata-media-thumbnail.h
 *
 * #GDataMediaThumbnail represents a "thumbnail" element from the
 * <ulink type="http" url="http://video.search.yahoo.com/mrss">Media RSS specification</ulink>.
 *
 * The class only implements parsing, not XML output, at the moment.
 */

#include <glib.h>
#include <libxml/parser.h>
#include <string.h>

#include "gdata-media-thumbnail.h"
#include "gdata-download-stream.h"
#include "gdata-parsable.h"
#include "gdata-parser.h"
#include "gdata-private.h"

static void gdata_media_thumbnail_finalize (GObject *object);
static void gdata_media_thumbnail_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
static gboolean pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data, GError **error);
static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
static gboolean
parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data,
            GError **error);

struct _GDataMediaThumbnailPrivate {
	gchar *uri;
	guint height;
	guint width;
	gint64 time;
};

enum {
	PROP_URI = 1,
	PROP_HEIGHT,
	PROP_WIDTH,
	PROP_TIME
};

G_DEFINE_TYPE (GDataMediaThumbnail, gdata_media_thumbnail, GDATA_TYPE_PARSABLE)

static void
gdata_media_thumbnail_class_init (GDataMediaThumbnailClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
	GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);

	g_type_class_add_private (klass, sizeof (GDataMediaThumbnailPrivate));

	gobject_class->get_property = gdata_media_thumbnail_get_property;
	gobject_class->finalize = gdata_media_thumbnail_finalize;

	parsable_class->pre_parse_xml = pre_parse_xml;
	parsable_class->get_namespaces = get_namespaces;
	parsable_class->parse_json = parse_json;
	parsable_class->element_name = "thumbnail";
	parsable_class->element_namespace = "media";

	/**
	 * GDataMediaThumbnail:uri:
	 *
	 * The URI of the thumbnail.
	 *
	 * For more information, see the <ulink type="http" url="http://video.search.yahoo.com/mrss">Media RSS specification</ulink>.
	 *
	 * Since: 0.4.0
	 */
	g_object_class_install_property (gobject_class, PROP_URI,
	                                 g_param_spec_string ("uri",
	                                                      "URI", "The URI of the thumbnail.",
	                                                      NULL,
	                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

	/**
	 * GDataMediaThumbnail:height:
	 *
	 * The height of the thumbnail, in pixels.
	 *
	 * For more information, see the <ulink type="http" url="http://video.search.yahoo.com/mrss">Media RSS specification</ulink>.
	 *
	 * Since: 0.4.0
	 */
	g_object_class_install_property (gobject_class, PROP_HEIGHT,
	                                 g_param_spec_uint ("height",
	                                                    "Height", "The height of the thumbnail, in pixels.",
	                                                    0, G_MAXUINT, 0,
	                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

	/**
	 * GDataMediaThumbnail:width:
	 *
	 * The width of the thumbnail, in pixels.
	 *
	 * For more information, see the <ulink type="http" url="http://video.search.yahoo.com/mrss">Media RSS specification</ulink>.
	 *
	 * Since: 0.4.0
	 */
	g_object_class_install_property (gobject_class, PROP_WIDTH,
	                                 g_param_spec_uint ("width",
	                                                    "Width", "The width of the thumbnail, in pixels.",
	                                                    0, G_MAXUINT, 0,
	                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

	/**
	 * GDataMediaThumbnail:time:
	 *
	 * The time offset of the thumbnail in relation to the media object, in milliseconds.
	 *
	 * For more information, see the <ulink type="http" url="http://video.search.yahoo.com/mrss">Media RSS specification</ulink>.
	 *
	 * Since: 0.4.0
	 */
	g_object_class_install_property (gobject_class, PROP_TIME,
	                                 g_param_spec_int64 ("time",
	                                                     "Time", "The time offset of the thumbnail in relation to the media object, in ms.",
	                                                     -1, G_MAXINT64, -1,
	                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

static void
gdata_media_thumbnail_init (GDataMediaThumbnail *self)
{
	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_MEDIA_THUMBNAIL, GDataMediaThumbnailPrivate);
}

static void
gdata_media_thumbnail_finalize (GObject *object)
{
	GDataMediaThumbnailPrivate *priv = GDATA_MEDIA_THUMBNAIL (object)->priv;

	g_free (priv->uri);

	/* Chain up to the parent class */
	G_OBJECT_CLASS (gdata_media_thumbnail_parent_class)->finalize (object);
}

static void
gdata_media_thumbnail_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
{
	GDataMediaThumbnailPrivate *priv = GDATA_MEDIA_THUMBNAIL (object)->priv;

	switch (property_id) {
		case PROP_URI:
			g_value_set_string (value, priv->uri);
			break;
		case PROP_HEIGHT:
			g_value_set_uint (value, priv->height);
			break;
		case PROP_WIDTH:
			g_value_set_uint (value, priv->width);
			break;
		case PROP_TIME:
			g_value_set_int64 (value, priv->time);
			break;
		default:
			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

/*
 * gdata_media_thumbnail_parse_time:
 * @time_string: a time string to parse
 *
 * Parses a time string in (a subset of) NTP format into a number of milliseconds since the start of a media stream.
 *
 * For more information about NTP format, see <ulink type="http" url="http://www.ietf.org/rfc/rfc2326.txt">RFC 2326 3.6 Normal Play Time</ulink>.
 *
 * To build an NTP-format string, see gdata_media_thumbnail_build_time().
 *
 * Return value: number of milliseconds since the start of a media stream
 */
static gint64
parse_time (const gchar *time_string)
{
	guint hours, minutes;
	gdouble seconds;
	gchar *end_pointer;

	g_return_val_if_fail (time_string != NULL, 0);

	hours = g_ascii_strtoull (time_string, &end_pointer, 10);
	if (end_pointer != time_string + 2)
		return -1;

	minutes = g_ascii_strtoull (time_string + 3, &end_pointer, 10);
	if (end_pointer != time_string + 5)
		return -1;

	seconds = g_ascii_strtod (time_string + 6, &end_pointer);
	if (end_pointer != time_string + strlen (time_string))
		return -1;

	return (gint64) ((seconds + minutes * 60 + hours * 3600) * 1000);
}

/*
 * gdata_media_thumbnail_build_time:
 * @_time: a number of milliseconds since the start of a media stream
 *
 * Builds an NTP-format time string describing @_time milliseconds since the start
 * of a media stream.
 *
 * Return value: an NTP-format string describing @_time; free with g_free()
 */
/*static gchar *
build_time (gint64 _time)
{
	guint hours, minutes;
	gfloat seconds;

	hours = _time % 3600000;
	_time -= hours * 3600000;

	minutes = _time % 60000;
	_time -= minutes * 60000;

	seconds = _time / 1000.0;

	return g_strdup_printf ("%02u:%02u:%02f", hours, minutes, seconds);
}*/

static gboolean
pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data, GError **error)
{
	GDataMediaThumbnailPrivate *priv = GDATA_MEDIA_THUMBNAIL (parsable)->priv;
	xmlChar *uri, *width, *height, *_time;
	guint width_uint, height_uint;
	gint64 time_int64;

	/* Get the width and height */
	width = xmlGetProp (root_node, (xmlChar*) "width");
	width_uint = (width == NULL) ? 0 : g_ascii_strtoull ((gchar*) width, NULL, 10);
	xmlFree (width);

	height = xmlGetProp (root_node, (xmlChar*) "height");
	height_uint = (height == NULL) ? 0 : g_ascii_strtoull ((gchar*) height, NULL, 10);
	xmlFree (height);

	/* Get and parse the time */
	_time = xmlGetProp (root_node, (xmlChar*) "time");
	if (_time == NULL) {
		time_int64 = -1;
	} else {
		time_int64 = parse_time ((gchar*) _time);
		if (time_int64 == -1) {
			gdata_parser_error_unknown_property_value (root_node, "time", (gchar*) _time, error);
			xmlFree (_time);
			return FALSE;
		}
		xmlFree (_time);
	}

	/* Get the URI */
	uri = xmlGetProp (root_node, (xmlChar*) "url");
	if (uri == NULL || *uri == '\0') {
		xmlFree (uri);
		return gdata_parser_error_required_property_missing (root_node, "url", error);
	}

	priv->uri = (gchar*) uri;
	priv->height = height_uint;
	priv->width = width_uint;
	priv->time = time_int64;

	return TRUE;
}

static void
get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
{
	g_hash_table_insert (namespaces, (gchar*) "media", (gchar*) "http://search.yahoo.com/mrss/");
}

/* Reference:
 * https://developers.google.com/youtube/v3/docs/videos#snippet.thumbnails */
static gboolean
parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data,
            GError **error)
{
	gboolean success;
	gint64 width, height;
	GDataMediaThumbnail *self = GDATA_MEDIA_THUMBNAIL (parsable);
	GDataMediaThumbnailPrivate *priv = self->priv;

	if (gdata_parser_string_from_json_member (reader, "url", P_DEFAULT,
	                                          &priv->uri, &success,
	                                          error)) {
		return success;
	} else if (gdata_parser_int_from_json_member (reader, "width",
	                                              P_DEFAULT, &width,
	                                              &success, error)) {
		priv->width = width;
		return success;
	} else if (gdata_parser_int_from_json_member (reader, "height",
	                                              P_DEFAULT, &height,
	                                              &success, error)) {
		priv->height = height;
		return success;
	} else {
		return GDATA_PARSABLE_CLASS (gdata_media_thumbnail_parent_class)->parse_json (parsable, reader, user_data, error);
	}
}

/**
 * gdata_media_thumbnail_get_uri:
 * @self: a #GDataMediaThumbnail
 *
 * Gets the #GDataMediaThumbnail:uri property.
 *
 * Return value: the thumbnail's URI
 *
 * Since: 0.4.0
 */
const gchar *
gdata_media_thumbnail_get_uri (GDataMediaThumbnail *self)
{
	g_return_val_if_fail (GDATA_IS_MEDIA_THUMBNAIL (self), NULL);
	return self->priv->uri;
}

/**
 * gdata_media_thumbnail_get_height:
 * @self: a #GDataMediaThumbnail
 *
 * Gets the #GDataMediaThumbnail:height property.
 *
 * Return value: the thumbnail's height in pixels, or <code class="literal">0</code>
 *
 * Since: 0.4.0
 */
guint
gdata_media_thumbnail_get_height (GDataMediaThumbnail *self)
{
	g_return_val_if_fail (GDATA_IS_MEDIA_THUMBNAIL (self), 0);
	return self->priv->height;
}

/**
 * gdata_media_thumbnail_get_width:
 * @self: a #GDataMediaThumbnail
 *
 * Gets the #GDataMediaThumbnail:width property.
 *
 * Return value: the thumbnail's width in pixels, or <code class="literal">0</code>
 *
 * Since: 0.4.0
 */
guint
gdata_media_thumbnail_get_width (GDataMediaThumbnail *self)
{
	g_return_val_if_fail (GDATA_IS_MEDIA_THUMBNAIL (self), 0);
	return self->priv->width;
}

/**
 * gdata_media_thumbnail_get_time:
 * @self: a #GDataMediaThumbnail
 *
 * Gets the #GDataMediaThumbnail:time property.
 *
 * Return value: the thumbnail's time offset in the media, or <code class="literal">-1</code>
 *
 * Since: 0.4.0
 */
gint64
gdata_media_thumbnail_get_time (GDataMediaThumbnail *self)
{
	g_return_val_if_fail (GDATA_IS_MEDIA_THUMBNAIL (self), -1);
	return self->priv->time;
}

/**
 * gdata_media_thumbnail_download:
 * @self: a #GDataMediaThumbnail
 * @service: the #GDataService
 * @cancellable: (allow-none): a #GCancellable for the entire download stream, or %NULL
 * @error: a #GError, or %NULL
 *
 * Downloads and returns a #GDataDownloadStream allowing the thumbnail data represented by @self to be read.
 *
 * To get the content type of the downloaded data, gdata_download_stream_get_content_type() can be called on the returned #GDataDownloadStream.
 * Calling gdata_download_stream_get_content_length() on the stream will not return a meaningful result, however, as the stream is encoded in chunks,
 * rather than by content length.
 *
 * In order to cancel the download, a #GCancellable passed in to @cancellable must be cancelled using g_cancellable_cancel(). Cancelling the individual
 * #GInputStream operations on the #GDataDownloadStream will not cancel the entire download; merely the read or close operation in question. See the
 * #GDataDownloadStream:cancellable for more details.
 *
 * Return value: (transfer full): a #GDataDownloadStream to download the thumbnail with, or %NULL; unref with g_object_unref()
 *
 * Since: 0.8.0
 */
GDataDownloadStream *
gdata_media_thumbnail_download (GDataMediaThumbnail *self, GDataService *service, GCancellable *cancellable, GError **error)
{
	const gchar *src_uri;

	g_return_val_if_fail (GDATA_IS_MEDIA_THUMBNAIL (self), NULL);
	g_return_val_if_fail (GDATA_IS_SERVICE (service), NULL);
	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* We keep a GError in the argument list so that we can add authentication errors, etc., in future if necessary */

	/* Get the download URI and create a stream for it */
	src_uri = gdata_media_thumbnail_get_uri (self);
	return GDATA_DOWNLOAD_STREAM (gdata_download_stream_new (service, NULL, src_uri, cancellable));
}
