/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2018-2019 Kalev Lember <klember@redhat.com>
 *
 * SPDX-License-Identifier: GPL-2.0+
 */

#include "config.h"

#include <gnome-software.h>

#include "gs-appstream.h"

#define	GS_APPSTREAM_MAX_SCREENSHOTS	5

GsApp *
gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GError **error)
{
	GsApp *app;
	g_autoptr(GsApp) app_new = gs_app_new (NULL);

	/* refine enough to get the unique ID */
	if (!gs_appstream_refine_app (plugin, app_new, silo, component,
				      GS_PLUGIN_REFINE_FLAGS_DEFAULT,
				      error))
		return NULL;

	/* never add wildcard apps to the plugin cache */
	if (gs_app_has_quirk (app_new, GS_APP_QUIRK_IS_WILDCARD))
		return g_steal_pointer (&app_new);

	/* look for existing object */
	app = gs_plugin_cache_lookup (plugin, gs_app_get_unique_id (app_new));
	if (app != NULL)
		return app;

	/* use the temp object we just created */
	gs_app_set_metadata (app_new, "GnomeSoftware::Creator",
			     gs_plugin_get_name (plugin));
	gs_plugin_cache_add (plugin, NULL, app_new);
	return g_steal_pointer (&app_new);
}

static gchar *
gs_appstream_format_description (XbNode *root, GError **error)
{
	g_autoptr(GString) str = g_string_new (NULL);
	g_autoptr(XbNode) n = xb_node_get_child (root);

	while (n != NULL) {
		g_autoptr(XbNode) n2 = NULL;

		/* support <p>, <ul>, <ol> and <li>, ignore all else */
		if (g_strcmp0 (xb_node_get_element (n), "p") == 0) {
			g_string_append_printf (str, "%s\n\n", xb_node_get_text (n));
		} else if (g_strcmp0 (xb_node_get_element (n), "ul") == 0) {
			g_autoptr(GPtrArray) children = xb_node_get_children (n);
			for (guint i = 0; i < children->len; i++) {
				XbNode *nc = g_ptr_array_index (children, i);
				if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) {
					g_string_append_printf (str, " • %s\n",
								xb_node_get_text (nc));
				}
			}
			g_string_append (str, "\n");
		} else if (g_strcmp0 (xb_node_get_element (n), "ol") == 0) {
			g_autoptr(GPtrArray) children = xb_node_get_children (n);
			for (guint i = 0; i < children->len; i++) {
				XbNode *nc = g_ptr_array_index (children, i);
				if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) {
					g_string_append_printf (str, " %u. %s\n",
								i + 1,
								xb_node_get_text (nc));
				}
			}
			g_string_append (str, "\n");
		}

		n2 = xb_node_get_next (n);
		g_set_object (&n, n2);
	}

	/* remove extra newlines */
	while (str->len > 0 && str->str[str->len - 1] == '\n')
		g_string_truncate (str, str->len - 1);

	/* success */
	return g_string_free (g_steal_pointer (&str), FALSE);
}

static gchar *
gs_appstream_build_icon_prefix (XbNode *component)
{
	const gchar *origin;
	const gchar *tmp;
	gint npath;
	g_auto(GStrv) path = NULL;
	g_autoptr(XbNode) components = NULL;

	/* no parent, e.g. AppData */
	components = xb_node_get_parent (component);
	if (components == NULL)
		return NULL;

	/* set explicitly */
	tmp = xb_node_query_text (components, "info/icon-prefix", NULL);
	if (tmp != NULL)
		return g_strdup (tmp);

	/* fall back to origin */
	origin = xb_node_get_attr (components, "origin");
	if (origin == NULL)
		return NULL;

	/* no metadata */
	tmp = xb_node_query_text (components, "info/filename", NULL);
	if (tmp == NULL)
		return NULL;

	/* check format */
	path = g_strsplit (tmp, "/", -1);
	npath = g_strv_length (path);
	if (npath < 3 || !(g_strcmp0 (path[npath-2], "xmls") == 0 || g_strcmp0 (path[npath-2], "yaml") == 0))
		return NULL;

	/* fix the new path */
	g_free (path[npath-1]);
	g_free (path[npath-2]);
	path[npath-1] = g_strdup (origin);
	path[npath-2] = g_strdup ("icons");
	return g_strjoinv ("/", path);
}

static AsIcon *
gs_appstream_new_icon (XbNode *component, XbNode *n, AsIconKind icon_kind, guint sz)
{
	AsIcon *icon = as_icon_new ();
	g_autofree gchar *icon_path = NULL;
	as_icon_set_kind (icon, icon_kind);
	switch (icon_kind) {
	case AS_ICON_KIND_REMOTE:
		as_icon_set_url (icon, xb_node_get_text (n));
		break;
	default:
		as_icon_set_name (icon, xb_node_get_text (n));
	}
	if (sz == 0)
		sz = xb_node_get_attr_as_uint (n, "width");
	if (sz > 0) {
		as_icon_set_width (icon, sz);
		as_icon_set_height (icon, sz);
	}
	icon_path = gs_appstream_build_icon_prefix (component);
	if (icon_path != NULL)
		as_icon_set_prefix (icon, icon_path);
	return icon;
}

static AsIcon *
gs_appstream_get_icon_by_kind (XbNode *component, AsIconKind icon_kind)
{
	g_autofree gchar *xpath = NULL;
	g_autoptr(XbNode) icon = NULL;

	xpath = g_strdup_printf ("icon[@type='%s']",
				 as_icon_kind_to_string (icon_kind));
	icon = xb_node_query_first (component, xpath, NULL);
	if (icon == NULL)
		return NULL;
	return gs_appstream_new_icon (component, icon, icon_kind, 0);
}

static AsIcon *
gs_appstream_get_icon_by_kind_and_size (XbNode *component, AsIconKind icon_kind, guint sz)
{
	g_autofree gchar *xpath = NULL;
	g_autoptr(XbNode) icon = NULL;

	xpath = g_strdup_printf ("icon[@type='%s'][@height='%u'][@width='%u']",
				 as_icon_kind_to_string (icon_kind), sz, sz);
	icon = xb_node_query_first (component, xpath, NULL);
	if (icon == NULL)
		return NULL;
	return gs_appstream_new_icon (component, icon, icon_kind, sz);
}

