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

#include "config.h"

#include "gs-extras-page.h"

#include "gs-app-row.h"
#include "gs-language.h"
#include "gs-shell.h"
#include "gs-common.h"
#include "gs-utils.h"
#include "gs-vendor.h"

#include <glib/gi18n.h>

typedef enum {
	GS_EXTRAS_PAGE_STATE_LOADING,
	GS_EXTRAS_PAGE_STATE_READY,
	GS_EXTRAS_PAGE_STATE_NO_RESULTS,
	GS_EXTRAS_PAGE_STATE_FAILED
} GsExtrasPageState;

typedef struct {
	gchar		*title;
	gchar		*search;
	gchar		*search_filename;
	gchar		*package_filename;
	gchar		*url_not_found;
	GsExtrasPage	*self;
} SearchData;

struct _GsExtrasPage
{
	GsPage			  parent_instance;

	GsPluginLoader		 *plugin_loader;
	GtkBuilder		 *builder;
	GCancellable		 *search_cancellable;
	GsShell			 *shell;
	GsExtrasPageState	  state;
	GtkSizeGroup		 *sizegroup_image;
	GtkSizeGroup		 *sizegroup_name;
	GtkSizeGroup		 *sizegroup_desc;
	GtkSizeGroup		 *sizegroup_button;
	GPtrArray		 *array_search_data;
	GsExtrasPageMode	  mode;
	GsLanguage		 *language;
	GsVendor		 *vendor;
	guint			  pending_search_cnt;

	GtkWidget		 *label_failed;
	GtkWidget		 *label_no_results;
	GtkWidget		 *list_box_results;
	GtkWidget		 *scrolledwindow;
	GtkWidget		 *spinner;
	GtkWidget		 *stack;
};

G_DEFINE_TYPE (GsExtrasPage, gs_extras_page, GS_TYPE_PAGE)

static void
search_data_free (SearchData *search_data)
{
	if (search_data->self != NULL)
		g_object_unref (search_data->self);
	g_free (search_data->title);
	g_free (search_data->search);
	g_free (search_data->search_filename);
	g_free (search_data->package_filename);
	g_free (search_data->url_not_found);
	g_slice_free (SearchData, search_data);
}

static GsExtrasPageMode
gs_extras_page_mode_from_string (const gchar *str)
{
	if (g_strcmp0 (str, "install-package-files") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES;
	if (g_strcmp0 (str, "install-provide-files") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES;
	if (g_strcmp0 (str, "install-package-names") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES;
	if (g_strcmp0 (str, "install-mime-types") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES;
	if (g_strcmp0 (str, "install-fontconfig-resources") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES;
	if (g_strcmp0 (str, "install-gstreamer-resources") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES;
	if (g_strcmp0 (str, "install-plasma-resources") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES;
	if (g_strcmp0 (str, "install-printer-drivers") == 0)
		return GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS;

	g_assert_not_reached ();
}

const gchar *
gs_extras_page_mode_to_string (GsExtrasPageMode mode)
{
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES)
		return "install-package-files";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES)
		return "install-provide-files";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES)
		return "install-package-names";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES)
		return "install-mime-types";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES)
		return "install-fontconfig-resources";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES)
		return "install-gstreamer-resources";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES)
		return "install-plasma-resources";
	if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS)
		return "install-printer-drivers";

	g_assert_not_reached ();
}

static gchar *
build_comma_separated_list (gchar **items)
{
	guint len;

	len = g_strv_length (items);
	if (len == 2) {
		/* TRANSLATORS: separator for a list of items */
		return g_strjoinv (_(" and "), items);
	} else {
		/* TRANSLATORS: separator for a list of items */
		return g_strjoinv (_(", "), items);
	}
}

static gchar *
build_title (GsExtrasPage *self)
{
	guint i;
	g_autofree gchar *titles = NULL;
	g_autoptr(GPtrArray) title_array = NULL;

	title_array = g_ptr_array_new ();
	for (i = 0; i < self->array_search_data->len; i++) {
		SearchData *search_data;

		search_data = g_ptr_array_index (self->array_search_data, i);
		g_ptr_array_add (title_array, search_data->title);
	}
	g_ptr_array_add (title_array, NULL);

	titles = build_comma_separated_list ((gchar **) title_array->pdata);

	switch (self->mode) {
	case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
		/* TRANSLATORS: Application window title for fonts installation.
		   %s will be replaced by name of the script we're searching for. */
		return g_strdup_printf (ngettext ("Available fonts for the %s script",
		                                  "Available fonts for the %s scripts",
		                                  self->array_search_data->len),
		                        titles);
		break;
	default:
		/* TRANSLATORS: Application window title for codec installation.
		   %s will be replaced by actual codec name(s) */
		return g_strdup_printf (ngettext ("Available software for %s",
		                                  "Available software for %s",
		                                  self->array_search_data->len),
		                        titles);
		break;
	}
}

