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

/**
 * SECTION:gs-app-list
 * @title: GsAppList
 * @include: gnome-software.h
 * @stability: Unstable
 * @short_description: An application list
 *
 * These functions provide a refcounted list of #GsApp objects.
 */

#include "config.h"

#include <glib.h>

#include "gs-app-private.h"
#include "gs-app-list-private.h"
#include "gs-app-collation.h"
#include "gs-enums.h"

struct _GsAppList
{
	GObject			 parent_instance;
	GPtrArray		*array;
	GMutex			 mutex;
	guint			 size_peak;
	GsAppListFlags		 flags;
	GsAppState		 state;
	guint			 progress;  /* 0–100 inclusive, or %GS_APP_PROGRESS_UNKNOWN */
	guint			 custom_progress; /* overrides the 'progress', if not %GS_APP_PROGRESS_UNKNOWN */
};

G_DEFINE_TYPE (GsAppList, gs_app_list, G_TYPE_OBJECT)

enum {
	PROP_STATE = 1,
	PROP_PROGRESS,
	PROP_LAST
};

enum {
	SIGNAL_APP_STATE_CHANGED,
	SIGNAL_LAST
};

static GParamSpec *properties [PROP_LAST];
static guint signals [SIGNAL_LAST] = { 0 };

/**
 * gs_app_list_get_state:
 * @list: A #GsAppList
 *
 * Gets the state of the list.
 *
 * This method will only return a valid result if gs_app_list_add_flag() has
 * been called with %GS_APP_LIST_FLAG_WATCH_APPS.
 *
 * Returns: the #GsAppState, e.g. %GS_APP_STATE_INSTALLED
 *
 * Since: 3.30
 **/
GsAppState
gs_app_list_get_state (GsAppList *list)
{
	g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_STATE_UNKNOWN);
	return list->state;
}

/**
 * gs_app_list_get_progress:
 * @list: A #GsAppList
 *
 * Gets the average percentage completion of all apps in the list. If any of the
 * apps in the list has progress %GS_APP_PROGRESS_UNKNOWN, or if the app list
 * is empty, %GS_APP_PROGRESS_UNKNOWN will be returned.
 *
 * This method will only return a valid result if gs_app_list_add_flag() has
 * been called with %GS_APP_LIST_FLAG_WATCH_APPS.
 *
 * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown
 *
 * Since: 3.30
 **/
guint
gs_app_list_get_progress (GsAppList *list)
{
	g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_PROGRESS_UNKNOWN);
	if (list->custom_progress != GS_APP_PROGRESS_UNKNOWN)
		return list->custom_progress;
	return list->progress;
}

static gboolean
app_list_notify_progress_idle_cb (gpointer user_data)
{
	GsAppList *list = user_data;

	g_object_notify_by_pspec (G_OBJECT (list), properties[PROP_PROGRESS]);
	g_object_unref (list);

	return G_SOURCE_REMOVE;
}

/**
 * gs_app_list_override_progress:
 * @list: a #GsAppList
 * @progress: a progress to set, between 0 and 100 inclusive, or %GS_APP_PROGRESS_UNKNOWN
 *
 * Override the progress property to be this value, or use %GS_APP_PROGRESS_UNKNOWN,
 * to unset the override. This can be used when only the overall progress is known,
 * instead of a per-application progress.
 *
 * Since: 42
 **/
void
gs_app_list_override_progress (GsAppList *list,
			       guint progress)
{
	g_return_if_fail (GS_IS_APP_LIST (list));

	if (list->custom_progress != progress) {
		list->custom_progress = progress;
		g_idle_add (app_list_notify_progress_idle_cb, g_object_ref (list));
	}
}

static void
gs_app_list_add_watched_for_app (GsAppList *list, GPtrArray *apps, GsApp *app)
{
	if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS)
		g_ptr_array_add (apps, app);
	if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_ADDONS) {
		g_autoptr(GsAppList) list2 = gs_app_dup_addons (app);

		for (guint i = 0; list2 != NULL && i < gs_app_list_length (list2); i++) {
			GsApp *app2 = gs_app_list_index (list2, i);
			g_ptr_array_add (apps, app2);
		}
	}
	if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_RELATED) {
		GsAppList *list2 = gs_app_get_related (app);
		for (guint i = 0; i < gs_app_list_length (list2); i++) {
			GsApp *app2 = gs_app_list_index (list2, i);
			g_ptr_array_add (apps, app2);
		}
	}
}