static void
gs_appstream_refine_icon (GsPlugin *plugin, GsApp *app, XbNode *component)
{
	g_autoptr(AsIcon) icon = NULL;
	g_autoptr(XbNode) n = NULL;

	/* try a stock icon first */
	icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_STOCK);
	if (icon != NULL) {
		/* the stock icon referenced by the AppStream data may not be present in the current
		 * theme (usually more stock icon entries are added to permit huge themes like Papirus
		 * to style all apps in the software center). Since we can not rely on the icon's presence,
		 * we also add other icons to the list and do not return here. */
		gs_app_add_icon (app, icon);
	}

	/* if HiDPI get a 128px cached icon */
	if (gs_plugin_get_scale (plugin) == 2) {
		icon = gs_appstream_get_icon_by_kind_and_size (component,
							       AS_ICON_KIND_CACHED,
							       128);
		if (icon != NULL) {
			gs_app_add_icon (app, icon);
			return;
		}
	}

	/* non-HiDPI cached icon */
	icon = gs_appstream_get_icon_by_kind_and_size (component,
						       AS_ICON_KIND_CACHED,
						       64);
	if (icon != NULL) {
		gs_app_add_icon (app, icon);
		return;
	}

	/* prefer local */
	icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_LOCAL);
	if (icon != NULL) {
		/* does not exist, so try to find using the icon theme */
		if (as_icon_get_kind (icon) == AS_ICON_KIND_LOCAL &&
		    as_icon_get_filename (icon) == NULL) {
			g_debug ("converting missing LOCAL icon %s to STOCK",
				 as_icon_get_name (icon));
			as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
		}
		gs_app_add_icon (app, icon);
		return;
	}

	/* remote URL */
	icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_REMOTE);
	if (icon != NULL) {
		gs_app_add_icon (app, icon);
		return;
	}

	/* assume a stock icon */
	n = xb_node_query_first (component, "icon", NULL);
	if (n != NULL) {
		icon = gs_appstream_new_icon (component, n, AS_ICON_KIND_STOCK, 0);
		gs_app_add_icon (app, icon);
	}
}

static gboolean
gs_appstream_refine_add_addons (GsPlugin *plugin,
				GsApp *app,
				XbSilo *silo,
				GError **error)
{
	g_autofree gchar *xpath = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) addons = NULL;

	/* get all components */
	xpath = g_strdup_printf ("components/component/extends[text()='%s']/..",
				 gs_app_get_id (app));
	addons = xb_silo_query (silo, xpath, 0, &error_local);
	if (addons == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < addons->len; i++) {
		XbNode *addon = g_ptr_array_index (addons, i);
		g_autoptr(GsApp) app2 = NULL;
		app2 = gs_appstream_create_app (plugin, silo, addon, error);
		if (app2 == NULL)
			return FALSE;
		gs_app_add_addon (app, app2);
	}
	return TRUE;
}

static gboolean
gs_appstream_refine_add_images (GsApp *app, AsScreenshot *ss, XbNode *screenshot, GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) images = NULL;

	/* get all components */
	images = xb_node_query (screenshot, "image", 0, &error_local);
	if (images == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < images->len; i++) {
		XbNode *image = g_ptr_array_index (images, i);
		g_autoptr(AsImage) im = as_image_new ();
		as_image_set_height (im, xb_node_get_attr_as_uint (image, "height"));
		as_image_set_width (im, xb_node_get_attr_as_uint (image, "width"));
		as_image_set_kind (im, as_image_kind_from_string (xb_node_get_attr (image, "type")));
		as_image_set_url (im, xb_node_get_text (image));
		as_screenshot_add_image (ss, im);
	}

	/* success */
	return TRUE;
}

static gboolean
gs_appstream_refine_add_screenshots (GsApp *app, XbNode *component, GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) screenshots = NULL;

	/* get all components */
	screenshots = xb_node_query (component, "screenshots/screenshot", 0, &error_local);
	if (screenshots == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < screenshots->len; i++) {
		XbNode *screenshot = g_ptr_array_index (screenshots, i);
		g_autoptr(AsScreenshot) ss = as_screenshot_new ();
		if (!gs_appstream_refine_add_images (app, ss, screenshot, error))
			return FALSE;
		gs_app_add_screenshot (app, ss);
	}

	/* FIXME: move into no refine flags section? */
	if (screenshots ->len > 0)
		gs_app_add_kudo (app, GS_APP_KUDO_HAS_SCREENSHOTS);

	/* success */
	return TRUE;
}

static gboolean
gs_appstream_refine_add_provides (GsApp *app, XbNode *component, GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) provides = NULL;

	/* get all components */
	provides = xb_node_query (component, "provides/*", 0, &error_local);
	if (provides == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < provides->len; i++) {
		XbNode *provide = g_ptr_array_index (provides, i);
		g_autoptr(AsProvide) pr = as_provide_new ();
		as_provide_set_kind (pr, as_provide_kind_from_string (xb_node_get_element (provide)));
		as_provide_set_value (pr, xb_node_get_text (provide));
		gs_app_add_provide (app, pr);
	}

	/* success */
	return TRUE;
}

static gboolean
gs_appstream_is_recent_release (XbNode *component)
{
	guint64 ts;
	guint64 secs;

	/* get newest release */
	ts = xb_node_query_attr_as_uint (component, "releases/release", "timestamp", NULL);
	if (ts == G_MAXUINT64)
		return FALSE;

	/* is last build less than one year ago? */
	secs = ((guint64) g_get_real_time () / G_USEC_PER_SEC) - ts;
	return secs / (60 * 60 * 24) < 365;
}

static gboolean
gs_appstream_copy_metadata (GsApp *app, XbNode *component, GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) values = NULL;

	/* get all components */
	values = xb_node_query (component, "custom/value", 0, &error_local);
	if (values == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < values->len; i++) {
		XbNode *value = g_ptr_array_index (values, i);
		const gchar *key = xb_node_get_attr (value, "key");
		if (key == NULL)
			continue;
		if (gs_app_get_metadata_item (app, key) != NULL)
			continue;
		gs_app_set_metadata (app, key, xb_node_get_text (value));
	}
	return TRUE;
}