static void
gs_extras_page_update_ui_state (GsExtrasPage *self)
{
	GtkWidget *widget;
	g_autofree gchar *title = NULL;

	if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS)
		return;

	/* main spinner */
	switch (self->state) {
	case GS_EXTRAS_PAGE_STATE_LOADING:
		gs_start_spinner (GTK_SPINNER (self->spinner));
		break;
	case GS_EXTRAS_PAGE_STATE_READY:
	case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
	case GS_EXTRAS_PAGE_STATE_FAILED:
		gs_stop_spinner (GTK_SPINNER (self->spinner));
		break;
	default:
		g_assert_not_reached ();
		break;
	}

	/* headerbar title */
	widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header"));
	switch (self->state) {
	case GS_EXTRAS_PAGE_STATE_LOADING:
	case GS_EXTRAS_PAGE_STATE_READY:
		title = build_title (self);
		gtk_label_set_label (GTK_LABEL (widget), title);
		break;
	case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
	case GS_EXTRAS_PAGE_STATE_FAILED:
		gtk_label_set_label (GTK_LABEL (widget), _("Unable to Find Requested Software"));
		break;
	default:
		g_assert_not_reached ();
		break;
	}

	/* stack */
	switch (self->state) {
	case GS_EXTRAS_PAGE_STATE_LOADING:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "spinner");
		break;
	case GS_EXTRAS_PAGE_STATE_READY:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "results");
		break;
	case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "no-results");
		break;
	case GS_EXTRAS_PAGE_STATE_FAILED:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "failed");
		break;
	default:
		g_assert_not_reached ();
		break;
	}
}

static void
gs_extras_page_set_state (GsExtrasPage *self,
                          GsExtrasPageState state)
{
	self->state = state;
	gs_extras_page_update_ui_state (self);
}

static void
app_row_button_clicked_cb (GsAppRow *app_row,
                           GsExtrasPage *self)
{
	GsApp *app = gs_app_row_get_app (app_row);

	if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE &&
	    gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) {
		gs_shell_show_uri (self->shell,
	                           gs_app_get_url (app, AS_URL_KIND_MISSING));
	} else if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE ||
	           gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL ||
	           gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) {
		gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL,
				     self->search_cancellable);
	} else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) {
		gs_page_remove_app (GS_PAGE (self), app, self->search_cancellable);
	} else {
		g_critical ("extras: app in unexpected state %u", gs_app_get_state (app));
	}
}

static void
gs_extras_page_add_app (GsExtrasPage *self, GsApp *app, GsAppList *list, SearchData *search_data)
{
	GtkWidget *app_row;
	g_autoptr(GList) existing_apps = NULL;

	/* Don't add same app twice */
	existing_apps = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
	for (GList *l = existing_apps; l != NULL; l = l->next) {
		GsApp *existing_app;

		existing_app = gs_app_row_get_app (GS_APP_ROW (l->data));
		if (app == existing_app)
			gtk_container_remove (GTK_CONTAINER (self->list_box_results),
			                      GTK_WIDGET (l->data));
	}

	app_row = gs_app_row_new (app);
	gs_app_row_set_colorful (GS_APP_ROW (app_row), TRUE);
	gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE);

	g_object_set_data_full (G_OBJECT (app_row), "missing-title", g_strdup (search_data->title), g_free);

	g_signal_connect (app_row, "button-clicked",
	                  G_CALLBACK (app_row_button_clicked_cb),
	                  self);

	gtk_container_add (GTK_CONTAINER (self->list_box_results), app_row);
	gs_app_row_set_size_groups (GS_APP_ROW (app_row),
				    self->sizegroup_image,
				    self->sizegroup_name,
				    self->sizegroup_desc,
				    self->sizegroup_button);
	gtk_widget_show (app_row);
}

