/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

/**
 * SECTION:as-release
 * @short_description: Object representing a single upstream release
 * @include: appstream-glib.h
 * @stability: Stable
 *
 * This object represents a single upstream release, typically a minor update.
 * Releases can contain a localized description of paragraph and list elements
 * and also have a version number and timestamp.
 *
 * Releases can be automatically generated by parsing upstream ChangeLogs or
 * .spec files, or can be populated using AppData files.
 *
 * See also: #AsApp
 */

#include "config.h"

#include <stdlib.h>

#include "as-cleanup.h"
#include "as-node-private.h"
#include "as-release-private.h"
#include "as-tag.h"
#include "as-utils-private.h"

#define AS_RELEASE_CHECKSUM_TYPE_MAX	G_CHECKSUM_SHA512

typedef struct _AsReleasePrivate	AsReleasePrivate;
struct _AsReleasePrivate
{
	gchar			*version;
	GHashTable		*descriptions;
	guint64			 timestamp;
	GPtrArray		*locations;
	gchar			**checksums;
};

G_DEFINE_TYPE_WITH_PRIVATE (AsRelease, as_release, G_TYPE_OBJECT)

#define GET_PRIVATE(o) (as_release_get_instance_private (o))

/**
 * as_release_finalize:
 **/
static void
as_release_finalize (GObject *object)
{
	AsRelease *release = AS_RELEASE (object);
	AsReleasePrivate *priv = GET_PRIVATE (release);

	g_free (priv->version);
	g_strfreev (priv->checksums);
	g_ptr_array_unref (priv->locations);
	if (priv->descriptions != NULL)
		g_hash_table_unref (priv->descriptions);

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

/**
 * as_release_init:
 **/
static void
as_release_init (AsRelease *release)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	priv->locations = g_ptr_array_new_with_free_func (g_free);
	priv->checksums = g_new0 (gchar *, AS_RELEASE_CHECKSUM_TYPE_MAX + 1);
}

/**
 * as_release_class_init:
 **/
static void
as_release_class_init (AsReleaseClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = as_release_finalize;
}

/**
 * as_release_get_version:
 * @release: a #AsRelease instance.
 *
 * Gets the release version.
 *
 * Returns: string, or %NULL for not set or invalid
 *
 * Since: 0.1.0
 **/
const gchar *
as_release_get_version (AsRelease *release)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	return priv->version;
}

/**
 * as_release_get_locations:
 * @release: a #AsRelease instance.
 *
 * Gets the release locations, typically URLs.
 *
 * Returns: (transfer none) (element-type utf8): list of locations
 *
 * Since: 0.3.5
 **/
GPtrArray *
as_release_get_locations (AsRelease *release)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	return priv->locations;
}

/**
 * as_release_get_location_default:
 * @release: a #AsRelease instance.
 *
 * Gets the default release location, typically a URL.
 *
 * Returns: string, or %NULL for not set or invalid
 *
 * Since: 0.3.5
 **/
const gchar *
as_release_get_location_default (AsRelease *release)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	if (priv->locations->len == 0)
		return NULL;
	return g_ptr_array_index (priv->locations, 0);
}

/**
 * as_release_get_checksum:
 * @release: a #AsRelease instance.
 *
 * Gets the release checksum, typically a SHA1 hash.
 *
 * Returns: string, or %NULL for not set or invalid
 *
 * Since: 0.3.5
 **/
const gchar *
as_release_get_checksum (AsRelease *release, GChecksumType checksum_type)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	g_return_val_if_fail (checksum_type <= AS_RELEASE_CHECKSUM_TYPE_MAX, NULL);
	return priv->checksums[checksum_type];
}

/**
 * as_release_get_timestamp:
 * @release: a #AsRelease instance.
 *
 * Gets the release timestamp.
 *
 * Returns: timestamp, or 0 for unset
 *
 * Since: 0.1.0
 **/
guint64
as_release_get_timestamp (AsRelease *release)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	return priv->timestamp;
}

/**
 * as_release_get_description:
 * @release: a #AsRelease instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the release description markup for a given locale.
 *
 * Returns: markup, or %NULL for not set or invalid
 *
 * Since: 0.1.0
 **/
const gchar *
as_release_get_description (AsRelease *release, const gchar *locale)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	if (priv->descriptions == NULL)
		return NULL;
	return as_hash_lookup_by_locale (priv->descriptions, locale);
}

