/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vi:set noexpandtab tabstop=8 shiftwidth=8:
 *
 * Copyright (C) 2013 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"

#include <packagekit-glib2/packagekit.h>

#include <gnome-software.h>

#include "packagekit-common.h"

gboolean
gs_plugin_packagekit_error_convert (GError **error,
				    GCancellable *check_cancellable)
{
	GError *error_tmp;

	if (error == NULL)
		return FALSE;

	if (*error != NULL)
		g_dbus_error_strip_remote_error (*error);

	/* these are allowed for low-level errors */
	if (gs_utils_error_convert_gio (error))
		return TRUE;

	/* not set */
	error_tmp = *error;
	if (error_tmp == NULL)
		return FALSE;

	/* already correct */
	if (error_tmp->domain == GS_PLUGIN_ERROR)
		return TRUE;

	/* get a local version */
	if (error_tmp->domain != PK_CLIENT_ERROR)
		return FALSE;

	if (g_cancellable_is_cancelled (check_cancellable)) {
		error_tmp->domain = GS_PLUGIN_ERROR;
		error_tmp->code = GS_PLUGIN_ERROR_CANCELLED;
		return TRUE;
	}

	/* daemon errors */
	if (error_tmp->code <= 0xff) {
		switch (error_tmp->code) {
		case PK_CLIENT_ERROR_NOT_SUPPORTED:
			error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
			break;
		#if PK_CHECK_VERSION(1, 2, 4)
		case PK_CLIENT_ERROR_DECLINED_INTERACTION:
			error_tmp->code = GS_PLUGIN_ERROR_CANCELLED;
			break;
		#else
		case PK_CLIENT_ERROR_FAILED:
			/* The text is not localized on the PackageKit side and it uses a generic error code
			 * FIXME: This can be dropped when we depend on a
			 * PackageKit version which includes https://github.com/PackageKit/PackageKit/pull/497 */
			if (g_strcmp0 (error_tmp->message, "user declined interaction") == 0)
				error_tmp->code = GS_PLUGIN_ERROR_CANCELLED;
			else
				error_tmp->code = GS_PLUGIN_ERROR_FAILED;
			break;
		#endif
		/* this is working around a bug in libpackagekit-glib */
		case PK_ERROR_ENUM_TRANSACTION_CANCELLED:
			error_tmp->code = GS_PLUGIN_ERROR_CANCELLED;
			break;
		case PK_CLIENT_ERROR_CANNOT_START_DAEMON:
		case PK_CLIENT_ERROR_INVALID_FILE:
		default:
			error_tmp->code = GS_PLUGIN_ERROR_FAILED;
			break;
		}

	/* backend errors */
	} else {
		switch (error_tmp->code - 0xff) {
		case PK_ERROR_ENUM_NOT_SUPPORTED:
			error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
			break;
		case PK_ERROR_ENUM_NO_CACHE:
		case PK_ERROR_ENUM_NO_NETWORK:
			error_tmp->code = GS_PLUGIN_ERROR_NO_NETWORK;
			break;
		case PK_ERROR_ENUM_PACKAGE_DOWNLOAD_FAILED:
		case PK_ERROR_ENUM_NO_MORE_MIRRORS_TO_TRY:
		case PK_ERROR_ENUM_CANNOT_FETCH_SOURCES:
			error_tmp->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
			break;
		case PK_ERROR_ENUM_BAD_GPG_SIGNATURE:
		case PK_ERROR_ENUM_CANNOT_INSTALL_REPO_UNSIGNED:
		case PK_ERROR_ENUM_CANNOT_UPDATE_REPO_UNSIGNED:
		case PK_ERROR_ENUM_GPG_FAILURE:
		case PK_ERROR_ENUM_MISSING_GPG_SIGNATURE:
		case PK_ERROR_ENUM_NO_LICENSE_AGREEMENT:
		case PK_ERROR_ENUM_NOT_AUTHORIZED:
		case PK_ERROR_ENUM_RESTRICTED_DOWNLOAD:
			error_tmp->code = GS_PLUGIN_ERROR_NO_SECURITY;
			break;
		case PK_ERROR_ENUM_NO_SPACE_ON_DEVICE:
			error_tmp->code = GS_PLUGIN_ERROR_NO_SPACE;
			break;
		case PK_ERROR_ENUM_CANCELLED_PRIORITY:
		case PK_ERROR_ENUM_TRANSACTION_CANCELLED:
			error_tmp->code = GS_PLUGIN_ERROR_CANCELLED;
			break;
		case PK_ERROR_ENUM_INVALID_PACKAGE_FILE:
		case PK_ERROR_ENUM_PACKAGE_INSTALL_BLOCKED:
		default:
			error_tmp->code = GS_PLUGIN_ERROR_FAILED;
			break;
		}
	}
	error_tmp->domain = GS_PLUGIN_ERROR;
	return TRUE;
}