static GsApp *
create_missing_app (SearchData *search_data)
{
	GsExtrasPage *self = search_data->self;
	GsApp *app;
	GString *summary_missing;
	g_autofree gchar *name = NULL;
	g_autofree gchar *url = NULL;

	app = gs_app_new ("missing-codec");

	/* TRANSLATORS: This string is used for codecs that weren't found */
	name = g_strdup_printf (_("%s not found"), search_data->title);
	gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, name);

	/* TRANSLATORS: hyperlink title */
	url = g_strdup_printf ("<a href=\"%s\">%s</a>", search_data->url_not_found, _("on the website"));

	summary_missing = g_string_new ("");
	switch (self->mode) {
	case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No applications are available that provide the file %s."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get missing applications "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get missing applications "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("%s is not available."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get missing applications "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get an application that can support this format "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No fonts are available for the %s script support."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get additional fonts "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No addon codecs are available for the %s format."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get a codec that can play this format "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No Plasma resources are available for %s support."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
                 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get additional Plasma resources "
					"might be found %s."), search_data->title, url);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS:
		/* TRANSLATORS: this is when we know about an application or
		 * addon, but it can't be listed for some reason */
		g_string_append_printf (summary_missing, _("No printer drivers are available for %s."), search_data->title);
		g_string_append (summary_missing, "\n");
		/* TRANSLATORS: first %s is the codec name, and second %s is a
		 * hyperlink with the "on the website" text */
		g_string_append_printf (summary_missing, _("Information about %s, as well as options "
					"for how to get a driver that supports this printer "
					"might be found %s."), search_data->title, url);

		break;
	default:
		g_assert_not_reached ();
		break;
	}
	gs_app_set_summary_missing (app, g_string_free (summary_missing, FALSE));

	gs_app_set_kind (app, AS_APP_KIND_GENERIC);
	gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE);
	gs_app_set_url (app, AS_URL_KIND_MISSING, search_data->url_not_found);

	return app;
}

static gchar *
build_no_results_label (GsExtrasPage *self)
{
	GsApp *app = NULL;
	guint num;
	g_autofree gchar *codec_titles = NULL;
	g_autofree gchar *url = NULL;
	g_autoptr(GList) list = NULL;
	g_autoptr(GPtrArray) array = NULL;

	list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
	num = g_list_length (list);

	g_assert (num > 0);

	array = g_ptr_array_new ();
	for (GList *l = list; l != NULL; l = l->next) {
		app = gs_app_row_get_app (GS_APP_ROW (l->data));
		g_ptr_array_add (array,
		                 g_object_get_data (G_OBJECT (l->data), "missing-title"));
	}
	g_ptr_array_add (array, NULL);

	url = g_strdup_printf ("<a href=\"%s\">%s</a>",
	                       gs_app_get_url (app, AS_URL_KIND_MISSING),
                               /* TRANSLATORS: hyperlink title */
                               _("this website"));

	codec_titles = build_comma_separated_list ((gchar **) array->pdata);
	/* TRANSLATORS: no codecs were found. First %s will be replaced by actual codec name(s), second %s is a link titled "this website" */
	return g_strdup_printf (ngettext ("Unfortunately, the %s you were searching for could not be found. Please see %s for more information.",
	                                  "Unfortunately, the %s you were searching for could not be found. Please see %s for more information.",
	                                  num),
	                        codec_titles,
	                        url);
}

static void
show_search_results (GsExtrasPage *self)
{
	GsApp *app;
	guint n_children;
	guint n_missing;
	g_autoptr(GList) list = NULL;

	list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
	n_children = g_list_length (list);

	/* count the number of rows with missing codecs */
	n_missing = 0;
	for (GList *l = list; l != NULL; l = l->next) {
		app = gs_app_row_get_app (GS_APP_ROW (l->data));
		if (g_strcmp0 (gs_app_get_id (app), "missing-codec") == 0) {
			n_missing++;
		}
	}

	if (n_children == 0 || n_children == n_missing) {
		g_autofree gchar *str = NULL;

		/* no results */
		g_debug ("extras: failed to find any results, %u", n_missing);
		str = build_no_results_label (self);
		gtk_label_set_label (GTK_LABEL (self->label_no_results), str);
		gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_NO_RESULTS);
	} else if (n_children == 1) {
		/* switch directly to details view */
		g_debug ("extras: found one result, showing in details view");
		g_assert (list != NULL);
		app = gs_app_row_get_app (GS_APP_ROW (list->data));
		gs_shell_change_mode (self->shell, GS_SHELL_MODE_DETAILS, app, TRUE);
	} else {
		/* show what we got */
		g_debug ("extras: got %u search results, showing", n_children);
		gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_READY);
	}
}

