/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2021 Matthias Klumpp <matthias@tenstral.net>
 *
 * 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:asc-result
 * @short_description: A composer result for a single unit.
 * @include: appstream-compose.h
 */

#include "config.h"
#include "asc-result.h"

#include "as-utils-private.h"
#include "asc-globals-private.h"
#include "asc-utils.h"
#include "asc-hint.h"

typedef struct
{
	AsBundleKind	bundle_kind;
	gchar		*bundle_id;

	GHashTable	*cpts; /* GRefString->AsComponent */
	GHashTable	*mdata_hashes; /* AsComponent->utf8 */
	GHashTable	*hints; /* GRefString->GPtrArray */
	GHashTable	*gcids; /* GRefString->utf8 (component-id -> global component-id) */
} AscResultPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AscResult, asc_result, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (asc_result_get_instance_private (o))

static void
asc_result_init (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);

	priv->bundle_kind = AS_BUNDLE_KIND_UNKNOWN;

	priv->cpts = g_hash_table_new_full (g_str_hash,
					    g_str_equal,
					    (GDestroyNotify) as_ref_string_release,
					    g_object_unref);
	priv->mdata_hashes = g_hash_table_new_full (g_direct_hash,
						    g_direct_equal,
						    NULL,
						    g_free);
	priv->hints = g_hash_table_new_full (g_str_hash,
					     g_str_equal,
					     (GDestroyNotify) as_ref_string_release,
					     (GDestroyNotify) g_ptr_array_unref);
	priv->gcids = g_hash_table_new_full (g_str_hash,
					     g_str_equal,
					     (GDestroyNotify) as_ref_string_release,
					     g_free);
}

static void
asc_result_finalize (GObject *object)
{
	AscResult *result = ASC_RESULT (object);
	AscResultPrivate *priv = GET_PRIVATE (result);

	g_free (priv->bundle_id);

	g_hash_table_unref (priv->cpts);
	g_hash_table_unref (priv->mdata_hashes);
	g_hash_table_unref (priv->hints);
	g_hash_table_unref (priv->gcids);

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

static void
asc_result_class_init (AscResultClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = asc_result_finalize;
}

/**
 * asc_result_unit_ignored:
 * @result: an #AscResult instance.
 *
 * Returns: %TRUE if this result means the analyzed unit was ignored entirely..
 **/
gboolean
asc_result_unit_ignored (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return (g_hash_table_size (priv->cpts) == 0) && (g_hash_table_size (priv->hints) == 0);
}

/**
 * asc_result_components_count:
 * @result: an #AscResult instance.
 *
 * Returns: The amount of components found for this unit.
 **/
guint
asc_result_components_count (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return g_hash_table_size (priv->cpts);
}

/**
 * asc_result_hints_count:
 * @result: an #AscResult instance.
 *
 * Returns: The amount of hints emitted for this unit.
 **/
guint
asc_result_hints_count (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	GHashTableIter iter;
	gpointer value;
	guint count = 0;

	g_hash_table_iter_init (&iter, priv->hints);
	while (g_hash_table_iter_next (&iter, NULL, &value))
		count += ((GPtrArray*) value)->len;
	return count;
}

/**
 * asc_result_get_bundle_kind:
 * @result: an #AscResult instance.
 *
 * Gets the bundle kind these results are for.
 **/
AsBundleKind
asc_result_get_bundle_kind (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return priv->bundle_kind;
}

/**
 * asc_result_set_bundle_kind:
 * @result: an #AscResult instance.
 *
 * Sets the kind of the bundle these results are for.
 **/
void
asc_result_set_bundle_kind (AscResult *result, AsBundleKind kind)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	priv->bundle_kind = kind;
}

/**
 * asc_result_get_bundle_id:
 * @result: an #AscResult instance.
 *
 * Gets the ID name of the bundle (a package / Flatpak / any entity containing metadata)
 * that these these results are generated for.
 **/
const gchar*
asc_result_get_bundle_id (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return priv->bundle_id;
}

/**
 * asc_result_set_bundle_id:
 * @result: an #AscResult instance.
 * @id: The new ID.
 *
 * Sets the name of the bundle these results are for.
 **/
void
asc_result_set_bundle_id (AscResult *result, const gchar *id)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	g_free (priv->bundle_id);
	priv->bundle_id = g_strdup (id);
}