gboolean
gs_plugin_packagekit_results_valid (PkResults *results,
				    GCancellable *check_cancellable,
				    GError **error)
{
	g_autoptr(PkError) error_code = NULL;

	/* method failed? */
	if (results == NULL) {
		gs_plugin_packagekit_error_convert (error, check_cancellable);
		return FALSE;
	}

	/* check error code */
	error_code = pk_results_get_error_code (results);
	if (error_code != NULL) {
		g_set_error_literal (error,
				     PK_CLIENT_ERROR,
				     pk_error_get_code (error_code),
				     pk_error_get_details (error_code));
		gs_plugin_packagekit_error_convert (error, check_cancellable);
		return FALSE;
	}

	/* all good */
	return TRUE;
}

gboolean
gs_plugin_packagekit_add_results (GsPlugin *plugin,
				  GsAppList *list,
				  PkResults *results,
				  GError **error)
{
	const gchar *package_id;
	guint i;
	PkPackage *package;
	g_autoptr(GHashTable) installed = NULL;
	g_autoptr(PkError) error_code = NULL;
	g_autoptr(GPtrArray) array_filtered = NULL;
	g_autoptr(GPtrArray) array = NULL;

	g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
	g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);

	/* check error code */
	error_code = pk_results_get_error_code (results);
	if (error_code != NULL) {
		g_set_error (error,
			     GS_PLUGIN_ERROR,
			     GS_PLUGIN_ERROR_INVALID_FORMAT,
			     "failed to get-packages: %s, %s",
			     pk_error_enum_to_string (pk_error_get_code (error_code)),
			     pk_error_get_details (error_code));
		return FALSE;
	}

	/* add all installed packages to a hash */
	installed = g_hash_table_new (g_str_hash, g_str_equal);
	array = pk_results_get_package_array (results);
	for (i = 0; i < array->len; i++) {
		package = g_ptr_array_index (array, i);
		if (pk_package_get_info (package) != PK_INFO_ENUM_INSTALLED)
			continue;
		g_hash_table_insert (installed,
				     (const gpointer) pk_package_get_name (package),
				     (const gpointer) pk_package_get_id (package));
	}

	/* if the search returns more than one package with the same name,
	 * ignore everything with that name except the installed package */
	array_filtered = g_ptr_array_new ();
	for (i = 0; i < array->len; i++) {
		package = g_ptr_array_index (array, i);
		package_id = g_hash_table_lookup (installed, pk_package_get_name (package));
		if (pk_package_get_info (package) == PK_INFO_ENUM_INSTALLED || package_id == NULL) {
			g_ptr_array_add (array_filtered, package);
		} else {
			g_debug ("ignoring available %s as installed %s also reported",
				 pk_package_get_id (package), package_id);
		}
	}

	/* process packages */
	for (i = 0; i < array_filtered->len; i++) {
		g_autoptr(GsApp) app = NULL;
		GsAppState state = GS_APP_STATE_UNKNOWN;
		package = g_ptr_array_index (array_filtered, i);

		app = gs_plugin_cache_lookup (plugin, pk_package_get_id (package));
		if (app == NULL) {
			app = gs_app_new (NULL);
			gs_plugin_packagekit_set_packaging_format (plugin, app);
			gs_app_set_management_plugin (app, plugin);
			gs_app_add_source (app, pk_package_get_name (package));
			gs_app_add_source_id (app, pk_package_get_id (package));
			gs_plugin_packagekit_set_package_name (app, package);
			gs_plugin_cache_add (plugin, pk_package_get_id (package), app);
		}
		gs_app_set_name (app,
				 GS_APP_QUALITY_LOWEST,
				 pk_package_get_name (package));
		gs_app_set_summary (app,
				    GS_APP_QUALITY_LOWEST,
				    pk_package_get_summary (package));
		gs_app_set_metadata (app, "GnomeSoftware::Creator",
				     gs_plugin_get_name (plugin));
		gs_app_set_version (app, pk_package_get_version (package));
		switch (pk_package_get_info (package)) {
		case PK_INFO_ENUM_INSTALLED:
			state = GS_APP_STATE_INSTALLED;
			break;
		case PK_INFO_ENUM_AVAILABLE:
			state = GS_APP_STATE_AVAILABLE;
			break;
#if PK_CHECK_VERSION(1, 3, 0)
		case PK_INFO_ENUM_INSTALL:
#endif
		case PK_INFO_ENUM_INSTALLING:
		case PK_INFO_ENUM_UPDATING:
#if PK_CHECK_VERSION(1, 3, 0)
		case PK_INFO_ENUM_OBSOLETE:
		case PK_INFO_ENUM_DOWNGRADE:
#endif
		case PK_INFO_ENUM_DOWNGRADING:
		case PK_INFO_ENUM_OBSOLETING:
		case PK_INFO_ENUM_UNTRUSTED:
			break;
		case PK_INFO_ENUM_UNAVAILABLE:
#if PK_CHECK_VERSION(1, 3, 0)
		case PK_INFO_ENUM_REMOVE:
#endif
		case PK_INFO_ENUM_REMOVING:
			state =  GS_APP_STATE_UNAVAILABLE;
			break;
		default:
			g_warning ("unknown info state of %s",
				   pk_info_enum_to_string (pk_package_get_info (package)));
		}
		if (state != GS_APP_STATE_UNKNOWN && gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, state);
		if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN)
			gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC);
		gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
		gs_app_list_add (list, app);
	}
	return TRUE;
}