static GPtrArray *
gs_app_list_get_watched_for_app (GsAppList *list, GsApp *app)
{
	GPtrArray *apps = g_ptr_array_new ();
	gs_app_list_add_watched_for_app (list, apps, app);
	return apps;
}

static GPtrArray *
gs_app_list_get_watched (GsAppList *list)
{
	GPtrArray *apps = g_ptr_array_new ();
	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app_tmp = g_ptr_array_index (list->array, i);
		gs_app_list_add_watched_for_app (list, apps, app_tmp);
	}
	return apps;
}

static void
gs_app_list_invalidate_progress (GsAppList *self)
{
	guint progress = 0;
	g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self);

	/* find the average percentage complete of the list */
	if (apps->len > 0) {
		guint64 pc_cnt = 0;
		gboolean unknown_seen = FALSE;

		for (guint i = 0; i < apps->len; i++) {
			GsApp *app_tmp = g_ptr_array_index (apps, i);
			guint app_progress = gs_app_get_progress (app_tmp);

			if (app_progress == GS_APP_PROGRESS_UNKNOWN) {
				unknown_seen = TRUE;
				break;
			}
			pc_cnt += gs_app_get_progress (app_tmp);
		}

		progress = (!unknown_seen) ? pc_cnt / apps->len : GS_APP_PROGRESS_UNKNOWN;
	} else {
		progress = GS_APP_PROGRESS_UNKNOWN;
	}

	if (self->progress != progress) {
		self->progress = progress;
		g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PROGRESS]);
	}
}

static void
gs_app_list_invalidate_state (GsAppList *self)
{
	GsAppState state = GS_APP_STATE_UNKNOWN;
	g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self);

	/* find any action state of the list */
	for (guint i = 0; i < apps->len; i++) {
		GsApp *app_tmp = g_ptr_array_index (apps, i);
		GsAppState state_tmp = gs_app_get_state (app_tmp);
		if (state_tmp == GS_APP_STATE_DOWNLOADING ||
		    state_tmp == GS_APP_STATE_INSTALLING ||
		    state_tmp == GS_APP_STATE_REMOVING) {
			state = state_tmp;
			break;
		}
	}
	if (self->state != state) {
		self->state = state;
		g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
	}
}

static void
gs_app_list_progress_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self)
{
	gs_app_list_invalidate_progress (self);
}

static void
gs_app_list_state_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self)
{
	gs_app_list_invalidate_state (self);

	g_signal_emit (self, signals[SIGNAL_APP_STATE_CHANGED], 0, app);
}

static void
gs_app_list_maybe_watch_app (GsAppList *list, GsApp *app)
{
	g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app);
	for (guint i = 0; i < apps->len; i++) {
		GsApp *app_tmp = g_ptr_array_index (apps, i);
		g_signal_connect_object (app_tmp, "notify::progress",
					 G_CALLBACK (gs_app_list_progress_notify_cb),
					 list, 0);
		g_signal_connect_object (app_tmp, "notify::state",
					 G_CALLBACK (gs_app_list_state_notify_cb),
					 list, 0);
	}
}

static void
gs_app_list_maybe_unwatch_app (GsAppList *list, GsApp *app)
{
	g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app);
	for (guint i = 0; i < apps->len; i++) {
		GsApp *app_tmp = g_ptr_array_index (apps, i);
		g_signal_handlers_disconnect_by_data (app_tmp, list);
	}
}

/**
 * gs_app_list_get_size_peak:
 * @list: A #GsAppList
 *
 * Returns the largest size the list has ever been.
 *
 * Returns: integer
 *
 * Since: 3.24
 **/
guint
gs_app_list_get_size_peak (GsAppList *list)
{
	return list->size_peak;
}

/**
 * gs_app_list_set_size_peak:
 * @list: A #GsAppList
 * @size_peak: A value to set
 *
 * Sets the largest size the list has ever been.
 *
 * Since: 43
 **/