static gboolean
gs_appstream_refine_app_updates (GsPlugin *plugin,
				 GsApp *app,
				 XbSilo *silo,
				 XbNode *component,
				 GError **error)
{
	AsUrgencyKind urgency_best = AS_URGENCY_KIND_UNKNOWN;
	g_autofree gchar *xpath = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GHashTable) installed = g_hash_table_new (g_str_hash, g_str_equal);
	g_autoptr(GPtrArray) releases_inst = NULL;
	g_autoptr(GPtrArray) releases = NULL;
	g_autoptr(GPtrArray) updates_list = g_ptr_array_new ();

	/* only for UPDATABLE apps */
	if (!gs_app_is_updatable (app))
		return TRUE;

	/* find out which releases are already installed */
	xpath = g_strdup_printf ("component/id[text()='%s']/../releases/*[@version]",
				 gs_app_get_id (app));
	releases_inst = xb_silo_query (silo, xpath, 0, &error_local);
	if (releases_inst == NULL) {
		if (!g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) {
			g_propagate_error (error, g_steal_pointer (&error_local));
			return FALSE;
		}
	} else {
		for (guint i = 0; i < releases_inst->len; i++) {
			XbNode *release = g_ptr_array_index (releases_inst, i);
			g_hash_table_insert (installed,
					     (gpointer) xb_node_get_attr (release, "version"),
					     (gpointer) release);
		}
	}
	g_clear_error (&error_local);

	/* get all components */
	releases = xb_node_query (component, "releases/*", 0, &error_local);
	if (releases == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < releases->len; i++) {
		XbNode *release = g_ptr_array_index (releases, i);
		const gchar *version = xb_node_get_attr (release, "version");
		g_autoptr(XbNode) description = NULL;
		AsUrgencyKind urgency_tmp;

		/* ignore releases with no version */
		if (version == NULL)
			continue;

		/* already installed */
		if (g_hash_table_lookup (installed, version) != NULL)
			continue;

		/* limit this to three versions backwards if there has never
		 * been a detected installed version */
		if (g_hash_table_size (installed) == 0 && i >= 3)
			break;

		/* use the 'worst' urgency, e.g. critical over enhancement */
		urgency_tmp = as_urgency_kind_from_string (xb_node_get_attr (release, "urgency"));
		if (urgency_tmp > urgency_best)
			urgency_best = urgency_tmp;

		/* add updates with a description */
		description = xb_node_query_first (release, "description", NULL);
		if (description == NULL)
			continue;
		g_ptr_array_add (updates_list, release);
	}

	/* only set if known */
	if (urgency_best != AS_URGENCY_KIND_UNKNOWN)
		gs_app_set_update_urgency (app, urgency_best);

	/* no prefix on each release */
	if (updates_list->len == 1) {
		XbNode *release = g_ptr_array_index (updates_list, 0);
		g_autoptr(XbNode) n = NULL;
		g_autofree gchar *desc = NULL;
		n = xb_node_query_first (release, "description", NULL);
		desc = gs_appstream_format_description (n, NULL);
		gs_app_set_update_details (app, desc);

	/* get the descriptions with a version prefix */
	} else if (updates_list->len > 1) {
		g_autoptr(GString) update_desc = g_string_new ("");
		for (guint i = 0; i < updates_list->len; i++) {
			XbNode *release = g_ptr_array_index (updates_list, i);
			g_autofree gchar *desc = NULL;
			g_autoptr(XbNode) n = NULL;

			n = xb_node_query_first (release, "description", NULL);
			desc = gs_appstream_format_description (n, NULL);
			g_string_append_printf (update_desc,
						"Version %s:\n%s\n\n",
						xb_node_get_attr (release, "version"),
						desc);
		}

		/* remove trailing newlines */
		if (update_desc->len > 2)
			g_string_truncate (update_desc, update_desc->len - 2);
		gs_app_set_update_details (app, update_desc->str);
	}

	/* if there is no already set update version use the newest */
	if (gs_app_get_update_version (app) == NULL &&
	    updates_list->len > 0) {
		XbNode *release = g_ptr_array_index (updates_list, 0);
		gs_app_set_update_version (app, xb_node_get_attr (release, "version"));
	}

	/* success */
	return TRUE;
}

/**
 * _gs_utils_locale_has_translations:
 * @locale: A locale, e.g. `en_GB` or `uz_UZ.utf8@cyrillic`
 *
 * Looks up if the locale is likely to have translations.
 *
 * Returns: %TRUE if the locale should have translations
 **/
static gboolean
_gs_utils_locale_has_translations (const gchar *locale)
{
	g_autofree gchar *locale_copy = g_strdup (locale);
	gchar *separator;

	/* Strip off the codeset and modifier, if present. */
	separator = strpbrk (locale_copy, ".@");
	if (separator != NULL)
		*separator = '\0';

	if (g_strcmp0 (locale_copy, "C") == 0)
		return FALSE;
	if (g_strcmp0 (locale_copy, "en") == 0)
		return FALSE;
	if (g_strcmp0 (locale_copy, "en_US") == 0)
		return FALSE;
	return TRUE;
}

static gboolean
gs_appstream_origin_valid (const gchar *origin)
{
	if (origin == NULL)
		return FALSE;
	if (g_strcmp0 (origin, "") == 0)
		return FALSE;
	return TRUE;
}

static gboolean
gs_appstream_is_valid_project_group (const gchar *project_group)
{
	if (project_group == NULL)
		return FALSE;
	return as_utils_is_environment_id (project_group);
}

