/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2012-2020 Matthias Klumpp <matthias@tenstral.net>
 * Copyright (C) 2014 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library 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.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:as-screenshot
 * @short_description: Object representing a single screenshot
 *
 * Screenshots have a localized caption and contain either a set of images
 * of different resolution or a video screencast showcasing the software.
 *
 * See also: #AsImage, #AsVideo
 */

#include "as-screenshot.h"
#include "as-screenshot-private.h"

#include "as-utils.h"
#include "as-utils-private.h"
#include "as-context-private.h"
#include "as-image-private.h"
#include "as-video-private.h"

typedef struct
{
	AsScreenshotKind kind;
	AsScreenshotMediaKind media_kind;
	GHashTable *caption;

	GPtrArray *images;
	GPtrArray *images_lang;
	GPtrArray *videos;
	GPtrArray *videos_lang;

	AsContext *context;
	gchar *active_locale_override;
} AsScreenshotPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AsScreenshot, as_screenshot, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (as_screenshot_get_instance_private (o))

/**
 * as_screenshot_init:
 **/
static void
as_screenshot_init (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);

	priv->kind = AS_SCREENSHOT_KIND_EXTRA;
	priv->media_kind = AS_SCREENSHOT_MEDIA_KIND_IMAGE;
	priv->caption = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->images = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->images_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->videos = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->videos_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
}

/**
 * as_screenshot_finalize:
 **/
static void
as_screenshot_finalize (GObject *object)
{
	AsScreenshot *screenshot = AS_SCREENSHOT (object);
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);

	g_free (priv->active_locale_override);
	g_ptr_array_unref (priv->images);
	g_ptr_array_unref (priv->images_lang);
	g_ptr_array_unref (priv->videos);
	g_ptr_array_unref (priv->videos_lang);
	g_hash_table_unref (priv->caption);
	if (priv->context != NULL)
		g_object_unref (priv->context);

	G_OBJECT_CLASS (as_screenshot_parent_class)->finalize (object);
}

/**
 * as_screenshot_class_init:
 **/
static void
as_screenshot_class_init (AsScreenshotClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = as_screenshot_finalize;
}

/**
 * as_screenshot_kind_from_string:
 * @kind: the string.
 *
 * Converts the text representation to an enumerated value.
 *
 * Returns: a %AsScreenshotKind, or %AS_SCREENSHOT_KIND_UNKNOWN if not known.
 **/
AsScreenshotKind
as_screenshot_kind_from_string (const gchar *kind)
{
	if (g_strcmp0 (kind, "default") == 0)
		return AS_SCREENSHOT_KIND_DEFAULT;
	if (g_strcmp0 (kind, "extra") == 0)
		return AS_SCREENSHOT_KIND_EXTRA;
	if ((g_strcmp0 (kind, "") == 0) || (kind == NULL))
		return AS_SCREENSHOT_KIND_EXTRA;
	return AS_SCREENSHOT_KIND_UNKNOWN;
}

/**
 * as_screenshot_kind_to_string:
 * @kind: the #AsScreenshotKind.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @kind
 **/
const gchar *
as_screenshot_kind_to_string (AsScreenshotKind kind)
{
	if (kind == AS_SCREENSHOT_KIND_DEFAULT)
		return "default";
	if (kind == AS_SCREENSHOT_KIND_EXTRA)
		return "extra";
	return NULL;
}

/**
 * as_screenshot_get_kind:
 * @screenshot: a #AsScreenshot instance.
 *
 * Gets the screenshot kind.
 *
 * Returns: a #AsScreenshotKind
 **/
AsScreenshotKind
as_screenshot_get_kind (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return priv->kind;
}

/**
 * as_screenshot_set_kind:
 * @screenshot: a #AsScreenshot instance.
 * @kind: the #AsScreenshotKind.
 *
 * Sets the screenshot kind.
 **/
void
as_screenshot_set_kind (AsScreenshot *screenshot, AsScreenshotKind kind)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	priv->kind = kind;
}