/**
 * asc_result_get_component:
 * @result: an #AscResult instance.
 * @cid: Component ID to look for.
 *
 * Gets the component by its component-id-
 *
 * returns: (transfer none): An #AsComponent
 **/
AsComponent*
asc_result_get_component (AscResult *result, const gchar *cid)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return g_hash_table_lookup (priv->cpts, cid);
}

/**
 * asc_result_fetch_components:
 * @result: an #AscResult instance.
 *
 * Gets all components this #AsResult instance contains.
 *
 * Returns: (transfer container) (element-type AsComponent) : An array of #AsComponent
 **/
GPtrArray*
asc_result_fetch_components (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	GPtrArray *res;
	GHashTableIter iter;
	gpointer value;

	res = g_ptr_array_new_full (g_hash_table_size (priv->cpts),
				    g_object_unref);

	g_hash_table_iter_init (&iter, priv->cpts);
	while (g_hash_table_iter_next (&iter, NULL, &value))
		g_ptr_array_add (res, g_object_ref (AS_COMPONENT (value)));
	return res;
}

/**
 * asc_result_get_hints:
 * @result: an #AscResult instance.
 * @cid: Component ID to look for.
 *
 * Gets hints for a component with the given component-id.
 *
 * Returns: (transfer none) (element-type AscHint): An array of #AscHint or %NULL
 **/
GPtrArray*
asc_result_get_hints (AscResult *result, const gchar *cid)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return g_hash_table_lookup (priv->hints, cid);
}

/**
 * asc_result_get_component_ids_with_hints:
 * @result: an #AscResult instance.
 *
 * Gets list of component-IDs which do have issue hints associated with them.
 *
 * Returns: (transfer container): An array of component-IDs. Free container with %g_free
 **/
const gchar**
asc_result_get_component_ids_with_hints (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return (const gchar**) g_hash_table_get_keys_as_array (priv->hints, NULL);
}

/**
 * asc_result_update_component_gcid:
 * @result: an #AscResult instance.
 * @cpt: The #AsComponent to edit.
 * @bytes: (nullable): The data to include in the global component ID, or %NULL
 *
 * Update the global component ID for the given component.
 *
 * Returns: %TRUE if the component existed and was updated.
 **/
gboolean
asc_result_update_component_gcid (AscResult *result, AsComponent *cpt, GBytes *bytes)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	g_autofree gchar *gcid = NULL;
	gchar *hash = NULL;
	const gchar *data;
	gsize data_len;
	const gchar *old_hash;
	const gchar *cid = as_component_get_id (cpt);

	if (bytes == NULL) {
		data = "";
		data_len = strlen (data);
	} else {
		data = g_bytes_get_data (bytes, &data_len);
	}

	if (as_is_empty (cid)) {
		gcid = asc_build_component_global_id (cid, NULL);
		g_hash_table_insert (priv->gcids,
				     g_ref_string_new_intern (cid),
				     g_steal_pointer (&gcid));
		return TRUE;
	}
	if (!g_hash_table_contains (priv->cpts, cid))
		return FALSE;

	old_hash = g_hash_table_lookup (priv->mdata_hashes, cpt);
	if (old_hash == NULL) {
		hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, data, data_len);
	} else {
		gsize old_hash_len;
		g_autofree gchar *tmp = NULL;

		old_hash_len = strlen (old_hash);
		tmp = malloc(old_hash_len + data_len);
		memcpy (tmp, old_hash, old_hash_len);
		memcpy (tmp + old_hash_len, data, data_len);

		hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, tmp, old_hash_len + data_len);
	}

	g_hash_table_insert (priv->mdata_hashes, cpt, hash);
	gcid = asc_build_component_global_id (cid, hash);
	g_hash_table_insert (priv->gcids,
			     g_ref_string_new_intern (cid),
			     g_steal_pointer (&gcid));

	return TRUE;
}

/**
 * asc_result_update_component_gcid_with_string:
 * @result: an #AscResult instance.
 * @cpt: The #AsComponent to edit.
 * @data: (nullable): The data as string to include in the global component ID, or %NULL
 *
 * Update the global component ID for the given component.
 * This is a convenience method for %asc_result_update_component_gcid
 *
 * Returns: %TRUE if the component existed and was updated.
 **/