static gboolean
gs_appstream_refine_app_content_rating (GsPlugin *plugin,
					GsApp *app,
					XbNode *content_rating,
					GError **error)
{
	g_autoptr(AsContentRating) cr = as_content_rating_new ();
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) content_attributes = NULL;
	const gchar *content_rating_kind = NULL;

	/* get kind */
	content_rating_kind = xb_node_get_attr (content_rating, "type");
	/* we only really expect/support OARS 1.0 and 1.1 */
	if (content_rating_kind == NULL ||
	    (g_strcmp0 (content_rating_kind, "oars-1.0") != 0 &&
	     g_strcmp0 (content_rating_kind, "oars-1.1") != 0)) {
		return TRUE;
	}

	as_content_rating_set_kind (cr, content_rating_kind);

	/* get attributes; no attributes being found (i.e.
	 * `<content_rating type="*"/>`) is OK: it means that all attributes have
	 * value `none`, as per the
	 * [OARS semantics](https://github.com/hughsie/oars/blob/master/specification/oars-1.1.md) */
	content_attributes = xb_node_query (content_rating, "content_attribute", 0, &error_local);
	if (content_attributes == NULL &&
	    g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
		g_clear_error (&error_local);
	} else if (content_attributes == NULL &&
		 g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) {
		return TRUE;
	} else if (content_attributes == NULL) {
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}

	for (guint i = 0; content_attributes != NULL && i < content_attributes->len; i++) {
		XbNode *content_attribute = g_ptr_array_index (content_attributes, i);
		as_content_rating_add_attribute (cr,
						 xb_node_get_attr (content_attribute, "id"),
						 as_content_rating_value_from_string (xb_node_get_text (content_attribute)));
	}

	gs_app_set_content_rating (app, cr);
	return TRUE;
}

static gboolean
gs_appstream_refine_app_content_ratings (GsPlugin *plugin,
					 GsApp *app,
					 XbNode *component,
					 GError **error)
{
	g_autoptr(GPtrArray) content_ratings = NULL;
	g_autoptr(GError) error_local = NULL;

	/* find any content ratings */
	content_ratings = xb_node_query (component, "content_rating", 0, &error_local);
	if (content_ratings == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < content_ratings->len; i++) {
		XbNode *content_rating = g_ptr_array_index (content_ratings, i);
		if (!gs_appstream_refine_app_content_rating (plugin, app, content_rating, error))
			return FALSE;
	}
	return TRUE;
}