/**
 * as_screenshot_get_media_kind:
 * @screenshot: a #AsScreenshot instance.
 *
 * Gets the screenshot media kind.
 *
 * Returns: a #AsScreenshotMediaKind
 **/
AsScreenshotMediaKind
as_screenshot_get_media_kind (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return priv->media_kind;
}

/**
 * as_screenshot_get_images:
 * @screenshot: a #AsScreenshot instance.
 *
 * Gets the images for this screenshots. Only images valid for the current
 * language are returned. We return all sizes.
 *
 * Returns: (transfer none) (element-type AsImage): an array
 **/
GPtrArray*
as_screenshot_get_images (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	if (priv->images_lang->len == 0)
		return as_screenshot_get_images_all (screenshot);
	return priv->images_lang;
}

/**
 * as_screenshot_add_image:
 * @screenshot: a #AsScreenshot instance.
 * @image: a #AsImage instance.
 *
 * Adds an image to the screenshot.
 **/
void
as_screenshot_add_image (AsScreenshot *screenshot, AsImage *image)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	g_ptr_array_add (priv->images, g_object_ref (image));

	if (as_utils_locale_is_compatible (as_image_get_locale (image), as_screenshot_get_active_locale (screenshot)))
		g_ptr_array_add (priv->images_lang, g_object_ref (image));
}

/**
 * as_screenshot_get_videos:
 * @screenshot: a #AsScreenshot instance.
 *
 * Gets the videos for this screenshots. Only videos valid for the current
 * language selection are returned. We return all sizes.
 *
 * Returns: (transfer none) (element-type AsVideo): an array
 **/
GPtrArray*
as_screenshot_get_videos (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	if (priv->videos_lang->len == 0)
		return priv->videos;
	return priv->videos_lang;
}

/**
 * as_screenshot_add_video:
 * @screenshot: a #AsScreenshot instance.
 * @video: a #AsVideo instance.
 *
 * Adds a video to the screenshot.
 **/
void
as_screenshot_add_video (AsScreenshot *screenshot, AsVideo *video)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	priv->media_kind = AS_SCREENSHOT_MEDIA_KIND_VIDEO;
	g_ptr_array_add (priv->videos, g_object_ref (video));

	if (as_utils_locale_is_compatible (as_video_get_locale (video), as_screenshot_get_active_locale (screenshot)))
		g_ptr_array_add (priv->videos_lang, g_object_ref (video));
}

/**
 * as_screenshot_get_caption:
 * @screenshot: a #AsScreenshot instance.
 *
 * Gets the image caption
 *
 * Returns: the caption
 **/
const gchar*
as_screenshot_get_caption (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return as_context_localized_ht_get (priv->context,
					    priv->caption,
					    priv->active_locale_override,
					    AS_VALUE_FLAG_NONE);
}

/**
 * as_screenshot_set_caption:
 * @screenshot: a #AsScreenshot instance.
 * @caption: the caption text.
 *
 * Sets a caption on the screenshot
 **/
void
as_screenshot_set_caption (AsScreenshot *screenshot, const gchar *caption, const gchar *locale)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	as_context_localized_ht_set (priv->context,
				     priv->caption,
				     caption,
				     locale);
}

/**
 * as_screenshot_is_valid:
 * @screenshot: a #AsScreenshot instance.
 *
 * Performs a quick validation on this screenshot
 *
 * Returns: TRUE if the screenshot is a complete #AsScreenshot
 **/
gboolean
as_screenshot_is_valid (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return priv->images->len > 0;
}

/**
 * as_screenshot_rebuild_suitable_media_list:
 * @screenshot: a #AsScreenshot instance.
 *
 * Rebuild list of images or videos suitable for the selected locale.
 */