void
gs_plugin_packagekit_resolve_packages_app (GsPlugin *plugin,
					   GPtrArray *packages,
					   GsApp *app)
{
	GPtrArray *sources;
	PkPackage *package;
	const gchar *pkgname;
	guint i, j;
	guint number_available = 0;
	guint number_installed = 0;

	/* find any packages that match the package name */
	number_installed = 0;
	number_available = 0;
	sources = gs_app_get_sources (app);
	for (j = 0; j < sources->len; j++) {
		pkgname = g_ptr_array_index (sources, j);
		for (i = 0; i < packages->len; i++) {
			package = g_ptr_array_index (packages, i);
			if (g_strcmp0 (pk_package_get_name (package), pkgname) == 0) {
				gs_plugin_packagekit_set_metadata_from_package (plugin, app, package);
				switch (pk_package_get_info (package)) {
				case PK_INFO_ENUM_INSTALLED:
					number_installed++;
					break;
				case PK_INFO_ENUM_AVAILABLE:
					number_available++;
					break;
				case PK_INFO_ENUM_UNAVAILABLE:
					number_available++;
					break;
				default:
					/* should we expect anything else? */
					break;
				}
			}
		}
	}

	/* if *all* the source packages for the app are installed then the
	 * application is considered completely installed */
	if (number_installed == sources->len && number_available == 0) {
		if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, GS_APP_STATE_INSTALLED);
	} else if (number_installed + number_available == sources->len) {
		/* the appstream plugin can mark the app as installed, even if it is not installed,
		   when it only has the same app ID with another package (like differently built
		   drivers for the distribution, where each build has enabled different features) */
		if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
			gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
		/* if all the source packages are installed and all the rest
		 * of the packages are available then the app is available */
		if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
	} else if (number_installed + number_available > sources->len) {
		/* we have more packages returned than source packages */
		gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
		gs_app_set_state (app, GS_APP_STATE_UPDATABLE);
	} else if (number_installed + number_available < sources->len) {
		g_autofree gchar *tmp = NULL;
		/* we have less packages returned than source packages */
		tmp = gs_app_to_string (app);
		g_debug ("Failed to find all packages for:\n%s", tmp);
		gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
	}
}