gboolean
gs_appstream_refine_app (GsPlugin *plugin,
			 GsApp *app,
			 XbSilo *silo,
			 XbNode *component,
			 GsPluginRefineFlags refine_flags,
			 GError **error)
{
	const gchar *tmp;
	g_autoptr(GPtrArray) bundles = NULL;
	g_autoptr(GPtrArray) launchables = NULL;
	g_autoptr(XbNode) req = NULL;

	/* is compatible */
	req = xb_node_query_first (component,
				   "requires/id[@type='id']"
				   "[text()='org.gnome.Software.desktop']", NULL);
	if (req != NULL) {
#if AS_CHECK_VERSION(0,7,15)
		gint rc = as_utils_vercmp_full (xb_node_get_attr (req, "version"),
		                                PACKAGE_VERSION,
		                                AS_VERSION_COMPARE_FLAG_NONE);
#else
		gint rc = as_utils_vercmp (xb_node_get_attr (req, "version"),
		                           PACKAGE_VERSION);
#endif
		if (rc > 0) {
			g_set_error (error,
				     GS_PLUGIN_ERROR,
				     GS_PLUGIN_ERROR_NOT_SUPPORTED,
				     "not for this gnome-software");
			return FALSE;
		}
	}

	/* types we can never launch */
	switch (gs_app_get_kind (app)) {
	case AS_APP_KIND_ADDON:
	case AS_APP_KIND_CODEC:
	case AS_APP_KIND_DRIVER:
	case AS_APP_KIND_FIRMWARE:
	case AS_APP_KIND_FONT:
	case AS_APP_KIND_GENERIC:
	case AS_APP_KIND_INPUT_METHOD:
	case AS_APP_KIND_LOCALIZATION:
	case AS_APP_KIND_OS_UPDATE:
	case AS_APP_KIND_OS_UPGRADE:
	case AS_APP_KIND_RUNTIME:
	case AS_APP_KIND_SOURCE:
		gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
		break;
	default:
		break;
	}

	/* check if the special metadata affects the not-launchable quirk */
	tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::not-launchable");
	if (tmp != NULL) {
		if (g_strcmp0 (tmp, "true") == 0)
			gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
		else if (g_strcmp0 (tmp, "false") == 0)
			gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
	}

	tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::hide-everywhere");
	if (tmp != NULL) {
		if (g_strcmp0 (tmp, "true") == 0)
			gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
		else if (g_strcmp0 (tmp, "false") == 0)
			gs_app_remove_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
	}

	/* try to detect old-style AppStream 'override'
	 * files without the merge attribute */
	if (xb_node_query_text (component, "name", NULL) == NULL &&
	    xb_node_query_text (component, "metadata_license", NULL) == NULL) {
		gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
	}

	/* set id */
	tmp = xb_node_query_text (component, "id", NULL);
	if (tmp != NULL && gs_app_get_id (app) == NULL)
		gs_app_set_id (app, tmp);

	/* set source */
	tmp = xb_node_query_text (component, "../info/filename", NULL);
	if (tmp != NULL && gs_app_get_metadata_item (app, "appstream::source-file") == NULL) {
		gs_app_set_metadata (app, "appstream::source-file", tmp);
	}

	/* set scope */
	tmp = xb_node_query_text (component, "../info/scope", NULL);
	if (tmp != NULL)
		gs_app_set_scope (app, as_app_scope_from_string (tmp));

	/* set content rating */
	if (TRUE) {
		if (!gs_appstream_refine_app_content_ratings (plugin, app, component, error))
			return FALSE;
	}

	/* set name */
	tmp = xb_node_query_text (component, "name", NULL);
	if (tmp != NULL)
		gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, tmp);

	/* set summary */
	tmp = xb_node_query_text (component, "summary", NULL);
	if (tmp != NULL)
		gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, tmp);

	/* add urls */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) {
		g_autoptr(GPtrArray) urls = NULL;
		urls = xb_node_query (component, "url", 0, NULL);
		if (urls != NULL) {
			for (guint i = 0; i < urls->len; i++) {
				XbNode *url = g_ptr_array_index (urls, i);
				const gchar *kind = xb_node_get_attr (url, "type");
				if (kind == NULL)
					continue;
				gs_app_set_url (app,
						as_url_kind_from_string (kind),
						xb_node_get_text (url));
			}
		}
	}

	/* add launchables */
	launchables = xb_node_query (component, "launchable", 0, NULL);
	if (launchables != NULL) {
		for (guint i = 0; i < launchables->len; i++) {
			XbNode *launchable = g_ptr_array_index (launchables, i);
			const gchar *kind = xb_node_get_attr (launchable, "type");
			if (g_strcmp0 (kind, "desktop-id") == 0) {
				gs_app_set_launchable (app,
						       AS_LAUNCHABLE_KIND_DESKTOP_ID,
						       xb_node_get_text (launchable));
				break;
			} else if (g_strcmp0 (kind, "url") == 0) {
				gs_app_set_launchable (app,
						       AS_LAUNCHABLE_KIND_URL,
						       xb_node_get_text (launchable));
			}
		}
	}

	/* set license */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 &&
	    gs_app_get_license (app) == NULL) {
		tmp = xb_node_query_text (component, "project_license", NULL);
		if (tmp != NULL)
			gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, tmp);
	}

	/* set description */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) {
		g_autofree gchar *description = NULL;
		g_autoptr(XbNode) n = xb_node_query_first (component, "description", NULL);
		if (n != NULL)
			description = gs_appstream_format_description (n, NULL);
		if (description != NULL)
			gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, description);
	}

	/* set icon */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) > 0 &&
	    gs_app_get_icons(app)->len == 0)
		gs_appstream_refine_icon (plugin, app, component);

	/* set categories */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) {
		g_autoptr(GPtrArray) categories = NULL;
		categories = xb_node_query (component, "categories/category", 0, NULL);
		if (categories != NULL) {
			for (guint i = 0; i < categories->len; i++) {
				XbNode *category = g_ptr_array_index (categories, i);
				gs_app_add_category (app, xb_node_get_text (category));

				/* Special case: We used to use the `Blacklisted`
				 * category to hide apps from their .desktop
				 * file or appdata. We now use a quirk for that.
				 * This special case can be removed when all
				 * appstream files no longer use the `Blacklisted`
				 * category (including external-appstream files
				 * put together by distributions). */
				if (g_strcmp0 (xb_node_get_text (category), "Blacklisted") == 0)
					gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
			}
		}
	}

	/* set project group */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) > 0 &&
	    gs_app_get_project_group (app) == NULL) {
		tmp = xb_node_query_text (component, "project_group", NULL);
		if (tmp != NULL && gs_appstream_is_valid_project_group (tmp))
			gs_app_set_project_group (app, tmp);
	}

	/* set developer name */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 &&
	    gs_app_get_developer_name (app) == NULL) {
		tmp = xb_node_query_text (component, "developer_name", NULL);
		if (tmp != NULL)
			gs_app_set_developer_name (app, tmp);
	}

	/* set id kind */
	if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN ||
	    gs_app_get_kind (app) == AS_APP_KIND_GENERIC) {
		tmp = xb_node_get_attr (component, "type");
		gs_app_set_kind (app, as_app_kind_from_string (tmp));
	}

	/* copy all the metadata */
	if (!gs_appstream_copy_metadata (app, component, error))
		return FALSE;

	/* add bundles */
	bundles = xb_node_query (component, "bundle", 0, NULL);
	if (bundles != NULL && gs_app_get_sources(app)->len == 0) {
		for (guint i = 0; i < bundles->len; i++) {
			XbNode *bundle = g_ptr_array_index (bundles, i);
			const gchar *kind = xb_node_get_attr (bundle, "type");
			gs_app_add_source (app, xb_node_get_text (bundle));
			gs_app_set_bundle_kind (app, as_bundle_kind_from_string (kind));

			/* get the type/name/arch/branch */
			if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) {
				g_auto(GStrv) split = g_strsplit (xb_node_get_text (bundle), "/", -1);
				if (g_strv_length (split) != 4) {
					g_set_error (error,
						     GS_PLUGIN_ERROR,
						     GS_PLUGIN_ERROR_NOT_SUPPORTED,
						     "invalid ID %s for a flatpak ref",
						     xb_node_get_text (bundle));
					return FALSE;
				}

				/* we only need the branch for the unique ID */
				gs_app_set_branch (app, split[3]);
			}
		}
	}

	/* add legacy package names */
	if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) {
		g_autoptr(GPtrArray) pkgnames = NULL;
		pkgnames = xb_node_query (component, "pkgname", 0, NULL);
		if (pkgnames != NULL && gs_app_get_sources(app)->len == 0) {
			for (guint i = 0; i < pkgnames->len; i++) {
				XbNode *pkgname = g_ptr_array_index (pkgnames, i);
				tmp = xb_node_get_text (pkgname);
				if (tmp != NULL && tmp[0] != '\0')
					gs_app_add_source (app, tmp);
			}
			gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
		}
	}

	/* set origin for flatpaks */
	if (gs_app_get_origin (app) == NULL &&
	    gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) {
		g_autoptr(XbNode) parent = xb_node_get_parent (component);
		if (parent != NULL) {
			tmp = xb_node_get_attr (parent, "origin");
			gs_app_set_origin (app, tmp);
		}
	}

	/* set addons */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) {
		if (!gs_appstream_refine_add_addons (plugin, app, silo, error))
			return FALSE;
	}

	/* set screenshots */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) > 0 &&
	    gs_app_get_screenshots(app)->len == 0) {
		if (!gs_appstream_refine_add_screenshots (app, component, error))
			return FALSE;
	}

	/* set provides */
	if (!gs_appstream_refine_add_provides (app, component, error))
		return FALSE;

	/* add kudos */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) {
		g_autoptr(GPtrArray) kudos = NULL;
		tmp = gs_plugin_get_locale (plugin);
		if (!_gs_utils_locale_has_translations (tmp)) {
			gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
		} else {

			g_autoptr(GString) xpath = g_string_new (NULL);
			g_auto(GStrv) variants = g_get_locale_variants (tmp);

			/* @variants includes @tmp */
			for (gsize i = 0; variants[i] != NULL; i++)
				xb_string_append_union (xpath, "languages/lang[text()='%s'][@percentage>50]", variants[i]);

			if (xb_node_query_text (component, xpath->str, NULL) != NULL)
				gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
		}

		/* any keywords */
		if (xb_node_query_text (component, "keywords/keyword", NULL) != NULL)
			gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS);

		/* HiDPI icon */
		if (xb_node_query_text (component, "icon[@width='128']", NULL) != NULL)
			gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON);

		/* was this application released recently */
		if (gs_appstream_is_recent_release (component))
			gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE);

		/* add a kudo to featured and popular apps */
		if (xb_node_query_text (component, "kudos/kudo[text()='GnomeSoftware::popular']", NULL) != NULL)
			gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
		if (xb_node_query_text (component, "categories/category[text()='Featured']", NULL) != NULL)
			gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);

		/* add new-style kudos */
		kudos = xb_node_query (component, "kudos/kudo", 0, NULL);
		for (guint i = 0; kudos != NULL && i < kudos->len; i++) {
			XbNode *kudo = g_ptr_array_index (kudos, i);
			switch (as_kudo_kind_from_string (xb_node_get_text (kudo))) {
			case AS_KUDO_KIND_SEARCH_PROVIDER:
				gs_app_add_kudo (app, GS_APP_KUDO_SEARCH_PROVIDER);
				break;
			case AS_KUDO_KIND_USER_DOCS:
				gs_app_add_kudo (app, GS_APP_KUDO_INSTALLS_USER_DOCS);
				break;
			case AS_KUDO_KIND_MODERN_TOOLKIT:
				gs_app_add_kudo (app, GS_APP_KUDO_MODERN_TOOLKIT);
				break;
			case AS_KUDO_KIND_NOTIFICATIONS:
				gs_app_add_kudo (app, GS_APP_KUDO_USES_NOTIFICATIONS);
				break;
			case AS_KUDO_KIND_HIGH_CONTRAST:
				gs_app_add_kudo (app, GS_APP_KUDO_HIGH_CONTRAST);
				break;
			case AS_KUDO_KIND_HI_DPI_ICON:
				gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON);
				break;
			default:
				break;
			}
		}
	}

	/* we have an origin in the XML */
	if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0 &&
	    gs_app_get_origin_appstream (app) == NULL) {
		g_autoptr(XbNode) parent = xb_node_get_parent (component);
		if (parent != NULL) {
			tmp = xb_node_get_attr (parent, "origin");
			if (gs_appstream_origin_valid (tmp))
				gs_app_set_origin_appstream (app, tmp);
		}
	}

	/* is there any update information */
	if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) {
		if (!gs_appstream_refine_app_updates (plugin,
						      app,
						      silo,
						      component,
						      error))
			return FALSE;
	}

	return TRUE;
}

