/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

/**
 * SECTION:as-content-rating
 * @short_description: Object representing a content rating
 * @include: appstream-glib.h
 * @stability: Unstable
 *
 * Content ratings are age-specific guidelines for applications.
 *
 * See also: #AsApp
 */

#include "config.h"

#include "as-node-private.h"
#include "as-content-rating-private.h"
#include "as-ref-string.h"
#include "as-tag.h"

typedef struct {
	AsRefString		*id;
	AsContentRatingValue	 value;
} AsContentRatingKey;

typedef struct
{
	AsRefString		*kind;
	GPtrArray		*keys; /* of AsContentRatingKey */
} AsContentRatingPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AsContentRating, as_content_rating, G_TYPE_OBJECT)

#define GET_PRIVATE(o) (as_content_rating_get_instance_private (o))

typedef enum
{
	OARS_1_0,
	OARS_1_1,
} OarsVersion;

static gboolean is_oars_key (const gchar *id, OarsVersion version);

static void
as_content_rating_finalize (GObject *object)
{
	AsContentRating *content_rating = AS_CONTENT_RATING (object);
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);

	if (priv->kind != NULL)
		as_ref_string_unref (priv->kind);
	g_ptr_array_unref (priv->keys);

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

static void
as_content_rating_key_free (AsContentRatingKey *key)
{
	if (key->id != NULL)
		as_ref_string_unref (key->id);
	g_slice_free (AsContentRatingKey, key);
}

static void
as_content_rating_init (AsContentRating *content_rating)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	priv->keys = g_ptr_array_new_with_free_func ((GDestroyNotify) as_content_rating_key_free);
}

static void
as_content_rating_class_init (AsContentRatingClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = as_content_rating_finalize;
}

static gint
ids_sort_cb (gconstpointer id_ptr_a, gconstpointer id_ptr_b)
{
	const gchar *id_a = *((const gchar **) id_ptr_a);
	const gchar *id_b = *((const gchar **) id_ptr_b);

	return g_strcmp0 (id_a, id_b);
}

/**
 * as_content_rating_get_rating_ids:
 * @content_rating: a #AsContentRating
 *
 * Gets the set of ratings IDs which are present in this @content_rating. An
 * example of a ratings ID is `violence-bloodshed`.
 *
 * The IDs are returned in lexicographical order.
 *
 * Returns: (array zero-terminated=1) (transfer container): %NULL-terminated
 *    array of ratings IDs; each ratings ID is owned by the #AsContentRating and
 *    must not be freed, but the container must be freed with g_free()
 *
 * Since: 0.7.15
 **/
const gchar **
as_content_rating_get_rating_ids (AsContentRating *content_rating)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	GPtrArray *ids = g_ptr_array_new_with_free_func (NULL);
	guint i;

	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);

	for (i = 0; i < priv->keys->len; i++) {
		AsContentRatingKey *key = g_ptr_array_index (priv->keys, i);
		g_ptr_array_add (ids, key->id);
	}

	g_ptr_array_sort (ids, ids_sort_cb);
	g_ptr_array_add (ids, NULL);  /* NULL terminator */

	return (const gchar **) g_ptr_array_free (g_steal_pointer (&ids), FALSE);
}

/**
 * as_content_rating_get_value:
 * @content_rating: a #AsContentRating
 * @id: A ratings ID, e.g. `violence-bloodshed`.
 *
 * Gets the set value of a content rating key.
 *
 * Returns: the #AsContentRatingValue, or %AS_CONTENT_RATING_VALUE_UNKNOWN
 *
 * Since: 0.6.4
 **/
AsContentRatingValue
as_content_rating_get_value (AsContentRating *content_rating, const gchar *id)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), AS_CONTENT_RATING_VALUE_UNKNOWN);
	guint i;
	for (i = 0; i < priv->keys->len; i++) {
		AsContentRatingKey *key = g_ptr_array_index (priv->keys, i);
		if (g_strcmp0 (key->id, id) == 0)
			return key->value;
	}

	/* According to the
	 * [OARS specification](https://github.com/hughsie/oars/blob/master/specification/oars-1.1.md),
	 * return %AS_CONTENT_RATING_VALUE_NONE if the #AsContentRating exists
	 * overall. Only return %AS_CONTENT_RATING_VALUE_UNKNOWN if the
	 * #AsContentRating doesn’t exist at all (or for other types of content
	 * rating). */
	if ((g_strcmp0 (priv->kind, "oars-1.0") == 0 && is_oars_key (id, OARS_1_0)) ||
	    (g_strcmp0 (priv->kind, "oars-1.1") == 0 && is_oars_key (id, OARS_1_1)))
		return AS_CONTENT_RATING_VALUE_NONE;
	else
		return AS_CONTENT_RATING_VALUE_UNKNOWN;
}