void
gs_plugin_packagekit_set_metadata_from_package (GsPlugin *plugin,
                                                GsApp *app,
                                                PkPackage *package)
{
	const gchar *data;

	gs_plugin_packagekit_set_packaging_format (plugin, app);
	gs_app_set_management_plugin (app, plugin);
	gs_app_add_source (app, pk_package_get_name (package));
	gs_app_add_source_id (app, pk_package_get_id (package));
	gs_plugin_packagekit_set_package_name (app, package);

	/* set origin */
	if (gs_app_get_origin (app) == NULL) {
		data = pk_package_get_data (package);
		if (g_str_has_prefix (data, "installed:"))
			data += 10;
		gs_app_set_origin (app, data);
	}

	/* set unavailable state */
	if (pk_package_get_info (package) == PK_INFO_ENUM_UNAVAILABLE) {
		gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
		if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_UNKNOWN)
			gs_app_set_size_installed (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
		if (gs_app_get_size_download (app, NULL) == GS_SIZE_TYPE_UNKNOWN)
			gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
	} else if (pk_package_get_info (package) == PK_INFO_ENUM_AVAILABLE &&
		   gs_app_get_state (app) == GS_APP_STATE_UPDATABLE) {
		if (gs_app_get_update_version (app) == NULL)
			gs_app_set_update_version (app, pk_package_get_version (package));
	} else if (gs_app_get_version (app) == NULL) {
		gs_app_set_version (app, pk_package_get_version (package));
	}
	gs_app_set_name (app,
			 GS_APP_QUALITY_LOWEST,
			 pk_package_get_name (package));
	gs_app_set_summary (app,
			    GS_APP_QUALITY_LOWEST,
			    pk_package_get_summary (package));
}

/* Hash functions which compare PkPackageIds on NAME, VERSION and ARCH, but not DATA.
 * This is because some backends do not append the origin.
 *
 * Borrowing some implementation details from pk-package-id.c, a package
 * ID is a semicolon-separated list of NAME;[VERSION];[ARCH];[DATA],
 * so a comparison which ignores DATA is just a strncmp() up to and
 * including the final semicolon.
 *
 * Doing it this way means zero allocations, which allows the hash and
 * equality functions to be fast. This is important when dealing with
 * large refine() package lists.
 *
 * The hash and equality functions assume that the IDs they are passed are
 * valid. */
static guint
package_id_hash (gconstpointer key)
{
	const gchar *package_id = key;
	gchar *no_data;
	gsize i, last_semicolon = 0;

	/* find the last semicolon, which starts the DATA section */
	for (i = 0; package_id[i] != '\0'; i++) {
		if (package_id[i] == ';')
			last_semicolon = i;
	}

	/* exit early if the DATA section was empty */
	if (last_semicolon + 1 == i)
		return g_str_hash (package_id);

	/* extract up to (and including) the last semicolon into a local string */
	no_data = g_alloca (last_semicolon + 2);
	memcpy (no_data, package_id, last_semicolon + 1);
	no_data[last_semicolon + 1] = '\0';

	return g_str_hash (no_data);
}

static gboolean
package_id_equal (gconstpointer a,
                  gconstpointer b)
{
	const gchar *package_id_a = a;
	const gchar *package_id_b = b;
	gsize i, n_semicolons = 0;

	/* compare up to and including the last semicolon */
	for (i = 0; package_id_a[i] != '\0' && package_id_b[i] != '\0'; i++) {
		if (package_id_a[i] != package_id_b[i])
			return FALSE;
		if (package_id_a[i] == ';')
			n_semicolons++;
		if (n_semicolons == 4)
			return TRUE;
	}

	return package_id_a[i] == package_id_b[i];
}

GHashTable *
gs_plugin_packagekit_details_array_to_hash (GPtrArray *array)
{
	g_autoptr(GHashTable) details_collection = NULL;

	details_collection = g_hash_table_new_full (package_id_hash, package_id_equal,
						    NULL, NULL);

	for (gsize i = 0; i < array->len; i++) {
		PkDetails *details = g_ptr_array_index (array, i);
		g_hash_table_insert (details_collection,
				     (void *) pk_details_get_package_id (details),
				     details);
	}

	return g_steal_pointer (&details_collection);
}