typedef struct {
	AsAppSearchMatch	 match_value;
	XbQuery			*query;
} GsAppstreamSearchHelper;

static void
gs_appstream_search_helper_free (GsAppstreamSearchHelper *helper)
{
	g_object_unref (helper->query);
	g_free (helper);
}

static guint16
gs_appstream_silo_search_component2 (GPtrArray *array, XbNode *component, const gchar *search)
{
	guint16 match_value = 0;

	/* do searches */
	for (guint i = 0; i < array->len; i++) {
		g_autoptr(GPtrArray) n = NULL;
		GsAppstreamSearchHelper *helper = g_ptr_array_index (array, i);
		xb_query_bind_str (helper->query, 0, search, NULL);
		n = xb_node_query_full (component, helper->query, NULL);
		if (n != NULL)
			match_value |= helper->match_value;
	}
	return match_value;
}

static guint16
gs_appstream_silo_search_component (GPtrArray *array, XbNode *component, const gchar * const *search)
{
	guint16 matches_sum = 0;

	/* do *all* search keywords match */
	for (guint i = 0; search[i] != NULL; i++) {
		guint tmp = gs_appstream_silo_search_component2 (array, component, search[i]);
		if (tmp == 0)
			return 0;
		matches_sum |= tmp;
	}
	return matches_sum;
}