void
gs_app_list_set_size_peak (GsAppList *list,
			   guint size_peak)
{
	g_return_if_fail (GS_IS_APP_LIST (list));
	list->size_peak = size_peak;
}

static GsApp *
gs_app_list_lookup_safe (GsAppList *list, const gchar *unique_id)
{
	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app = g_ptr_array_index (list->array, i);
		if (as_utils_data_id_equal (gs_app_get_unique_id (app), unique_id))
			return app;
	}
	return NULL;
}

/**
 * gs_app_list_lookup:
 * @list: A #GsAppList
 * @unique_id: A unique_id
 *
 * Finds the first matching application in the list using the usual wildcard
 * rules allowed in unique_ids.
 *
 * Returns: (transfer none): a #GsApp, or %NULL if not found
 *
 * Since: 3.22
 **/
GsApp *
gs_app_list_lookup (GsAppList *list, const gchar *unique_id)
{
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&list->mutex);
	return gs_app_list_lookup_safe (list, unique_id);
}

/**
 * gs_app_list_has_flag:
 * @list: A #GsAppList
 * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED
 *
 * Gets if a specific flag is set.
 *
 * Returns: %TRUE if the flag is set
 *
 * Since: 3.24
 **/
gboolean
gs_app_list_has_flag (GsAppList *list, GsAppListFlags flag)
{
	return (list->flags & flag) > 0;
}

/**
 * gs_app_list_add_flag:
 * @list: A #GsAppList
 * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED
 *
 * Gets if a specific flag is set.
 *
 * Returns: %TRUE if the flag is set
 *
 * Since: 3.30
 **/
void
gs_app_list_add_flag (GsAppList *list, GsAppListFlags flag)
{
	if (list->flags & flag)
		return;
	list->flags |= flag;

	/* turn this on for existing apps */
	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app = g_ptr_array_index (list->array, i);
		gs_app_list_maybe_watch_app (list, app);
	}
}

static gboolean
gs_app_list_check_for_duplicate (GsAppList *list, GsApp *app)
{
	GsApp *app_old;
	const gchar *id;

	/* adding a wildcard */
	if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
		for (guint i = 0; i < list->array->len; i++) {
			GsApp *app_tmp = g_ptr_array_index (list->array, i);
			if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD))
				continue;
			/* not adding exactly the same wildcard */
			if (g_strcmp0 (gs_app_get_unique_id (app_tmp),
				       gs_app_get_unique_id (app)) == 0)
				return FALSE;
		}
		return TRUE;
	}

	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app_tmp = g_ptr_array_index (list->array, i);
		if (app_tmp == app)
			return FALSE;
	}

	/* does not exist */
	id = gs_app_get_unique_id (app);
	if (id == NULL) {
		/* not much else we can do... */
		return TRUE;
	}

	/* existing app is a wildcard */
	app_old = gs_app_list_lookup_safe (list, id);
	if (app_old == NULL)
		return TRUE;
	if (gs_app_has_quirk (app_old, GS_APP_QUIRK_IS_WILDCARD))
		return TRUE;

	/* already exists */
	return FALSE;
}

typedef enum {
	GS_APP_LIST_ADD_FLAG_NONE		= 0,
	GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE	= 1 << 0,
	GS_APP_LIST_ADD_FLAG_LAST
} GsAppListAddFlag;

static void
gs_app_list_add_safe (GsAppList *list, GsApp *app, GsAppListAddFlag flag)
{
	/* check for duplicate */
	if ((flag & GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE) > 0 &&
	    !gs_app_list_check_for_duplicate (list, app))
		return;

	/* just use the ref */
	gs_app_list_maybe_watch_app (list, app);
	g_ptr_array_add (list->array, g_object_ref (app));

	/* update the historical max */
	if (list->array->len > list->size_peak)
		list->size_peak = list->array->len;
}

/**
 * gs_app_list_add:
 * @list: A #GsAppList
 * @app: A #GsApp
 *
 * If the application does not already exist in the list then it is added,
 * incrementing the reference count.
 * If the application already exists then a warning is printed to the console.
 *
 * Applications that have the application ID lazy-loaded will always be added
 * to the list, and to clean these up the plugin loader will also call the
 * gs_app_list_filter_duplicates() method when all plugins have run.
 *
 * Since: 3.22
 **/