gboolean
asc_result_update_component_gcid_with_string (AscResult *result, AsComponent *cpt, const gchar *data)
{
	g_autoptr(GBytes) bytes = g_bytes_new_static (data? data : "", strlen (data? data : ""));
	return asc_result_update_component_gcid (result, cpt, bytes);
}

/**
 * asc_result_gcid_for_cid:
 * @result: an #AscResult instance.
 * @cid: Component ID to look for.
 *
 * Retrieve the global component-ID string for the given component-ID,
 * as long as component with the given ID is registered with this #AscResult.
 * Otherwise, %NULL is returned.
 */
const gchar*
asc_result_gcid_for_cid (AscResult *result, const gchar *cid)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	return (const gchar*) g_hash_table_lookup (priv->gcids, cid);
}

/**
 * asc_result_component_gcid_for_cid:
 * @result: an #AscResult instance.
 * @cpt: Component to look for.
 *
 * Retrieve the global component-ID string for the given #AsComponent,
 * as long as component with the given ID is registered with this #AscResult.
 * Otherwise, %NULL is returned.
 */
const gchar*
asc_result_gcid_for_component (AscResult *result, AsComponent *cpt)
{
	return asc_result_gcid_for_cid (result, as_component_get_id (cpt));
}

/**
 * asc_result_get_component_gcids:
 * @result: an #AscResult instance.
 *
 * Retrieve a list of all global component-IDs that this result knows of.
 *
 * Returns: (transfer container): An array of global component IDs. Free with %g_free
 */
const gchar**
asc_result_get_component_gcids (AscResult *result)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	const gchar **strv;
	GHashTableIter iter;
	gpointer value;
	guint i = 0;

	strv = g_new0 (const gchar *, g_hash_table_size (priv->gcids) + 1);
	g_hash_table_iter_init (&iter, priv->gcids);
	while (g_hash_table_iter_next (&iter, NULL, &value))
		strv[i++] = (const gchar *) value;
	return strv;
}

/**
 * asc_result_add_component:
 * @result: an #AscResult instance.
 * @cpt: The #AsComponent to add.
 * @bytes: Source data used to generate the GCID hash, or %NULL if nonexistent.
 * @error: A #GError or %NULL
 *
 * Add component to the results set.
 *
 * Returns: %TRUE on success.
 **/
gboolean
asc_result_add_component (AscResult *result, AsComponent *cpt, GBytes *bytes, GError **error)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	AsComponentKind ckind;
	const gchar *cid = as_component_get_id (cpt);

	if (as_is_empty (cid)) {
		g_set_error_literal (error,
				     ASC_COMPOSE_ERROR,
				     ASC_COMPOSE_ERROR_FAILED,
				     "Can not add component with empty ID to results set.");
		return FALSE;
	}


	/* web applications, operating systems, repositories
	 * and component-removal merges don't (need to) have a package/bundle name set */
	ckind = as_component_get_kind (cpt);
	if ((ckind != AS_COMPONENT_KIND_WEB_APP) &&
	    (ckind != AS_COMPONENT_KIND_OPERATING_SYSTEM) &&
	    (as_component_get_merge_kind (cpt) != AS_MERGE_KIND_REMOVE_COMPONENT)) {
		if (priv->bundle_kind == AS_BUNDLE_KIND_PACKAGE) {
			gchar *pkgnames[2] = {priv->bundle_id, NULL};
			as_component_set_pkgnames (cpt, pkgnames);
		} else if ((priv->bundle_kind != AS_BUNDLE_KIND_UNKNOWN) && (priv->bundle_kind >= AS_BUNDLE_KIND_LAST)) {
			g_autoptr(AsBundle) bundle = as_bundle_new ();
			as_bundle_set_kind (bundle, priv->bundle_kind);
			as_bundle_set_id (bundle, priv->bundle_id);
			as_component_add_bundle (cpt, bundle);
		}
        }

        g_hash_table_insert (priv->cpts,
			     g_ref_string_new_intern (cid),
			     g_object_ref (cpt));
	asc_result_update_component_gcid (result, cpt, bytes);
	return TRUE;
}