gboolean
gs_appstream_search (GsPlugin *plugin,
		     XbSilo *silo,
		     const gchar * const *values,
		     GsAppList *list,
		     GCancellable *cancellable,
		     GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func ((GDestroyNotify) gs_appstream_search_helper_free);
	g_autoptr(GPtrArray) components = NULL;
	g_autoptr(GTimer) timer = g_timer_new ();
	const struct {
		AsAppSearchMatch	 match_value;
		const gchar		*xpath;
	} queries[] = {
		{ AS_APP_SEARCH_MATCH_MIMETYPE,	"mimetypes/mimetype[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_PKGNAME,	"pkgname[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_COMMENT,	"summary[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_NAME,	"name[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_KEYWORD,	"keywords/keyword[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_ID,	"id[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_ID,	"launchable[text()~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_ORIGIN,	"../components[@origin~=stem(?)]" },
		{ AS_APP_SEARCH_MATCH_NONE,	NULL }
	};

	/* add some weighted queries */
	for (guint i = 0; queries[i].xpath != NULL; i++) {
		g_autoptr(GError) error_query = NULL;
		g_autoptr(XbQuery) query = xb_query_new (silo, queries[i].xpath, &error_query);
		if (query != NULL) {
			GsAppstreamSearchHelper *helper = g_new0 (GsAppstreamSearchHelper, 1);
			helper->match_value = queries[i].match_value;
			helper->query = g_steal_pointer (&query);
			g_ptr_array_add (array, helper);
		} else {
			g_debug ("ignoring: %s", error_query->message);
		}
	}

	/* get all components */
	components = xb_silo_query (silo, "components/component", 0, &error_local);
	if (components == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < components->len; i++) {
		XbNode *component = g_ptr_array_index (components, i);
		guint16 match_value = gs_appstream_silo_search_component (array, component, values);
		if (match_value != 0) {
			g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error);
			if (app == NULL)
				return FALSE;
			if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
				g_debug ("not returning wildcard %s",
					 gs_app_get_unique_id (app));
				continue;
			}
			g_debug ("add %s", gs_app_get_unique_id (app));
			gs_app_set_match_value (app, match_value);
			gs_app_list_add (list, app);
		}
	}
	g_debug ("search took %fms", g_timer_elapsed (timer, NULL) * 1000);
	return TRUE;
}

gboolean
gs_appstream_add_category_apps (GsPlugin *plugin,
				XbSilo *silo,
				GsCategory *category,
				GsAppList *list,
				GCancellable *cancellable,
				GError **error)
{
	GPtrArray *desktop_groups;
	g_autoptr(GError) error_local = NULL;

	desktop_groups = gs_category_get_desktop_groups (category);
	if (desktop_groups->len == 0) {
		g_warning ("no desktop_groups for %s", gs_category_get_id (category));
		return TRUE;
	}
	for (guint j = 0; j < desktop_groups->len; j++) {
		const gchar *desktop_group = g_ptr_array_index (desktop_groups, j);
		g_autofree gchar *xpath = NULL;
		g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1);
		g_autoptr(GPtrArray) components = NULL;

		/* generate query */
		if (g_strv_length (split) == 1) {
			xpath = g_strdup_printf ("components/component/categories/"
						 "category[text()='%s']/../..",
						 split[0]);
		} else if (g_strv_length (split) == 2) {
			xpath = g_strdup_printf ("components/component/categories/"
						 "category[text()='%s']/../"
						 "category[text()='%s']/../..",
						 split[0], split[1]);
		}
		components = xb_silo_query (silo, xpath, 0, &error_local);
		if (components == NULL) {
			if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
				return TRUE;
			if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
				return TRUE;
			g_propagate_error (error, g_steal_pointer (&error_local));
			return FALSE;
		}

		/* create app */
		for (guint i = 0; i < components->len; i++) {
			XbNode *component = g_ptr_array_index (components, i);
			g_autoptr(GsApp) app = NULL;
			const gchar *id = xb_node_query_text (component, "id", NULL);
			if (id == NULL)
				continue;
			app = gs_app_new (id);
			gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
			gs_app_list_add (list, app);
		}

	}
	return TRUE;
}

static guint
gs_appstream_count_component_for_groups (GsPlugin *plugin, XbSilo *silo, const gchar *desktop_group)
{
	guint limit = 10;
	g_autofree gchar *xpath = NULL;
	g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1);
	g_autoptr(GPtrArray) array = NULL;
	g_autoptr(GError) error_local = NULL;

	if (g_strv_length (split) == 1) { /* "all" group for a parent category */
		xpath = g_strdup_printf ("components/component/categories/"
					 "category[text()='%s']/../..",
					 split[0]);
	} else if (g_strv_length (split) == 2) {
		xpath = g_strdup_printf ("components/component/categories/"
					 "category[text()='%s']/../"
					 "category[text()='%s']/../..",
					 split[0], split[1]);
	} else {
		return 0;
	}

	array = xb_silo_query (silo, xpath, limit, &error_local);
	if (array == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return 0;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return 0;
		g_warning ("%s", error_local->message);
		return 0;
	}
	return array->len;
}

/* we're not actually adding categories here, we're just setting the number of
 * applications available in each category */
gboolean
gs_appstream_add_categories (GsPlugin *plugin,
			     XbSilo *silo,
			     GPtrArray *list,
			     GCancellable *cancellable,
			     GError **error)
{
	for (guint j = 0; j < list->len; j++) {
		GsCategory *parent = GS_CATEGORY (g_ptr_array_index (list, j));
		GPtrArray *children = gs_category_get_children (parent);

		for (guint i = 0; i < children->len; i++) {
			GsCategory *cat = g_ptr_array_index (children, i);
			GPtrArray *groups = gs_category_get_desktop_groups (cat);
			for (guint k = 0; k < groups->len; k++) {
				const gchar *group = g_ptr_array_index (groups, k);
				guint cnt = gs_appstream_count_component_for_groups (plugin, silo, group);
				for (guint l = 0; l < cnt; l++) {
					gs_category_increment_size (parent);
					if (children->len > 1) {
						/* Parent category has multiple groups, so increment
						 * each group's size too */
						gs_category_increment_size (cat);
					}
				}
			}
		}
		continue;
	}
	return TRUE;
}

gboolean
gs_appstream_add_popular (GsPlugin *plugin,
			  XbSilo *silo,
			  GsAppList *list,
			  GCancellable *cancellable,
			  GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) array = NULL;

	/* find out how many packages are in each category */
	array = xb_silo_query (silo,
			       "components/component/kudos/"
			       "kudo[text()='GnomeSoftware::popular']/../..",
			       0, &error_local);
	if (array == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < array->len; i++) {
		g_autoptr(GsApp) app = NULL;
		XbNode *component = g_ptr_array_index (array, i);
		const gchar *component_id = xb_node_query_text (component, "id", NULL);
		if (component_id == NULL)
			continue;
		app = gs_app_new (component_id);
		gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
		gs_app_list_add (list, app);
	}
	return TRUE;
}

gboolean
gs_appstream_add_recent (GsPlugin *plugin,
			 XbSilo *silo,
			 GsAppList *list,
			 guint64 age,
			 GCancellable *cancellable,
			 GError **error)
{
	guint64 now = (guint64) g_get_real_time () / G_USEC_PER_SEC;
	g_autofree gchar *xpath = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) array = NULL;

	/* use predicate conditions to the max */
	xpath = g_strdup_printf ("components/component/releases/"
				 "release[@timestamp>%" G_GUINT64_FORMAT "]/../..",
				 now - (30 * 24 * 60 * 60));
	array = xb_silo_query (silo, xpath, 0, &error_local);
	if (array == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < array->len; i++) {
		XbNode *component = g_ptr_array_index (array, i);
		g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error);
		if (app == NULL)
			return FALSE;
		gs_app_list_add (list, app);
	}
	return TRUE;
}