/**
 * as_release_set_version:
 * @release: a #AsRelease instance.
 * @version: the version string.
 * @version_len: the size of @version, or -1 if %NULL-terminated.
 *
 * Sets the release version.
 *
 * Since: 0.1.0
 **/
void
as_release_set_version (AsRelease *release,
			const gchar *version,
			gssize version_len)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	g_free (priv->version);
	priv->version = as_strndup (version, version_len);
}

/**
 * as_release_add_location:
 * @release: a #AsRelease instance.
 * @location: the location string.
 * @location_len: the size of @location, or -1 if %NULL-terminated.
 *
 * Adds a release location.
 *
 * Since: 0.3.5
 **/
void
as_release_add_location (AsRelease *release,
			 const gchar *location,
			 gssize location_len)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	g_ptr_array_add (priv->locations, as_strndup (location, location_len));
}

/**
 * as_release_set_checksum:
 * @release: a #AsRelease instance.
 * @checksum_type: a #GChecksumType, e.g. %G_CHECKSUM_SHA1
 * @checksum: the checksum string.
 * @checksum_len: the size of @checksum, or -1 if %NULL-terminated.
 *
 * Sets the release checksum.
 *
 * Since: 0.3.5
 **/
void
as_release_set_checksum (AsRelease *release,
			 GChecksumType checksum_type,
			 const gchar *checksum,
			 gssize checksum_len)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	g_return_if_fail (checksum_type <= AS_RELEASE_CHECKSUM_TYPE_MAX);
	g_free (priv->checksums[checksum_type]);
	priv->checksums[checksum_type] = as_strndup (checksum, checksum_len);
}

/**
 * as_release_set_timestamp:
 * @release: a #AsRelease instance.
 * @timestamp: the timestamp value.
 *
 * Sets the release timestamp.
 *
 * Since: 0.1.0
 **/
void
as_release_set_timestamp (AsRelease *release, guint64 timestamp)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	priv->timestamp = timestamp;
}

/**
 * as_release_set_description:
 * @release: a #AsRelease instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @description: the description markup.
 * @description_len: the size of @description, or -1 if %NULL-terminated.
 *
 * Sets the description release markup.
 *
 * Since: 0.1.0
 **/
void
as_release_set_description (AsRelease *release,
			    const gchar *locale,
			    const gchar *description,
			    gssize description_len)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	if (locale == NULL)
		locale = "C";
	if (priv->descriptions == NULL) {
		priv->descriptions = g_hash_table_new_full (g_str_hash,
							    g_str_equal,
							    g_free,
							    g_free);
	}
	g_hash_table_insert (priv->descriptions,
			     g_strdup (locale),
			     as_strndup (description, description_len));
}

/**
 * _g_checksum_type_from_string:
 **/
static GChecksumType
_g_checksum_type_from_string (const gchar *checksum_type)
{
	if (g_ascii_strcasecmp (checksum_type, "md5") == 0)
		return G_CHECKSUM_MD5;
	if (g_ascii_strcasecmp (checksum_type, "sha1") == 0)
		return G_CHECKSUM_SHA1;
	if (g_ascii_strcasecmp (checksum_type, "sha256") == 0)
		return G_CHECKSUM_SHA256;
	if (g_ascii_strcasecmp (checksum_type, "sha512") == 0)
		return G_CHECKSUM_SHA512;
	return -1;
}

/**
 * _g_checksum_type_to_string:
 **/
static const gchar *
_g_checksum_type_to_string (GChecksumType checksum_type)
{
	if (checksum_type == G_CHECKSUM_MD5)
		return "md5";
	if (checksum_type == G_CHECKSUM_SHA1)
		return "sha1";
	if (checksum_type == G_CHECKSUM_SHA256)
		return "sha256";
	if (checksum_type == G_CHECKSUM_SHA512)
		return "sha512";
	return NULL;
}

/**
 * as_release_node_insert: (skip)
 * @release: a #AsRelease instance.
 * @parent: the parent #GNode to use..
 * @ctx: the #AsNodeContext
 *
 * Inserts the release into the DOM tree.
 *
 * Returns: (transfer none): A populated #GNode
 *
 * Since: 0.1.1
 **/