static void
as_screenshot_rebuild_suitable_media_list (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);

	/* rebuild our list of images suitable for the current locale */
	g_ptr_array_unref (priv->images_lang);
	priv->images_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	for (guint i = 0; i < priv->images->len; i++) {
		AsImage *img = AS_IMAGE (g_ptr_array_index (priv->images, i));
		if (!as_utils_locale_is_compatible (as_image_get_locale (img), as_screenshot_get_active_locale (screenshot)))
			continue;
		g_ptr_array_add (priv->images_lang, g_object_ref (img));
	}

	/* rebuild our list of videos suitable for the current locale */
	g_ptr_array_unref (priv->videos_lang);
	priv->videos_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	for (guint i = 0; i < priv->videos->len; i++) {
		AsVideo *vid = AS_VIDEO (g_ptr_array_index (priv->videos, i));
		if (!as_utils_locale_is_compatible (as_video_get_locale (vid), as_screenshot_get_active_locale (screenshot)))
			continue;
		g_ptr_array_add (priv->videos_lang, g_object_ref (vid));
	}
}

/**
 * as_screenshot_get_active_locale:
 *
 * Get the current active locale, which
 * is used to get localized messages.
 */
const gchar*
as_screenshot_get_active_locale (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	const gchar *locale;

	/* return context locale, if the locale isn't explicitly overridden for this component */
	if ((priv->context != NULL) && (priv->active_locale_override == NULL)) {
		locale = as_context_get_locale (priv->context);
	} else {
		locale = priv->active_locale_override;
	}

	if (locale == NULL)
		return "C";
	else
		return locale;
}

/**
 * as_screenshot_set_active_locale:
 *
 * Set the current active locale, which
 * is used to get localized messages.
 * If the #AsComponent linking this #AsScreenshot was fetched
 * from a localized database, usually only
 * one locale is available.
 */
void
as_screenshot_set_active_locale (AsScreenshot *screenshot, const gchar *locale)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);

	g_free (priv->active_locale_override);
	priv->active_locale_override = g_strdup (locale);

	/* rebuild our list of images suitable for the current locale */
	as_screenshot_rebuild_suitable_media_list (screenshot);
}

/**
 * as_screenshot_get_images_all:
 * @screenshot: an #AsScreenshot instance.
 *
 * Returns an array of all images we have, regardless of their
 * size and language.
 *
 * Returns: (transfer none) (element-type AsImage): an array
 *
 * Since: 0.10
 **/
GPtrArray*
as_screenshot_get_images_all (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return priv->images;
}

/**
 * as_screenshot_get_context:
 * @screenshot: an #AsScreenshot instance.
 *
 * Returns: the #AsContext associated with this screenshot.
 * This function may return %NULL if no context is set.
 *
 * Since: 0.11.2
 */
AsContext*
as_screenshot_get_context (AsScreenshot *screenshot)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	return priv->context;
}

/**
 * as_screenshot_set_context:
 * @screenshot: an #AsScreenshot instance.
 * @context: the #AsContext.
 *
 * Sets the document context this screenshot is associated
 * with.
 *
 * Since: 0.11.2
 */
void
as_screenshot_set_context (AsScreenshot *screenshot, AsContext *context)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	if (priv->context != NULL)
		g_object_unref (priv->context);
	priv->context = g_object_ref (context);

	/* reset individual properties, so the new context overrides them */
	g_free (priv->active_locale_override);
	priv->active_locale_override = NULL;

	as_screenshot_rebuild_suitable_media_list (screenshot);
}

/**
 * as_screenshot_load_from_xml:
 * @screenshot: an #AsScreenshot instance.
 * @ctx: the AppStream document context.
 * @node: the XML node.
 * @error: a #GError.
 *
 * Loads data from an XML node.
 **/