/**
 * asc_result_add_component_with_string:
 * @result: an #AscResult instance.
 * @cpt: The #AsComponent to add.
 * @data: Source data used to generate the GCID hash, or %NULL if nonexistent.
 * @error: A #GError or %NULL
 *
 * Add component to the results set, using string data.
 *
 * Returns: %TRUE on success.
 **/
gboolean
asc_result_add_component_with_string (AscResult *result, AsComponent *cpt, const gchar *data, GError **error)
{
	g_autoptr(GBytes) bytes = g_bytes_new_static (data? data : "", strlen (data? data : ""));
	return asc_result_add_component (result, cpt, bytes, error);
}

/**
 * asc_result_remove_component:
 * @result: an #AscResult instance.
 * @cpt: The #AsComponent to remove.
 *
 * Remove a component from the results set.
 *
 * Returns: %TRUE if the component was found and removed.
 **/
gboolean
asc_result_remove_component (AscResult *result, AsComponent *cpt)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	gboolean ret;

	ret = g_hash_table_remove (priv->cpts,
				   as_component_get_id (cpt));
	g_hash_table_remove (priv->gcids, as_component_get_id (cpt));
	g_hash_table_remove (priv->mdata_hashes, cpt);

	return ret;
}

/**
 * asc_result_remove_component_by_id:
 * @result: an #AscResult instance.
 * @cid: a component-ID
 *
 * Remove a component from the results set.
 *
 * Returns: %TRUE if the component was found and removed.
 **/
gboolean
asc_result_remove_component_by_id (AscResult *result, const gchar *cid)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	AsComponent *cpt;

	cpt = g_hash_table_lookup (priv->cpts, cid);
	if (cpt == NULL)
		return FALSE;
	return asc_result_remove_component (result, cpt);
}

static gboolean
asc_result_add_hint_va (AscResult *result, AsComponent *cpt, const gchar *component_id, const gchar *tag, const gchar *key1, va_list *args, gchar **args_v)
{
	AscResultPrivate *priv = GET_PRIVATE (result);
	const gchar *cur_key;
	const gchar *cur_val;
	g_autoptr(AscHint) hint = NULL;
	GPtrArray *hints = NULL;
	guint i;
	gboolean ret = TRUE;

	g_assert ((cpt == NULL) != (component_id == NULL));
	if (component_id == NULL) {
		g_assert (cpt != NULL);
		component_id = as_component_get_id (cpt);
	}

	hint = asc_hint_new_for_tag (tag, NULL);
	if (hint == NULL) {
		g_error ("Unable to find description of issue hint '%s' - this is a bug!", tag);
		return ret;
	}

	i = 1;
	cur_key = key1;
	while (cur_key != NULL) {
		if (args_v == NULL)
			cur_val = va_arg (*args, gchar*);
		else
			cur_val = args_v[i++];
		if (cur_val == NULL)
			cur_val = "";
		asc_hint_add_explanation_var (hint, cur_key, cur_val);

		if (args_v == NULL)
			cur_key = va_arg (*args, gchar*);
		else
			cur_key = args_v[i++];
	}

	hints = g_hash_table_lookup (priv->hints, component_id);
	if (hints == NULL) {
		hints = g_ptr_array_new_with_free_func (g_object_unref);
		g_hash_table_insert (priv->hints,
				     g_ref_string_new_intern (component_id),
				     hints);
	}

	/* we stop dealing with this component as soon as we encounter a fatal error. */
	if (asc_hint_is_error (hint)) {
		if (cpt == NULL)
			asc_result_remove_component_by_id (result, component_id);
		else
			asc_result_remove_component (result, cpt);
		ret = FALSE;
	}

	g_ptr_array_add (hints, g_steal_pointer (&hint));
	return ret;
}

/**
 * asc_result_add_hint_by_cid: (skip)
 * @result: an #AscResult instance.
 * @component_id: The component-ID of the affected #AsComponent
 * @tag: AppStream Compose Issue hint tag
 * @key1: First key to add a value for, or %NULL
 * @...: replacement keys and values for the issue explanation, terminated by %NULL
 *
 * Add an issue hint for a component.
 *
 * Returns: %TRUE if the added hint did not cause the component to be invalidated.
 **/