void
gs_plugin_packagekit_refine_details_app (GsPlugin *plugin,
					 GHashTable *details_collection,
					 GHashTable *prepared_updates,
					 GsApp *app)
{
	GPtrArray *source_ids;
	PkDetails *details;
	const gchar *package_id;
	guint j;
	guint64 download_size = 0, install_size = 0;

	/* @source_ids can have as many as 200 elements (google-noto); typically
	 * it has 1 or 2
	 *
	 * @details_collection is typically a large list of apps in the
	 * repository, on the order of 400 or 700 apps */
	source_ids = gs_app_get_source_ids (app);
	for (j = 0; j < source_ids->len; j++) {
		guint64 download_sz;

		package_id = g_ptr_array_index (source_ids, j);
		details = g_hash_table_lookup (details_collection, package_id);
		if (details == NULL)
			continue;

		if (gs_app_get_license (app) == NULL &&
		    pk_details_get_license (details) != NULL &&
		    g_ascii_strcasecmp (pk_details_get_license (details), "unknown") != 0) {
			g_autofree gchar *license_spdx = NULL;
			license_spdx = as_license_to_spdx_id (pk_details_get_license (details));
			if (license_spdx != NULL && g_ascii_strcasecmp (license_spdx, "unknown") == 0) {
				g_clear_pointer (&license_spdx, g_free);
				license_spdx = g_strdup (pk_details_get_license (details));
				if (license_spdx != NULL)
					g_strstrip (license_spdx);
			}
			if (license_spdx != NULL) {
				gs_app_set_license (app,
						    GS_APP_QUALITY_LOWEST,
						    license_spdx);
			}
		}
		if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) {
			gs_app_set_url (app,
					AS_URL_KIND_HOMEPAGE,
					pk_details_get_url (details));
		}
		if (gs_app_get_description (app) == NULL) {
			gs_app_set_description (app,
			                        GS_APP_QUALITY_LOWEST,
			                        pk_details_get_description (details));
		}
		install_size += pk_details_get_size (details);
		download_sz = pk_details_get_download_size (details);

		/* If the package is already prepared as part of an offline
		 * update, no additional downloads need to be done. */
		if (download_sz != G_MAXUINT64 &&
		    !g_hash_table_contains (prepared_updates, package_id))
			download_size += download_sz;
	}

	/* the size is the size of all sources */
	if (gs_app_get_state (app) == GS_APP_STATE_UPDATABLE) {
		if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size);
		if (download_size > 0 && gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, download_size);
	} else if (gs_app_is_installed (app)) {
		if (gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
		if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size);
	} else {
		if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size);
		if (download_size > 0 && gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID)
			gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, download_size);
	}
}

void
gs_plugin_packagekit_set_packaging_format (GsPlugin *plugin, GsApp *app)
{
	if (gs_plugin_check_distro_id (plugin, "fedora") ||
	    gs_plugin_check_distro_id (plugin, "rhel")) {
		gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM");
	} else if (gs_plugin_check_distro_id (plugin, "debian") ||
	           gs_plugin_check_distro_id (plugin, "ubuntu")) {
		gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "deb");
	} else {
		return;
	}

	gs_app_set_metadata (app, "GnomeSoftware::PackagingBaseCssColor", "error_color");
}

void
gs_plugin_packagekit_set_package_name (GsApp *app,
				       PkPackage *package)
{
	g_autofree gchar *tmp = NULL;

	g_return_if_fail (GS_IS_APP (app));
	g_return_if_fail (PK_IS_PACKAGE (package));

	if (gs_app_get_metadata_item (app, "GnomeSoftware::packagename-value") != NULL)
		return;

	tmp = g_strdup_printf ("%s-%s.%s",
				pk_package_get_name (package),
				pk_package_get_version (package),
				pk_package_get_arch (package));
	gs_app_set_metadata (app, "GnomeSoftware::packagename-value", tmp);
}