gboolean
as_screenshot_load_from_xml (AsScreenshot *screenshot, AsContext *ctx, xmlNode *node, GError **error)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	xmlNode *iter;
	g_autofree gchar *prop = NULL;
	gboolean children_found = FALSE;

	prop = (gchar*) xmlGetProp (node, (xmlChar*) "type");
	if (g_strcmp0 (prop, "default") == 0)
		priv->kind = AS_SCREENSHOT_KIND_DEFAULT;
	else
		priv->kind = AS_SCREENSHOT_KIND_EXTRA;

	for (iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar*) iter->name;
		children_found = TRUE;

		if (g_strcmp0 (node_name, "image") == 0) {
			g_autoptr(AsImage) image = as_image_new ();
			if (as_image_load_from_xml (image, ctx, iter, NULL))
				as_screenshot_add_image (screenshot, image);
		} else if (g_strcmp0 (node_name, "video") == 0) {
			g_autoptr(AsVideo) video = as_video_new ();
			if (as_video_load_from_xml (video, ctx, iter, NULL))
				as_screenshot_add_video (screenshot, video);
		} else if (g_strcmp0 (node_name, "caption") == 0) {
			g_autofree gchar *content = NULL;
			g_autofree gchar *lang = NULL;

			content = as_xml_get_node_value (iter);
			if (content == NULL)
				continue;

			lang = as_xml_get_node_locale_match (ctx, iter);
			if (lang != NULL)
				as_screenshot_set_caption (screenshot, content, lang);
		}
	}

	if (!children_found) {
		/* we are likely dealing with a legacy screenshot node, which does not have <image/> children,
		 * but instead contains the screenshot URL as text. This was briefly supported in an older AppStream
		 * version for metainfo files, but it should no longer be used.
		 * We support it here only for legacy compatibility. */
		g_autoptr(AsImage) image = as_image_new ();

		if (as_image_load_from_xml (image, ctx, node, NULL))
			as_screenshot_add_image (screenshot, image);
		else
			return FALSE; /* this screenshot is invalid */
	}

	/* propagate context - we do this last so the image list for the selected locale is rebuilt properly */
	as_screenshot_set_context (screenshot, ctx);

	return TRUE;
}

/**
 * as_screenshot_to_xml_node:
 * @screenshot: an #AsScreenshot instance.
 * @ctx: the AppStream document context.
 * @root: XML node to attach the new nodes to.
 *
 * Serializes the data to an XML node.
 **/
void
as_screenshot_to_xml_node (AsScreenshot *screenshot, AsContext *ctx, xmlNode *root)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	xmlNode *subnode;

	subnode = xmlNewChild (root, NULL, (xmlChar*) "screenshot", NULL);
	if (priv->kind == AS_SCREENSHOT_KIND_DEFAULT)
		xmlNewProp (subnode, (xmlChar*) "type", (xmlChar*) "default");

	as_xml_add_localized_text_node (subnode, "caption", priv->caption);

	if (priv->media_kind == AS_SCREENSHOT_MEDIA_KIND_IMAGE) {
		for (guint i = 0; i < priv->images->len; i++) {
			AsImage *image = AS_IMAGE (g_ptr_array_index (priv->images, i));
			as_image_to_xml_node (image, ctx, subnode);
		}
	} else if (priv->media_kind == AS_SCREENSHOT_MEDIA_KIND_VIDEO) {
		for (guint i = 0; i < priv->videos->len; i++) {
			AsVideo *video = AS_VIDEO (g_ptr_array_index (priv->videos, i));
			as_video_to_xml_node (video, ctx, subnode);
		}
	}
}

/**
 * as_screenshot_load_from_yaml:
 * @screenshot: an #AsScreenshot instance.
 * @ctx: the AppStream document context.
 * @node: the YAML node.
 * @error: a #GError.
 *
 * Loads data from a YAML field.
 **/