gboolean
asc_result_add_hint_by_cid (AscResult *result, const gchar *component_id, const gchar *tag, const gchar *key1, ...)
{
	va_list args;
	gboolean ret;

	va_start (args, key1);
	ret = asc_result_add_hint_va (result,
				      NULL,
				      component_id,
				      tag,
				      key1,
				      &args,
				      NULL);
	va_end (args);
	return ret;
}

/**
 * asc_result_add_hint_by_cid_v: (rename-to asc_result_add_hint_by_cid)
 * @result: an #AscResult instance.
 * @component_id: The component-ID of the affected #AsComponent
 * @tag: AppStream Compose Issue hint tag
 * @kv: List of key-value pairs for replacement variables.
 *
 * Add an issue hint for a component.
 *
 * Returns: %TRUE if the added hint did not cause the component to be invalidated.
 **/
gboolean
asc_result_add_hint_by_cid_v (AscResult *result, const gchar *component_id, const gchar *tag, gchar **kv)
{
	gboolean ret;
	const gchar *first_key = NULL;

	if (kv != NULL)
		first_key = kv[0];
	ret = asc_result_add_hint_va (result,
				      NULL,
				      component_id,
				      tag,
				      first_key,
				      NULL,
				      kv);
	return ret;
}

/**
 * asc_result_add_hint: (skip)
 * @result: an #AscResult instance.
 * @cpt: The affected #AsComponent
 * @tag: AppStream Compose Issue hint tag
 * @key1: First key to add a value for, or %NULL
 * @...: replacement keys and values for the issue explanation, terminated by %NULL
 *
 * Add an issue hint for a component.
 *
 * Returns: %TRUE if the added hint did not cause the component to be invalidated.
 **/
gboolean
asc_result_add_hint (AscResult *result, AsComponent *cpt, const gchar *tag, const gchar *key1, ...)
{
	va_list args;
	gboolean ret;

	if (cpt != NULL) {
		va_start (args, key1);
		ret = asc_result_add_hint_va (result,
					cpt,
					NULL,
					tag,
					key1,
					&args,
					NULL);
		va_end (args);
	} else {
		va_start (args, key1);
		ret = asc_result_add_hint_va (result,
					NULL,
					"general",
					tag,
					key1,
					&args,
					NULL);
		va_end (args);
	}

	return ret;
}

/**
 * asc_result_add_hint_simple: (skip)
 * @result: an #AscResult instance.
 * @cpt: The affected #AsComponent
 * @tag: AppStream Compose Issue hint tag
 *
 * Add an issue hint which does not have any variables to replace in its
 * explanation text for a component.
 *
 * Returns: %TRUE if the added hint did not cause the component to be invalidated.
 **/
gboolean
asc_result_add_hint_simple (AscResult *result, AsComponent *cpt, const gchar *tag)
{
	if (cpt != NULL) {
		return asc_result_add_hint_va (result,
						cpt,
						NULL,
						tag,
						NULL,
						NULL,
						NULL);
	} else {
		return asc_result_add_hint_va (result,
						NULL,
						"general",
						tag,
						NULL,
						NULL,
						NULL);
	}
}

/**
 * asc_result_add_hint_v: (rename-to asc_result_add_hint)
 * @result: an #AscResult instance.
 * @cpt: The affected #AsComponent
 * @tag: AppStream Compose Issue hint tag
 * @kv: List of key-value pairs for replacement variables.
 *
 * Add an issue hint for a component.
 *
 * Returns: %TRUE if the added hint did not cause the component to be invalidated.
 **/
gboolean
asc_result_add_hint_v (AscResult *result, AsComponent *cpt, const gchar *tag, gchar **kv)
{
	gboolean ret;
	const gchar *first_key = NULL;

	if (kv != NULL)
		first_key = kv[0];

	if (cpt != NULL) {
		ret = asc_result_add_hint_va (result,
					cpt,
					NULL,
					tag,
					first_key,
					NULL,
					kv);
	} else {
		ret = asc_result_add_hint_va (result,
						NULL,
						"general",
						tag,
						first_key,
						NULL,
						kv);
	}

	return ret;
}

/**
 * asc_result_new:
 *
 * Creates a new #AscResult.
 **/
AscResult*
asc_result_new (void)
{
	AscResult *result;
	result = g_object_new (ASC_TYPE_RESULT, NULL);
	return ASC_RESULT (result);
}