static void
search_files_cb (GObject *source_object,
                 GAsyncResult *res,
                 gpointer user_data)
{
	SearchData *search_data = (SearchData *) user_data;
	GsExtrasPage *self = search_data->self;
	g_autoptr(GsAppList) list = NULL;
	guint i;
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	g_autoptr(GError) error = NULL;

	list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
	if (list == NULL) {
		g_autofree gchar *str = NULL;
		if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
			g_debug ("extras: search files cancelled");
			return;
		}
		g_warning ("failed to find any search results: %s", error->message);
		str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
		gtk_label_set_label (GTK_LABEL (self->label_failed), str);
		gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
		return;
	}

	/* add missing item */
	if (gs_app_list_length (list) == 0) {
		g_autoptr(GsApp) app = NULL;
		g_debug ("extras: no search result for %s, showing as missing",
			 search_data->title);
		app = create_missing_app (search_data);
		gs_app_list_add (list, app);
	}

	for (i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);

		g_debug ("%s\n\n", gs_app_to_string (app));
		gs_extras_page_add_app (self, app, list, search_data);
	}

	self->pending_search_cnt--;

	/* have all searches finished? */
	if (self->pending_search_cnt == 0)
		show_search_results (self);
}

static void
file_to_app_cb (GObject *source_object,
                GAsyncResult *res,
                gpointer user_data)
{
	SearchData *search_data = (SearchData *) user_data;
	GsExtrasPage *self = search_data->self;
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	g_autoptr(GError) error = NULL;
	g_autoptr(GsApp) app = NULL;
	g_autoptr(GsAppList) list = NULL;

	list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
	if (list == NULL) {
		if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
			g_debug ("extras: search what provides cancelled");
			return;
		}
		if (g_error_matches (error,
				     GS_PLUGIN_ERROR,
				     GS_PLUGIN_ERROR_FAILED)) {
			g_debug ("extras: no search result for %s, showing as missing", search_data->title);
			app = create_missing_app (search_data);
		} else {
			g_autofree gchar *str = NULL;

			g_warning ("failed to find any search results: %s", error->message);
			str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
			gtk_label_set_label (GTK_LABEL (self->label_failed), str);
			gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
			return;
		}
	} else {
		app = g_object_ref (gs_app_list_index (list, 0));
	}

	g_debug ("%s\n\n", gs_app_to_string (app));
	gs_extras_page_add_app (self, app, list, search_data);

	self->pending_search_cnt--;

	/* have all searches finished? */
	if (self->pending_search_cnt == 0)
		show_search_results (self);
}

static void
get_search_what_provides_cb (GObject *source_object,
                             GAsyncResult *res,
                             gpointer user_data)
{
	SearchData *search_data = (SearchData *) user_data;
	GsExtrasPage *self = search_data->self;
	g_autoptr(GsAppList) list = NULL;
	guint i;
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	g_autoptr(GError) error = NULL;

	list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
	if (list == NULL) {
		g_autofree gchar *str = NULL;
		if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
			g_debug ("extras: search what provides cancelled");
			return;
		}
		g_warning ("failed to find any search results: %s", error->message);
		str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
		gtk_label_set_label (GTK_LABEL (self->label_failed), str);
		gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
		return;
	}

	/* add missing item */
	if (gs_app_list_length (list) == 0) {
		g_autoptr(GsApp) app = NULL;
		g_debug ("extras: no search result for %s, showing as missing",
			 search_data->title);
		app = create_missing_app (search_data);
		gs_app_list_add (list, app);
	}

	for (i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);

		g_debug ("%s\n\n", gs_app_to_string (app));
		gs_extras_page_add_app (self, app, list, search_data);
	}

	self->pending_search_cnt--;

	/* have all searches finished? */
	if (self->pending_search_cnt == 0)
		show_search_results (self);
}