void
gs_app_list_add (GsAppList *list, GsApp *app)
{
	g_autoptr(GMutexLocker) locker = NULL;
	g_return_if_fail (GS_IS_APP_LIST (list));
	g_return_if_fail (GS_IS_APP (app));
	locker = g_mutex_locker_new (&list->mutex);
	gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE);

	/* recalculate global state */
	gs_app_list_invalidate_state (list);
	gs_app_list_invalidate_progress (list);
}

/**
 * gs_app_list_remove:
 * @list: A #GsAppList
 * @app: A #GsApp
 *
 * Removes an application from the list. If the application does not exist the
 * request is ignored.
 *
 * Returns: %TRUE if the app was removed, %FALSE if it did not exist in the @list
 * Since: 43
 **/
gboolean
gs_app_list_remove (GsAppList *list, GsApp *app)
{
	g_autoptr(GMutexLocker) locker = NULL;
	gboolean removed;

	g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
	g_return_val_if_fail (GS_IS_APP (app), FALSE);

	locker = g_mutex_locker_new (&list->mutex);
	removed = g_ptr_array_remove (list->array, app);
	if (removed) {
		gs_app_list_maybe_unwatch_app (list, app);

		/* recalculate global state */
		gs_app_list_invalidate_state (list);
		gs_app_list_invalidate_progress (list);
	}

	return removed;
}

/**
 * gs_app_list_add_list:
 * @list: A #GsAppList
 * @donor: Another #GsAppList
 *
 * Adds all the applications in @donor to @list.
 *
 * Since: 3.22
 **/
void
gs_app_list_add_list (GsAppList *list, GsAppList *donor)
{
	guint i;
	g_autoptr(GMutexLocker) locker = NULL;

	g_return_if_fail (GS_IS_APP_LIST (list));
	g_return_if_fail (GS_IS_APP_LIST (donor));
	g_return_if_fail (list != donor);

	locker = g_mutex_locker_new (&list->mutex);

	/* add each app */
	for (i = 0; i < donor->array->len; i++) {
		GsApp *app = gs_app_list_index (donor, i);
		gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE);
	}

	/* recalculate global state */
	gs_app_list_invalidate_state (list);
	gs_app_list_invalidate_progress (list);
}

/**
 * gs_app_list_index:
 * @list: A #GsAppList
 * @idx: An index into the list
 *
 * Gets an application at a specific position in the list.
 *
 * Returns: (transfer none): a #GsApp, or %NULL if invalid
 *
 * Since: 3.22
 **/
GsApp *
gs_app_list_index (GsAppList *list, guint idx)
{
	return GS_APP (g_ptr_array_index (list->array, idx));
}

/**
 * gs_app_list_length:
 * @list: A #GsAppList
 *
 * Gets the length of the application list.
 *
 * Returns: Integer
 *
 * Since: 3.22
 **/
guint
gs_app_list_length (GsAppList *list)
{
	g_return_val_if_fail (GS_IS_APP_LIST (list), 0);
	return list->array->len;
}

static void
gs_app_list_remove_all_safe (GsAppList *list)
{
	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app = g_ptr_array_index (list->array, i);
		gs_app_list_maybe_unwatch_app (list, app);
	}
	g_ptr_array_set_size (list->array, 0);
	gs_app_list_invalidate_state (list);
	gs_app_list_invalidate_progress (list);
}

/**
 * gs_app_list_remove_all:
 * @list: A #GsAppList
 *
 * Removes all applications from the list.
 *
 * Since: 3.22
 **/
void
gs_app_list_remove_all (GsAppList *list)
{
	g_autoptr(GMutexLocker) locker = NULL;
	g_return_if_fail (GS_IS_APP_LIST (list));
	locker = g_mutex_locker_new (&list->mutex);
	gs_app_list_remove_all_safe (list);
}

/**
 * gs_app_list_filter:
 * @list: A #GsAppList
 * @func: A #GsAppListFilterFunc
 * @user_data: the user pointer to pass to @func
 *
 * If func() returns TRUE for the GsApp, then the app is kept.
 *
 * Since: 3.22
 **/