/**
 * as_content_rating_value_to_string:
 * @value: the #AsContentRatingValue.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @value
 *
 * Since: 0.5.12
 **/
const gchar *
as_content_rating_value_to_string (AsContentRatingValue value)
{
	if (value == AS_CONTENT_RATING_VALUE_NONE)
		return "none";
	if (value == AS_CONTENT_RATING_VALUE_MILD)
		return "mild";
	if (value == AS_CONTENT_RATING_VALUE_MODERATE)
		return "moderate";
	if (value == AS_CONTENT_RATING_VALUE_INTENSE)
		return "intense";
	return "unknown";
}

/**
 * as_content_rating_value_from_string:
 * @value: the string.
 *
 * Converts the text representation to an enumerated value.
 *
 * Returns: a #AsContentRatingValue or %AS_CONTENT_RATING_VALUE_UNKNOWN for unknown
 *
 * Since: 0.5.12
 **/
AsContentRatingValue
as_content_rating_value_from_string (const gchar *value)
{
	if (g_strcmp0 (value, "none") == 0)
		return AS_CONTENT_RATING_VALUE_NONE;
	if (g_strcmp0 (value, "mild") == 0)
		return AS_CONTENT_RATING_VALUE_MILD;
	if (g_strcmp0 (value, "moderate") == 0)
		return AS_CONTENT_RATING_VALUE_MODERATE;
	if (g_strcmp0 (value, "intense") == 0)
		return AS_CONTENT_RATING_VALUE_INTENSE;
	return AS_CONTENT_RATING_VALUE_UNKNOWN;
}

/* The struct definition below assumes we don’t grow more
 * #AsContentRating values. */
G_STATIC_ASSERT (AS_CONTENT_RATING_VALUE_LAST == AS_CONTENT_RATING_VALUE_INTENSE + 1);

static const struct {
	const gchar	*id;
	OarsVersion	 oars_version;  /* when the key was first added */
	guint		 csm_age_none;  /* for %AS_CONTENT_RATING_VALUE_NONE */
	guint		 csm_age_mild;  /* for %AS_CONTENT_RATING_VALUE_MILD */
	guint		 csm_age_moderate;  /* for %AS_CONTENT_RATING_VALUE_MODERATE */
	guint		 csm_age_intense;  /* for %AS_CONTENT_RATING_VALUE_INTENSE */
} oars_to_csm_mappings[] =  {
	/* Each @id must only appear once. The set of @csm_age_* values for a
	 * given @id must be complete and non-decreasing. */
	/* v1.0 */
	{ "violence-cartoon",	OARS_1_0, 0, 3, 4, 6 },
	{ "violence-fantasy",	OARS_1_0, 0, 3, 7, 8 },
	{ "violence-realistic",	OARS_1_0, 0, 4, 9, 14 },
	{ "violence-bloodshed",	OARS_1_0, 0, 9, 11, 18 },
	{ "violence-sexual",	OARS_1_0, 0, 18, 18, 18 },
	{ "drugs-alcohol",	OARS_1_0, 0, 11, 13, 16 },
	{ "drugs-narcotics",	OARS_1_0, 0, 12, 14, 17 },
	{ "drugs-tobacco",	OARS_1_0, 0, 10, 13, 13 },
	{ "sex-nudity",		OARS_1_0, 0, 12, 14, 14 },
	{ "sex-themes",		OARS_1_0, 0, 13, 14, 15 },
	{ "language-profanity",	OARS_1_0, 0, 8, 11, 14 },
	{ "language-humor",	OARS_1_0, 0, 3, 8, 14 },
	{ "language-discrimination", OARS_1_0, 0, 9, 10, 11 },
	{ "money-advertising",	OARS_1_0, 0, 7, 8, 10 },
	{ "money-gambling",	OARS_1_0, 0, 7, 10, 18 },
	{ "money-purchasing",	OARS_1_0, 0, 12, 14, 15 },
	{ "social-chat",	OARS_1_0, 0, 4, 10, 13 },
	{ "social-audio",	OARS_1_0, 0, 15, 15, 15 },
	{ "social-contacts",	OARS_1_0, 0, 12, 12, 12 },
	{ "social-info",	OARS_1_0, 0, 0, 13, 13 },
	{ "social-location",	OARS_1_0, 0, 13, 13, 13 },
	/* v1.1 additions */
	{ "sex-homosexuality",	OARS_1_1, 0, 10, 13, 18 },
	{ "sex-prostitution",	OARS_1_1, 0, 12, 14, 18 },
	{ "sex-adultery",	OARS_1_1, 0, 8, 10, 18 },
	{ "sex-appearance",	OARS_1_1, 0, 10, 10, 15 },
	{ "violence-worship",	OARS_1_1, 0, 13, 15, 18 },
	{ "violence-desecration", OARS_1_1, 0, 13, 15, 18 },
	{ "violence-slavery",	OARS_1_1, 0, 13, 15, 18 },
};