gboolean
gs_appstream_add_alternates (GsPlugin *plugin,
			     XbSilo *silo,
			     GsApp *app,
			     GsAppList *list,
			     GCancellable *cancellable,
			     GError **error)
{
	GPtrArray *sources = gs_app_get_sources (app);
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) ids = NULL;
	g_autoptr(GString) xpath = g_string_new (NULL);

	/* probably a package we know nothing about */
	if (gs_app_get_id (app) == NULL)
		return TRUE;

	/* actual ID */
	xb_string_append_union (xpath, "components/component/id[text()='%s']",
				gs_app_get_id (app));

	/* new ID -> old ID */
	xb_string_append_union (xpath, "components/component/id[text()='%s']/../provides/id",
				gs_app_get_id (app));

	/* old ID -> new ID */
	xb_string_append_union (xpath, "components/component/provides/id[text()='%s']/../../id",
				gs_app_get_id (app));

	/* find apps that use the same pkgname */
	for (guint j = 0; j < sources->len; j++) {
		const gchar *source = g_ptr_array_index (sources, j);
		g_autofree gchar *source_safe = xb_string_escape (source);
		xb_string_append_union (xpath,
					"components/component/pkgname[text()='%s']/../id",
					source_safe);
	}

	/* do a big query, and return all the unique results */
	ids = xb_silo_query (silo, xpath->str, 0, &error_local);
	if (ids == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < ids->len; i++) {
		XbNode *n = g_ptr_array_index (ids, i);
		g_autoptr(GsApp) app2 = NULL;
		app2 = gs_app_new (xb_node_get_text (n));
		gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD);
		gs_app_list_add (list, app2);
	}
	return TRUE;
}

gboolean
gs_appstream_add_featured (GsPlugin *plugin,
			   XbSilo *silo,
			   GsAppList *list,
			   GCancellable *cancellable,
			   GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) array = NULL;

	/* find out how many packages are in each category */
	array = xb_silo_query (silo,
			       "components/component/custom/"
			       "value[@key='GnomeSoftware::FeatureTile-css']/../..",
			       0, &error_local);
	if (array == NULL) {
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			return TRUE;
		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
			return TRUE;
		g_propagate_error (error, g_steal_pointer (&error_local));
		return FALSE;
	}
	for (guint i = 0; i < array->len; i++) {
		g_autoptr(GsApp) app = NULL;
		XbNode *component = g_ptr_array_index (array, i);
		const gchar *component_id = xb_node_query_text (component, "id", NULL);
		if (component_id == NULL)
			continue;
		app = gs_app_new (component_id);
		gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
		if (!gs_appstream_copy_metadata (app, component, error))
			return FALSE;
		gs_app_list_add (list, app);
	}
	return TRUE;
}

void
gs_appstream_component_add_keyword (XbBuilderNode *component, const gchar *str)
{
	g_autoptr(XbBuilderNode) keyword = NULL;
	g_autoptr(XbBuilderNode) keywords = NULL;

	/* create <keywords> if it does not already exist */
	keywords = xb_builder_node_get_child (component, "keywords", NULL);
	if (keywords == NULL)
		keywords = xb_builder_node_insert (component, "keywords", NULL);

	/* create <keyword>str</keyword> if it does not already exist */
	keyword = xb_builder_node_get_child (keywords, "keyword", str);
	if (keyword == NULL) {
		keyword = xb_builder_node_insert (keywords, "keyword", NULL);
		xb_builder_node_set_text (keyword, str, -1);
	}
}

void
gs_appstream_component_add_provide (XbBuilderNode *component, const gchar *str)
{
	g_autoptr(XbBuilderNode) provide = NULL;
	g_autoptr(XbBuilderNode) provides = NULL;

	/* create <provides> if it does not already exist */
	provides = xb_builder_node_get_child (component, "provides", NULL);
	if (provides == NULL)
		provides = xb_builder_node_insert (component, "provides", NULL);

	/* create <id>str</id> if it does not already exist */
	provide = xb_builder_node_get_child (provides, "id", str);
	if (provide == NULL) {
		provide = xb_builder_node_insert (provides, "id", NULL);
		xb_builder_node_set_text (provide, str, -1);
	}
}

void
gs_appstream_component_add_category (XbBuilderNode *component, const gchar *str)
{
	g_autoptr(XbBuilderNode) category = NULL;
	g_autoptr(XbBuilderNode) categories = NULL;

	/* create <categories> if it does not already exist */
	categories = xb_builder_node_get_child (component, "categories", NULL);
	if (categories == NULL)
		categories = xb_builder_node_insert (component, "categories", NULL);

	/* create <category>str</category> if it does not already exist */
	category = xb_builder_node_get_child (categories, "category", str);
	if (category == NULL) {
		category = xb_builder_node_insert (categories, "category", NULL);
		xb_builder_node_set_text (category, str, -1);
	}
}

void
gs_appstream_component_add_icon (XbBuilderNode *component, const gchar *str)
{
	g_autoptr(XbBuilderNode) icon = NULL;

	/* create <icon>str</icon> if it does not already exist */
	icon = xb_builder_node_get_child (component, "icon", NULL);
	if (icon == NULL) {
		icon = xb_builder_node_insert (component, "icon",
					       "type", "stock",
					       NULL);
		xb_builder_node_set_text (icon, str, -1);
	}
}

void
gs_appstream_component_add_extra_info (GsPlugin *plugin, XbBuilderNode *component)
{
	const gchar *kind = xb_builder_node_get_attr (component, "type");

	/* add the gnome-software-specific 'Addon' group and ensure they
	 * all have an icon set */
	switch (as_app_kind_from_string (kind)) {
	case AS_APP_KIND_WEB_APP:
		gs_appstream_component_add_keyword (component, kind);
		break;
	case AS_APP_KIND_FONT:
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "Font");
		break;
	case AS_APP_KIND_SHELL_EXTENSION:
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "ShellExtension");
		gs_appstream_component_add_icon (component, "application-x-addon-symbolic");
		break;
	case AS_APP_KIND_DRIVER:
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "Driver");
		gs_appstream_component_add_icon (component, "application-x-firmware-symbolic");
		break;
	case AS_APP_KIND_LOCALIZATION:
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "Localization");
		gs_appstream_component_add_icon (component, "accessories-dictionary-symbolic");
		break;
	case AS_APP_KIND_CODEC:
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "Codec");
		gs_appstream_component_add_icon (component, "application-x-addon");
		break;
	case AS_APP_KIND_INPUT_METHOD:
		gs_appstream_component_add_keyword (component, kind);
		gs_appstream_component_add_category (component, "Addon");
		gs_appstream_component_add_category (component, "InputSource");
		gs_appstream_component_add_icon (component, "system-run-symbolic");
		break;
	case AS_APP_KIND_FIRMWARE:
		gs_appstream_component_add_icon (component, "system-run-symbolic");
		break;
	default:
		break;
	}
}