void
gs_app_list_filter (GsAppList *list, GsAppListFilterFunc func, gpointer user_data)
{
	guint i;
	GsApp *app;
	g_autoptr(GsAppList) old = NULL;
	g_autoptr(GMutexLocker) locker = NULL;

	g_return_if_fail (GS_IS_APP_LIST (list));
	g_return_if_fail (func != NULL);

	locker = g_mutex_locker_new (&list->mutex);

	/* deep copy to a temp list and clear the current one */
	old = gs_app_list_copy (list);
	gs_app_list_remove_all_safe (list);

	/* see if any of the apps need filtering */
	for (i = 0; i < old->array->len; i++) {
		app = gs_app_list_index (old, i);
		if (func (app, user_data))
			gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE);
	}
}

typedef struct {
	GsAppListSortFunc	 func;
	gpointer		 user_data;
} GsAppListSortHelper;

static gint
gs_app_list_sort_cb (gconstpointer a, gconstpointer b, gpointer user_data)
{
	GsApp *app1 = GS_APP (*(GsApp **) a);
	GsApp *app2 = GS_APP (*(GsApp **) b);
	const GsAppListSortHelper *helper = (GsAppListSortHelper *) user_data;
	return helper->func (app1, app2, helper->user_data);
}

/**
 * gs_app_list_sort:
 * @list: A #GsAppList
 * @func: A #GsAppListSortFunc
 * @user_data: user data to pass to @func
 *
 * Sorts the application list.
 *
 * Since: 3.22
 **/
void
gs_app_list_sort (GsAppList *list, GsAppListSortFunc func, gpointer user_data)
{
	g_autoptr(GMutexLocker) locker = NULL;
	GsAppListSortHelper helper;
	g_return_if_fail (GS_IS_APP_LIST (list));
	locker = g_mutex_locker_new (&list->mutex);
	helper.func = func;
	helper.user_data = user_data;
	g_ptr_array_sort_with_data (list->array, gs_app_list_sort_cb, &helper);
}

/**
 * gs_app_list_truncate:
 * @list: A #GsAppList
 * @length: the new length
 *
 * Truncates the application list. It is an error if @length is larger than the
 * size of the list.
 *
 * Since: 3.24
 **/
void
gs_app_list_truncate (GsAppList *list, guint length)
{
	g_autoptr(GMutexLocker) locker = NULL;

	g_return_if_fail (GS_IS_APP_LIST (list));
	g_return_if_fail (length <= list->array->len);

	/* mark this list as unworthy */
	list->flags |= GS_APP_LIST_FLAG_IS_TRUNCATED;

	/* everything */
	if (length == 0) {
		gs_app_list_remove_all (list);
		return;
	}

	/* remove the apps in the positions larger than the length */
	locker = g_mutex_locker_new (&list->mutex);
	g_ptr_array_set_size (list->array, length);
}

/**
 * gs_app_list_randomize:
 * @list: A #GsAppList
 *
 * Randomize the order of the list, but don't change the order until
 * the next day.
 *
 * Since: 3.22
 **/
void
gs_app_list_randomize (GsAppList *list)
{
	GRand *rand;
	g_autoptr(GDateTime) date = NULL;
	g_autoptr(GMutexLocker) locker = NULL;

	g_return_if_fail (GS_IS_APP_LIST (list));

	locker = g_mutex_locker_new (&list->mutex);

	if (!gs_app_list_length (list))
		return;

	rand = g_rand_new ();
	date = g_date_time_new_now_utc ();
	g_rand_set_seed (rand, (guint32) g_date_time_get_day_of_year (date));

	/* Fisher–Yates shuffle of the array.
	 * See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */
	for (guint i = gs_app_list_length (list) - 1; i >= 1; i--) {
		gpointer tmp;
		guint j = g_rand_int_range (rand, 0, i + 1);

		tmp = list->array->pdata[i];
		list->array->pdata[i] = list->array->pdata[j];
		list->array->pdata[j] = tmp;
	}

	g_rand_free (rand);
}