gboolean
as_screenshot_load_from_yaml (AsScreenshot *screenshot, AsContext *ctx, GNode *node, GError **error)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	GNode *n;

	for (n = node->children; n != NULL; n = n->next) {
		GNode *in;
		const gchar *key = as_yaml_node_get_key (n);
		const gchar *value = as_yaml_node_get_value (n);

		if (g_strcmp0 (key, "default") == 0) {
			if ((g_strcmp0 (value, "true") == 0) || (g_strcmp0 (value, "yes") == 0))
				priv->kind = AS_SCREENSHOT_KIND_DEFAULT;
			else
				priv->kind = AS_SCREENSHOT_KIND_EXTRA;
		} else if (g_strcmp0 (key, "caption") == 0) {
			/* the caption is a localized element */
			as_yaml_set_localized_table (ctx, n, priv->caption);
		} else if (g_strcmp0 (key, "source-image") == 0) {
			/* there can only be one source image */
			g_autoptr(AsImage) image = as_image_new ();
			if (as_image_load_from_yaml (image, ctx, n, AS_IMAGE_KIND_SOURCE, NULL))
				as_screenshot_add_image (screenshot, image);
		} else if (g_strcmp0 (key, "thumbnails") == 0) {
			/* the thumbnails are a list of images */
			for (in = n->children; in != NULL; in = in->next) {
				g_autoptr(AsImage) image = as_image_new ();
				if (as_image_load_from_yaml (image, ctx, in, AS_IMAGE_KIND_THUMBNAIL, NULL))
					as_screenshot_add_image (screenshot, image);
			}
		} else if (g_strcmp0 (key, "videos") == 0) {
			for (in = n->children; in != NULL; in = in->next) {
				g_autoptr(AsVideo) video = as_video_new ();
				if (as_video_load_from_yaml (video, ctx, in, NULL))
					as_screenshot_add_video (screenshot, video);
			}
		} else {
			as_yaml_print_unknown ("screenshot", key);
		}
	}

	/* propagate context - we do this last so the image list for the selected locale is rebuilt properly */
	as_screenshot_set_context (screenshot, ctx);

	return TRUE;
}

/**
 * as_screenshot_emit_yaml:
 * @screenshot: an #AsScreenshot instance.
 * @ctx: the AppStream document context.
 * @emitter: The YAML emitter to emit data on.
 *
 * Emit YAML data for this object.
 **/
void
as_screenshot_emit_yaml (AsScreenshot *screenshot, AsContext *ctx, yaml_emitter_t *emitter)
{
	AsScreenshotPrivate *priv = GET_PRIVATE (screenshot);
	AsImage *source_img = NULL;

	as_yaml_mapping_start (emitter);

	if (priv->kind == AS_SCREENSHOT_KIND_DEFAULT)
		as_yaml_emit_entry (emitter, "default", "true");

	as_yaml_emit_localized_entry (emitter, "caption", priv->caption);

	if (priv->media_kind == AS_SCREENSHOT_MEDIA_KIND_IMAGE) {
		as_yaml_emit_scalar (emitter, "thumbnails");
		as_yaml_sequence_start (emitter);
		for (guint i = 0; i < priv->images->len; i++) {
			AsImage *img = AS_IMAGE (g_ptr_array_index (priv->images, i));

			if (as_image_get_kind (img) == AS_IMAGE_KIND_SOURCE) {
				source_img = img;
				continue;
			}

			as_image_emit_yaml (img, ctx, emitter);
		}
		as_yaml_sequence_end (emitter);

		/* we *must* have a source-image by now if the data follows the spec, but better be safe... */
		if (source_img != NULL) {
			as_yaml_emit_scalar (emitter, "source-image");
			as_image_emit_yaml (source_img, ctx, emitter);
		}
	} else if (priv->media_kind == AS_SCREENSHOT_MEDIA_KIND_VIDEO) {
		as_yaml_emit_scalar (emitter, "videos");
		as_yaml_sequence_start (emitter);
		for (guint i = 0; i < priv->videos->len; i++) {
			AsVideo *video = AS_VIDEO (g_ptr_array_index (priv->videos, i));
			as_video_emit_yaml (video, ctx, emitter);
		}
		as_yaml_sequence_end (emitter);
	}

	as_yaml_mapping_end (emitter);
}

/**
 * as_screenshot_new:
 *
 * Creates a new #AsScreenshot.
 *
 * Returns: (transfer full): a #AsScreenshot
 **/
AsScreenshot*
as_screenshot_new (void)
{
	AsScreenshot *screenshot;
	screenshot = g_object_new (AS_TYPE_SCREENSHOT, NULL);
	return AS_SCREENSHOT (screenshot);
}