GNode *
as_release_node_insert (AsRelease *release, GNode *parent, AsNodeContext *ctx)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	GNode *n;
	_cleanup_free_ gchar *timestamp_str = NULL;

	timestamp_str = g_strdup_printf ("%" G_GUINT64_FORMAT,
					 priv->timestamp);
	n = as_node_insert (parent, "release", NULL,
			    AS_NODE_INSERT_FLAG_NONE,
			    "timestamp", timestamp_str,
			    "version", priv->version,
			    NULL);
	if (as_node_context_get_version (ctx) >= 0.9) {
		const gchar *tmp;
		guint i;
		for (i = 0; i < priv->locations->len; i++) {
			tmp = g_ptr_array_index (priv->locations, i);
			as_node_insert (n, "location", tmp,
					AS_NODE_INSERT_FLAG_NONE, NULL);
		}
		for (i = 0; i <= AS_RELEASE_CHECKSUM_TYPE_MAX; i++) {
			if (priv->checksums[i] == NULL)
				continue;
			as_node_insert (n, "checksum", priv->checksums[i],
					AS_NODE_INSERT_FLAG_NONE,
					"type", _g_checksum_type_to_string (i),
					NULL);
		}
	}
	if (priv->descriptions != NULL && as_node_context_get_version (ctx) >= 0.6) {
		as_node_insert_localized (n, "description", priv->descriptions,
					  AS_NODE_INSERT_FLAG_PRE_ESCAPED |
					  AS_NODE_INSERT_FLAG_DEDUPE_LANG);
	}
	return n;
}

/**
 * as_release_node_parse:
 * @release: a #AsRelease instance.
 * @node: a #GNode.
 * @ctx: a #AsNodeContext.
 * @error: A #GError or %NULL.
 *
 * Populates the object from a DOM node.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.0
 **/
gboolean
as_release_node_parse (AsRelease *release, GNode *node,
		       AsNodeContext *ctx, GError **error)
{
	AsReleasePrivate *priv = GET_PRIVATE (release);
	GNode *n;
	const gchar *tmp;
	gchar *taken;
	guint i;

	tmp = as_node_get_attribute (node, "timestamp");
	if (tmp != NULL)
		as_release_set_timestamp (release, atol (tmp));
	taken = as_node_take_attribute (node, "version");
	if (taken != NULL) {
		g_free (priv->version);
		priv->version = taken;
	}

	/* get optional locations */
	g_ptr_array_set_size (priv->locations, 0);
	for (n = node->children; n != NULL; n = n->next) {
		if (as_node_get_tag (n) != AS_TAG_LOCATION)
			continue;
		g_ptr_array_add (priv->locations, as_node_take_data (n));
	}

	/* get optional checksums */
	for (i = 0; i <= AS_RELEASE_CHECKSUM_TYPE_MAX; i++) {
		g_free (priv->checksums[i]);
		priv->checksums[i] = NULL;
	}
	for (n = node->children; n != NULL; n = n->next) {
		GChecksumType checksum_type;
		const gchar *type;
		if (as_node_get_tag (n) != AS_TAG_CHECKSUM)
			continue;
		type = as_node_get_attribute (n, "type");
		if (type == NULL)
			continue;
		checksum_type = _g_checksum_type_from_string (type);
		if ((gint) checksum_type == -1)
			continue;
		priv->checksums[checksum_type] = as_node_take_data (n);
	}

	/* AppStream: multiple <description> tags */
	if (as_node_context_get_source_kind (ctx) == AS_APP_SOURCE_KIND_APPSTREAM) {
		for (n = node->children; n != NULL; n = n->next) {
			_cleanup_string_free_ GString *xml = NULL;
			if (as_node_get_tag (n) != AS_TAG_DESCRIPTION)
				continue;
			xml = as_node_to_xml (n->children,
					      AS_NODE_TO_XML_FLAG_INCLUDE_SIBLINGS);
			as_release_set_description (release,
						    as_node_get_attribute (n, "xml:lang"),
						    xml->str, xml->len);
		}

	/* AppData: mutliple languages encoded in one <description> tag */
	} else {
		n = as_node_find (node, "description");
		if (n != NULL) {
			if (priv->descriptions != NULL)
				g_hash_table_unref (priv->descriptions);
			priv->descriptions = as_node_get_localized_unwrap (n, error);
			if (priv->descriptions == NULL)
				return FALSE;
		}
	}

	return TRUE;
}

/**
 * as_release_new:
 *
 * Creates a new #AsRelease.
 *
 * Returns: (transfer full): a #AsRelease
 *
 * Since: 0.1.0
 **/
AsRelease *
as_release_new (void)
{
	AsRelease *release;
	release = g_object_new (AS_TYPE_RELEASE, NULL);
	return AS_RELEASE (release);
}