static void
gs_extras_page_load (GsExtrasPage *self, GPtrArray *array_search_data)
{
	guint i;

	/* cancel any pending searches */
	g_cancellable_cancel (self->search_cancellable);
	g_clear_object (&self->search_cancellable);
	self->search_cancellable = g_cancellable_new ();

	if (array_search_data != NULL) {
		if (self->array_search_data != NULL)
			g_ptr_array_unref (self->array_search_data);
		self->array_search_data = g_ptr_array_ref (array_search_data);
	}

	self->pending_search_cnt = 0;

	/* remove old entries */
	gs_container_remove_all (GTK_CONTAINER (self->list_box_results));

	/* set state as loading */
	self->state = GS_EXTRAS_PAGE_STATE_LOADING;

	/* start new searches, separate one for each codec */
	for (i = 0; i < self->array_search_data->len; i++) {
		GsPluginRefineFlags refine_flags;
		SearchData *search_data;

		refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
		               GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
		               GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES;

		search_data = g_ptr_array_index (self->array_search_data, i);
		if (search_data->search_filename != NULL) {
			g_autoptr(GsPluginJob) plugin_job = NULL;
			plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_FILES,
							 "search", search_data->search_filename,
							 "refine-flags", refine_flags,
							 NULL);
			g_debug ("searching filename: '%s'", search_data->search_filename);
			gs_plugin_loader_job_process_async (self->plugin_loader,
							    plugin_job,
							    self->search_cancellable,
							    search_files_cb,
							    search_data);
		} else if (search_data->package_filename != NULL) {
			g_autoptr (GFile) file = NULL;
			g_autoptr(GsPluginJob) plugin_job = NULL;
			file = g_file_new_for_path (search_data->package_filename);
			plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
							 "file", file,
							 "refine-flags", refine_flags,
							 NULL);
			g_debug ("resolving filename to app: '%s'", search_data->package_filename);
			gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
							    self->search_cancellable,
							    file_to_app_cb,
							    search_data);
		} else {
			g_autoptr(GsPluginJob) plugin_job = NULL;
			g_debug ("searching what provides: '%s'", search_data->search);
			plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES,
							 "search", search_data->search,
							 "refine-flags", refine_flags,
							 NULL);
			gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
							    self->search_cancellable,
							    get_search_what_provides_cb,
							    search_data);
		}
		self->pending_search_cnt++;
	}
}

static void
gs_extras_page_reload (GsPage *page)
{
	GsExtrasPage *self = GS_EXTRAS_PAGE (page);
	if (self->array_search_data != NULL)
		gs_extras_page_load (self, NULL);
}