static gboolean
gs_app_list_filter_app_is_better (GsApp *app, GsApp *found, GsAppListFilterFlags flags)
{
	/* optional 1st layer sort */
	if ((flags & GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED) > 0) {
		if (gs_app_is_installed (app) && !gs_app_is_installed (found))
			return TRUE;
		if (!gs_app_is_installed (app) && gs_app_is_installed (found))
			return FALSE;
	}

	/* 2nd layer, priority and bundle kind */
	if (gs_app_compare_priority (app, found) < 0)
		return TRUE;

	/* assume is worse */
	return FALSE;
}

static GPtrArray *
gs_app_list_filter_app_get_keys (GsApp *app, GsAppListFilterFlags flags)
{
	GPtrArray *keys = g_ptr_array_new_with_free_func (g_free);
	g_autoptr(GString) key = NULL;

	/* just use the unique ID */
	if (flags == GS_APP_LIST_FILTER_FLAG_NONE) {
		if (gs_app_get_unique_id (app) != NULL)
			g_ptr_array_add (keys, g_strdup (gs_app_get_unique_id (app)));
		return keys;
	}

	/* use the ID and any provided items */
	if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES) {
		GPtrArray *provided = gs_app_get_provided (app);
		g_ptr_array_add (keys, g_strdup (gs_app_get_id (app)));
		for (guint i = 0; i < provided->len; i++) {
			AsProvided *prov = g_ptr_array_index (provided, i);
			GPtrArray *items;
			if (as_provided_get_kind (prov) != AS_PROVIDED_KIND_ID)
				continue;
			items = as_provided_get_items (prov);
			for (guint j = 0; j < items->len; j++)
				g_ptr_array_add (keys, g_strdup (g_ptr_array_index (items, j)));
		}
		return keys;
	}

	/* specific compound type */
	key = g_string_new (NULL);
	if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID) {
		const gchar *tmp = gs_app_get_id (app);
		if (tmp != NULL)
			g_string_append (key, gs_app_get_id (app));
	}
	if (flags & GS_APP_LIST_FILTER_FLAG_KEY_DEFAULT_SOURCE) {
		const gchar *tmp = gs_app_get_default_source (app);
		if (tmp != NULL)
			g_string_append_printf (key, ":%s", tmp);
	}
	if (flags & GS_APP_LIST_FILTER_FLAG_KEY_VERSION) {
		const gchar *tmp = gs_app_get_version (app);
		if (tmp != NULL)
			g_string_append_printf (key, ":%s", tmp);
	}
	if (key->len == 0)
		return keys;
	g_ptr_array_add (keys, g_string_free (g_steal_pointer (&key), FALSE));
	return keys;
}

/**
 * gs_app_list_filter_duplicates:
 * @list: A #GsAppList
 * @flags: a #GsAppListFilterFlags, e.g. GS_APP_LIST_FILTER_KEY_ID
 *
 * Filter any duplicate applications from the list.
 *
 * Since: 3.22
 **/
void
gs_app_list_filter_duplicates (GsAppList *list, GsAppListFilterFlags flags)
{
	g_autoptr(GHashTable) hash = NULL;
	g_autoptr(GHashTable) kept_apps = NULL;
	g_autoptr(GsAppList) old = NULL;
	g_autoptr(GMutexLocker) locker = NULL;

	g_return_if_fail (GS_IS_APP_LIST (list));

	locker = g_mutex_locker_new (&list->mutex);

	/* a hash table to hold apps with unique app ids */
	hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	/* a hash table containing apps we want to keep */
	kept_apps = g_hash_table_new (g_direct_hash, g_direct_equal);

	for (guint i = 0; i < list->array->len; i++) {
		GsApp *app = gs_app_list_index (list, i);
		GsApp *found = NULL;
		g_autoptr(GPtrArray) keys = NULL;

		/* get all the keys used to identify this app */
		keys = gs_app_list_filter_app_get_keys (app, flags);
		for (guint j = 0; j < keys->len; j++) {
			const gchar *key = g_ptr_array_index (keys, j);
			found = g_hash_table_lookup (hash, key);
			if (found != NULL)
				break;
		}

		/* new app */
		if (found == NULL) {
			for (guint j = 0; j < keys->len; j++) {
				const gchar *key = g_ptr_array_index (keys, j);
				g_hash_table_insert (hash, g_strdup (key), app);
			}
			g_hash_table_add (kept_apps, app);
			continue;
		}

		/* better? */
		if (flags != GS_APP_LIST_FILTER_FLAG_NONE &&
		    gs_app_list_filter_app_is_better (app, found, flags)) {
			for (guint j = 0; j < keys->len; j++) {
				const gchar *key = g_ptr_array_index (keys, j);
				g_hash_table_insert (hash, g_strdup (key), app);
			}
			g_hash_table_remove (kept_apps, found);
			g_hash_table_add (kept_apps, app);
		}
	}

	/* deep copy to a temp list and clear the current one */
	old = gs_app_list_copy (list);
	gs_app_list_remove_all_safe (list);

	/* add back the apps we want to keep */
	for (guint i = 0; i < old->array->len; i++) {
		GsApp *app = gs_app_list_index (old, i);
		if (g_hash_table_contains (kept_apps, app)) {
			gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE);
			/* In case the same instance is in the 'list' multiple times */
			g_hash_table_remove (kept_apps, app);
		}
	}
}