static gboolean
is_oars_key (const gchar *id, OarsVersion version)
{
	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++) {
		if (g_str_equal (id, oars_to_csm_mappings[i].id))
			return (oars_to_csm_mappings[i].oars_version <= version);
	}
	return FALSE;
}

/**
 * as_content_rating_attribute_to_csm_age:
 * @id: the subsection ID e.g. `violence-cartoon`
 * @value: the #AsContentRatingValue, e.g. %AS_CONTENT_RATING_VALUE_INTENSE
 *
 * Gets the Common Sense Media approved age for a specific rating level.
 *
 * Returns: The age in years, or 0 for no details.
 *
 * Since: 0.7.15
 **/
guint
as_content_rating_attribute_to_csm_age (const gchar *id, AsContentRatingValue value)
{
	if (value == AS_CONTENT_RATING_VALUE_UNKNOWN ||
	    value == AS_CONTENT_RATING_VALUE_LAST)
		return 0;

	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++) {
		if (g_str_equal (id, oars_to_csm_mappings[i].id)) {
			switch (value) {
			case AS_CONTENT_RATING_VALUE_NONE:
				return oars_to_csm_mappings[i].csm_age_none;
			case AS_CONTENT_RATING_VALUE_MILD:
				return oars_to_csm_mappings[i].csm_age_mild;
			case AS_CONTENT_RATING_VALUE_MODERATE:
				return oars_to_csm_mappings[i].csm_age_moderate;
			case AS_CONTENT_RATING_VALUE_INTENSE:
				return oars_to_csm_mappings[i].csm_age_intense;
			case AS_CONTENT_RATING_VALUE_UNKNOWN:
			case AS_CONTENT_RATING_VALUE_LAST:
			default:
				/* Handled above. */
				g_assert_not_reached ();
				return 0;
			}
		}
	}

	/* @id not found. */
	return 0;
}

/**
 * as_content_rating_get_all_rating_ids:
 *
 * Returns a list of all the valid OARS content rating attribute IDs as could
 * be passed to as_content_rating_add_attribute() or
 * as_content_rating_attribute_to_csm_age().
 *
 * Returns: (array zero-terminated=1) (transfer container): a %NULL-terminated
 *    array of IDs, to be freed with g_free() (the element values are owned by
 *    libappstream-glib and must not be freed)
 * Since: 0.7.15
 */
const gchar **
as_content_rating_get_all_rating_ids (void)
{
	g_autofree const gchar **ids = NULL;

	ids = g_new0 (const gchar *, G_N_ELEMENTS (oars_to_csm_mappings) + 1 /* NULL terminator */);
	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++)
		ids[i] = oars_to_csm_mappings[i].id;

	return g_steal_pointer (&ids);
}

/**
 * as_content_rating_get_minimum_age:
 * @content_rating: a #AsContentRating
 *
 * Gets the lowest Common Sense Media approved age for the content_rating block.
 * NOTE: these numbers are based on the data and descriptions available from
 * https://www.commonsensemedia.org/about-us/our-mission/about-our-ratings and
 * you may disagree with them.
 *
 * You're free to disagree with these, and of course you should use your own
 * brain to work our if your child is able to cope with the concepts enumerated
 * here. Some 13 year olds may be fine with the concept of mutilation of body
 * parts; others may get nightmares.
 *
 * Returns: The age in years, 0 for no rating, or G_MAXUINT for no details.
 *
 * Since: 0.5.12
 **/
guint
as_content_rating_get_minimum_age (AsContentRating *content_rating)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	guint i;
	guint csm_age = 0;

	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), 0);

	/* check kind */
	if (g_strcmp0 (priv->kind, "oars-1.0") != 0 &&
	    g_strcmp0 (priv->kind, "oars-1.1") != 0)
		return G_MAXUINT;

	for (i = 0; i < priv->keys->len; i++) {
		AsContentRatingKey *key;
		guint csm_tmp;
		key = g_ptr_array_index (priv->keys, i);
		csm_tmp = as_content_rating_attribute_to_csm_age (key->id, key->value);
		if (csm_tmp > 0 && csm_tmp > csm_age)
			csm_age = csm_tmp;
	}
	return csm_age;
}