static void
gs_extras_page_search_package_files (GsExtrasPage *self, gchar **files)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; files[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup (files[i]);
		search_data->package_filename = g_strdup (files[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_provide_files (GsExtrasPage *self, gchar **files)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; files[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup (files[i]);
		search_data->search_filename = g_strdup (files[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_package_names (GsExtrasPage *self, gchar **package_names)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; package_names[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup (package_names[i]);
		search_data->search = g_strdup (package_names[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_mime_types (GsExtrasPage *self, gchar **mime_types)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; mime_types[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup_printf (_("%s file format"), mime_types[i]);
		search_data->search = g_strdup (mime_types[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_MIME);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static gchar *
font_tag_to_lang (const gchar *tag)
{
	if (g_str_has_prefix (tag, ":lang="))
		return g_strdup (tag + 6);

	return NULL;
}

static gchar *
gs_extras_page_font_tag_to_localised_name (GsExtrasPage *self, const gchar *tag)
{
	gchar *name;
	g_autofree gchar *lang = NULL;
	g_autofree gchar *language = NULL;

	/* use fontconfig to get the language code */
	lang = font_tag_to_lang (tag);
	if (lang == NULL) {
		g_warning ("Could not parse language tag '%s'", tag);
		return NULL;
	}

	/* convert to localisable name */
	language = gs_language_iso639_to_language (self->language, lang);
	if (language == NULL) {
		g_warning ("Could not match language code '%s' to an ISO639 language", lang);
		return NULL;
	}

	/* get translation, or return untranslated string */
	name = g_strdup (dgettext("iso_639", language));
	if (name == NULL)
		name = g_strdup (language);

	return name;
}

static void
gs_extras_page_search_fontconfig_resources (GsExtrasPage *self, gchar **resources)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; resources[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = gs_extras_page_font_tag_to_localised_name (self, resources[i]);
		search_data->search = g_strdup (resources[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_FONT);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_gstreamer_resources (GsExtrasPage *self, gchar **resources)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; resources[i] != NULL; i++) {
		SearchData *search_data;
		g_auto(GStrv) parts = NULL;

		parts = g_strsplit (resources[i], "|", 2);

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup (parts[0]);
		search_data->search = g_strdup (parts[1]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_CODEC);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_plasma_resources (GsExtrasPage *self, gchar **resources)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i;

	for (i = 0; resources[i] != NULL; i++) {
		SearchData *search_data;

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup (resources[i]);
		search_data->search = g_strdup (resources[i]);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

static void
gs_extras_page_search_printer_drivers (GsExtrasPage *self, gchar **device_ids)
{
	g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
	guint i, j;
	guint len;

	len = g_strv_length (device_ids);
	if (len > 1)
		/* hardcode for now as we only support one at a time */
		len = 1;

	/* make a list of provides tags */
	for (i = 0; i < len; i++) {
		SearchData *search_data;
		gchar *p;
		guint n_fields;
		g_autofree gchar *tag = NULL;
		g_autofree gchar *mfg = NULL;
		g_autofree gchar *mdl = NULL;
		g_auto(GStrv) fields = NULL;

		fields = g_strsplit (device_ids[i], ";", 0);
		n_fields = g_strv_length (fields);
		mfg = mdl = NULL;
		for (j = 0; j < n_fields && (!mfg || !mdl); j++) {
			if (g_str_has_prefix (fields[j], "MFG:"))
				mfg = g_strdup (fields[j] + 4);
			else if (g_str_has_prefix (fields[j], "MDL:"))
				mdl = g_strdup (fields[j] + 4);
		}

		if (!mfg || !mdl) {
			g_warning("invalid line '%s', missing field",
				    device_ids[i]);
			continue;
		}

		tag = g_strdup_printf ("%s;%s;", mfg, mdl);

		/* Replace spaces with underscores */
		for (p = tag; *p != '\0'; p++)
			if (*p == ' ')
				*p = '_';

		search_data = g_slice_new0 (SearchData);
		search_data->title = g_strdup_printf ("%s %s", mfg, mdl);
		search_data->search = g_ascii_strdown (tag, -1);
		search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_HARDWARE);
		search_data->self = g_object_ref (self);
		g_ptr_array_add (array_search_data, search_data);
	}

	gs_extras_page_load (self, array_search_data);
}

void
gs_extras_page_search (GsExtrasPage  *self,
                       const gchar   *mode_str,
                       gchar        **resources)
{
	self->mode = gs_extras_page_mode_from_string (mode_str);
	switch (self->mode) {
	case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES:
		gs_extras_page_search_package_files (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES:
		gs_extras_page_search_provide_files (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES:
		gs_extras_page_search_package_names (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES:
		gs_extras_page_search_mime_types (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
		gs_extras_page_search_fontconfig_resources (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES:
		gs_extras_page_search_gstreamer_resources (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES:
		gs_extras_page_search_plasma_resources (self, resources);
		break;
	case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS:
		gs_extras_page_search_printer_drivers (self, resources);
		break;
	default:
		g_assert_not_reached ();
		break;
	}
}

static void
gs_extras_page_switch_to (GsPage *page,
                          gboolean scroll_up)
{
	GsExtrasPage *self = GS_EXTRAS_PAGE (page);
	GtkWidget *widget;

	if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS) {
		g_warning ("Called switch_to(codecs) when in mode %s",
			   gs_shell_get_mode_string (self->shell));
		return;
	}

	widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header"));
	gtk_widget_show (widget);

	if (scroll_up) {
		GtkAdjustment *adj;
		adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow));
		gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj));
	}

	gs_extras_page_update_ui_state (self);
}

static void
row_activated_cb (GtkListBox *list_box,
                  GtkListBoxRow *row,
                  GsExtrasPage *self)
{
	GsApp *app;

	app = gs_app_row_get_app (GS_APP_ROW (row));

	if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE &&
	    gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) {
		gs_shell_show_uri (self->shell,
		                   gs_app_get_url (app, AS_URL_KIND_MISSING));
	} else {
		gs_shell_show_app (self->shell, app);
	}
}

static gchar *
get_app_sort_key (GsApp *app)
{
	GString *key = NULL;
	g_autofree gchar *sort_name = NULL;

	key = g_string_sized_new (64);

	/* sort missing applications as last */
	switch (gs_app_get_state (app)) {
	case AS_APP_STATE_UNAVAILABLE:
		g_string_append (key, "9:");
		break;
	default:
		g_string_append (key, "1:");
		break;
	}

	/* finally, sort by short name */
	if (gs_app_get_name (app) != NULL) {
		sort_name = gs_utils_sort_key (gs_app_get_name (app));
		g_string_append (key, sort_name);
	}

	return g_string_free (key, FALSE);
}

static gint
list_sort_func (GtkListBoxRow *a,
                GtkListBoxRow *b,
                gpointer user_data)
{
	GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a));
	GsApp *a2 = gs_app_row_get_app (GS_APP_ROW (b));
	g_autofree gchar *key1 = get_app_sort_key (a1);
	g_autofree gchar *key2 = get_app_sort_key (a2);

	/* compare the keys according to the algorithm above */
	return g_strcmp0 (key1, key2);
}

static void
list_header_func (GtkListBoxRow *row,
                  GtkListBoxRow *before,
                  gpointer user_data)
{
	GtkWidget *header;

	/* first entry */
	header = gtk_list_box_row_get_header (row);
	if (before == NULL) {
		gtk_list_box_row_set_header (row, NULL);
		return;
	}

	/* already set */
	if (header != NULL)
		return;

	/* set new */
	header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
	gtk_list_box_row_set_header (row, header);
}

static gboolean
gs_extras_page_setup (GsPage *page,
                      GsShell *shell,
                      GsPluginLoader *plugin_loader,
                      GtkBuilder *builder,
                      GCancellable *cancellable,
                      GError **error)
{
	GsExtrasPage *self = GS_EXTRAS_PAGE (page);

	g_return_val_if_fail (GS_IS_EXTRAS_PAGE (self), TRUE);

	self->shell = shell;

	self->plugin_loader = g_object_ref (plugin_loader);
	self->builder = g_object_ref (builder);

	g_signal_connect (self->list_box_results, "row-activated",
			  G_CALLBACK (row_activated_cb), self);
	gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_results),
				      list_header_func,
				      self, NULL);
	gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_results),
				    list_sort_func,
				    self, NULL);
	return TRUE;
}

static void
gs_extras_page_dispose (GObject *object)
{
	GsExtrasPage *self = GS_EXTRAS_PAGE (object);

	g_cancellable_cancel (self->search_cancellable);
	g_clear_object (&self->search_cancellable);

	g_clear_object (&self->sizegroup_image);
	g_clear_object (&self->sizegroup_name);
	g_clear_object (&self->sizegroup_desc);
	g_clear_object (&self->sizegroup_button);
	g_clear_object (&self->language);
	g_clear_object (&self->vendor);
	g_clear_object (&self->builder);
	g_clear_object (&self->plugin_loader);

	g_clear_pointer (&self->array_search_data, g_ptr_array_unref);

	G_OBJECT_CLASS (gs_extras_page_parent_class)->dispose (object);
}

static void
gs_extras_page_init (GsExtrasPage *self)
{
	g_autoptr(GError) error = NULL;

	gtk_widget_init_template (GTK_WIDGET (self));

	self->state = GS_EXTRAS_PAGE_STATE_LOADING;
	self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->vendor = gs_vendor_new ();

	/* map ISO639 to language names */
	self->language = gs_language_new ();
	gs_language_populate (self->language, &error);
	if (error != NULL)
		g_error ("Failed to map ISO639 to language names: %s", error->message);
}

static void
gs_extras_page_class_init (GsExtrasPageClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GsPageClass *page_class = GS_PAGE_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	object_class->dispose = gs_extras_page_dispose;
	page_class->switch_to = gs_extras_page_switch_to;
	page_class->reload = gs_extras_page_reload;
	page_class->setup = gs_extras_page_setup;

	gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-extras-page.ui");

	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_failed);
	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_no_results);
	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, list_box_results);
	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, scrolledwindow);
	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, spinner);
	gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, stack);
}

GsExtrasPage *
gs_extras_page_new (void)
{
	GsExtrasPage *self;
	self = g_object_new (GS_TYPE_EXTRAS_PAGE, NULL);
	return GS_EXTRAS_PAGE (self);
}