/**
 * gs_app_list_copy:
 * @list: A #GsAppList
 *
 * Returns a deep copy of the application list.
 *
 * Returns: A newly allocated #GsAppList
 *
 * Since: 3.22
 **/
GsAppList *
gs_app_list_copy (GsAppList *list)
{
	GsAppList *new;
	guint i;

	g_return_val_if_fail (GS_IS_APP_LIST (list), NULL);

	new = gs_app_list_new ();
	for (i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);
		gs_app_list_add_safe (new, app, GS_APP_LIST_ADD_FLAG_NONE);
	}
	return new;
}

static void
gs_app_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
	GsAppList *self = GS_APP_LIST (object);
	switch (prop_id) {
	case PROP_STATE:
		g_value_set_enum (value, self->state);
		break;
	case PROP_PROGRESS:
		g_value_set_uint (value, self->progress);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_app_list_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	switch (prop_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_app_list_finalize (GObject *object)
{
	GsAppList *list = GS_APP_LIST (object);
	g_ptr_array_unref (list->array);
	g_mutex_clear (&list->mutex);
	G_OBJECT_CLASS (gs_app_list_parent_class)->finalize (object);
}

static void
gs_app_list_class_init (GsAppListClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->get_property = gs_app_list_get_property;
	object_class->set_property = gs_app_list_set_property;
	object_class->finalize = gs_app_list_finalize;

	/**
	 * GsAppList:state:
	 */
	properties[PROP_STATE] =
		g_param_spec_enum ("state", NULL, NULL,
				   GS_TYPE_APP_STATE,
				   GS_APP_STATE_UNKNOWN,
				   G_PARAM_READABLE);

	/**
	 * GsAppList:progress:
	 *
	 * A percentage (0–100, inclusive) indicating the progress through the
	 * current task on this app list. The value may otherwise be
	 * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide
	 * confidence interval on any app, or if the app list is empty.
	 */
	properties[PROP_PROGRESS] =
		g_param_spec_uint ("progress", NULL, NULL,
				   0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN,
				   G_PARAM_READABLE);

	g_object_class_install_properties (object_class, PROP_LAST, properties);

	/**
	 * GsAppList:app-state-changed:
	 * @app: a #GsApp
	 *
	 * Emitted when any of the internal #GsApp instances changes its state.
	 *
	 * Since: 3.40
	 */
	signals [SIGNAL_APP_STATE_CHANGED] =
		g_signal_new ("app-state-changed",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      0, NULL, NULL, g_cclosure_marshal_generic,
			      G_TYPE_NONE, 1, GS_TYPE_APP);
}

static void
gs_app_list_init (GsAppList *list)
{
	g_mutex_init (&list->mutex);
	list->array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	list->custom_progress = GS_APP_PROGRESS_UNKNOWN;
}

/**
 * gs_app_list_new:
 *
 * Creates a new list.
 *
 * Returns: A newly allocated #GsAppList
 *
 * Since: 3.22
 **/
GsAppList *
gs_app_list_new (void)
{
	GsAppList *list;
	list = g_object_new (GS_TYPE_APP_LIST, NULL);
	return GS_APP_LIST (list);
}