/**
 * as_content_rating_get_kind:
 * @content_rating: a #AsContentRating instance.
 *
 * Gets the content_rating kind.
 *
 * Returns: a string, e.g. "oars-1.0", or NULL
 *
 * Since: 0.5.12
 **/
const gchar *
as_content_rating_get_kind (AsContentRating *content_rating)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);
	return priv->kind;
}

/**
 * as_content_rating_set_kind:
 * @content_rating: a #AsContentRating instance.
 * @kind: the rating kind, e.g. "oars-1.0"
 *
 * Sets the content rating kind.
 *
 * Since: 0.5.12
 **/
void
as_content_rating_set_kind (AsContentRating *content_rating, const gchar *kind)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	g_return_if_fail (AS_IS_CONTENT_RATING (content_rating));
	as_ref_string_assign_safe (&priv->kind, kind);
}

/**
 * as_content_rating_node_insert: (skip)
 * @content_rating: a #AsContentRating instance.
 * @parent: the parent #GNode to use.
 * @ctx: the #AsNodeContext
 *
 * Inserts the content_rating into the DOM tree.
 *
 * Returns: (transfer none): A populated #GNode, or %NULL
 *
 * Since: 0.5.12
 **/
GNode *
as_content_rating_node_insert (AsContentRating *content_rating,
			       GNode *parent,
			       AsNodeContext *ctx)
{
	AsContentRatingKey *key;
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	GNode *n;
	guint i;

	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);

	n = as_node_insert (parent, "content_rating", NULL,
			    AS_NODE_INSERT_FLAG_NONE,
			    NULL);
	if (priv->kind != NULL)
		as_node_add_attribute (n, "type", priv->kind);
	for (i = 0; i < priv->keys->len; i++) {
		const gchar *tmp;
		key = g_ptr_array_index (priv->keys, i);
		tmp = as_content_rating_value_to_string (key->value);
		as_node_insert (n, "content_attribute", tmp,
				AS_NODE_INSERT_FLAG_NONE,
				"id", key->id,
				NULL);
	}
	return n;
}

/**
 * as_content_rating_add_attribute:
 * @content_rating: a #AsContentRating instance.
 * @id: a content rating ID, e.g. `money-gambling`.
 * @value: a #AsContentRatingValue, e.g. %AS_CONTENT_RATING_VALUE_MODERATE.
 *
 * Adds an attribute value to the content rating.
 *
 * Since: 0.7.14
 **/
void
as_content_rating_add_attribute (AsContentRating *content_rating,
				 const gchar *id,
				 AsContentRatingValue value)
{
	AsContentRatingKey *key = g_slice_new0 (AsContentRatingKey);
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);

	g_return_if_fail (AS_IS_CONTENT_RATING (content_rating));
	g_return_if_fail (id != NULL);
	g_return_if_fail (value != AS_CONTENT_RATING_VALUE_UNKNOWN);

	key->id = as_ref_string_new (id);
	key->value = value;
	g_ptr_array_add (priv->keys, key);
}

/**
 * as_content_rating_node_parse:
 * @content_rating: a #AsContentRating 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.5.12
 **/
gboolean
as_content_rating_node_parse (AsContentRating *content_rating, GNode *node,
			      AsNodeContext *ctx, GError **error)
{
	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
	GNode *c;
	const gchar *tmp;
	g_autoptr(GHashTable) captions = NULL;

	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), FALSE);

	/* get ID */
	tmp = as_node_get_attribute (node, "type");
	if (tmp != NULL)
		as_content_rating_set_kind (content_rating, tmp);

	/* get keys */
	for (c = node->children; c != NULL; c = c->next) {
		AsContentRatingKey *key;
		if (as_node_get_tag (c) != AS_TAG_CONTENT_ATTRIBUTE)
			continue;
		key = g_slice_new0 (AsContentRatingKey);
		as_ref_string_assign (&key->id, as_node_get_attribute_as_refstr (c, "id"));
		key->value = as_content_rating_value_from_string (as_node_get_data (c));
		g_ptr_array_add (priv->keys, key);
	}
	return TRUE;
}

/**
 * as_content_rating_new:
 *
 * Creates a new #AsContentRating.
 *
 * Returns: (transfer full): a #AsContentRating
 *
 * Since: 0.5.12
 **/
AsContentRating *
as_content_rating_new (void)
{
	AsContentRating *content_rating;
	content_rating = g_object_new (AS_TYPE_CONTENT, NULL);
	return AS_CONTENT_RATING (content_rating);
}
