// vim: colorcolumn=80 ts=4 sw=4

#include "gfi-list-widget.h"

/* GLOBALS FOR PROPERTIES */

enum property_types
{
	PROP_TYPE = 1,
	N_PROPERTIES
};

static GParamSpec *properties[N_PROPERTIES];

/* GLOBALS FOR SIGNALS */

enum signal_types
{
	REMOVE_REQUESTED,
	UPGRADE_REQUESTED,
	REFRESH_DONE,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

/* METHOD DECLARATIONS */

static void gfi_list_widget_dispose (GObject *object);
static void gfi_list_widget_finalize (GObject *object);

/* INTERNAL FUNCTION DECLARATIONS */

static void	refresh_listitem_text (GfiListWidget *self,
		GfiListItem *gfi_list_item, FlatpakInstalledRef *inst_ref);

/* GOBJECT DEFINITION */

struct _GfiListWidget
{
	GtkWidget parent_instance;

	GfiListWidgetType type;
	GtkWidget *scrolled_window;
	GtkWidget *list_view;
	GListModel *list_model;		/* actually a GtkListStore */
	GtkListItemFactory *factory;
	GtkSelectionModel *selection_model;
	FlatpakInstallation *user_inst;
};

G_DEFINE_TYPE (GfiListWidget, gfi_list_widget, GTK_TYPE_WIDGET)

/* ----------------- */

/* PROPERTIES - GETTERS AND SETTERS */

static void
gfi_list_widget_set_property (GObject *object,
		guint property_id,
		const GValue *value,
		GParamSpec *pspec)
{
	GfiListWidget *self = GFI_LIST_WIDGET(object);

	switch (property_id)
	{
		case PROP_TYPE:
			self->type = g_value_get_int (value);
			g_object_notify_by_pspec (G_OBJECT(self), properties[PROP_TYPE]);
			break;

		default:
			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
gfi_list_widget_get_property (GObject *object,
		guint property_id,
		GValue *value,
		GParamSpec *pspec)
{
	GfiListWidget *self = GFI_LIST_WIDGET(object);

	switch (property_id)
	{
		case PROP_TYPE:
			g_value_set_int (value, self->type);
			break;

		default:
			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}


/* INTERNAL FUNCTION DEFINITIONS */

static void
action_button_clicked_cb (GfiListItem *item,
		GfiListItemActionButtonType type,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	GtkListItem *list_item;
	FlatpakInstalledRef *ref;

	list_item = gfi_list_item_get_parent_list_item (item);
	ref = gtk_list_item_get_item (list_item);
	g_return_if_fail (FLATPAK_IS_INSTALLED_REF (ref));

	switch (type)
	{
		case GFI_LIST_ITEM_ACTION_BUTTON_TYPE_REMOVE:
			g_signal_emit (self, signals[REMOVE_REQUESTED], 0,
					FLATPAK_REF(ref));
			break;

		case GFI_LIST_ITEM_ACTION_BUTTON_TYPE_UPGRADE:
			g_signal_emit (self, signals[UPGRADE_REQUESTED], 0,
					FLATPAK_REF(ref));
			break;

		case GFI_LIST_ITEM_ACTION_BUTTON_TYPE_CUSTOM:
			g_debug ("%s: custom btn action type NOT IMPLEMENTED",
					__func__);
			break;

		default:
			g_warning ("%s: Invalid button action type.", __func__);
			return;
			break;
	}
}

static void
newest_version_async_ready_cb (GObject *source_object,
		GAsyncResult *res,
		gpointer user_data)
{
	GfiListWidget *self;
	GfiListItem *gfi_list_item;
	FlatpakInstalledRef *inst_ref = FLATPAK_INSTALLED_REF(source_object);
	UserDataPair *pair = user_data;
	char *app_latest_version;

	/* C, you know I love you, but this is lame... */
	self = pair->data1;
	gfi_list_item = pair->data2;
	g_free (pair);
	g_return_if_fail (GFI_IS_LIST_WIDGET (self));
	g_return_if_fail (GFI_IS_LIST_ITEM (gfi_list_item));

	app_latest_version = newest_version_of_app_from_installed_ref_finish (
			inst_ref, res);

	if (app_latest_version)
		g_object_set_data_full (G_OBJECT(inst_ref),
				"app_latest_version", app_latest_version, g_free);

	/* All that lameness of the UserDataPair stuff just for this one call... */
	refresh_listitem_text (self, gfi_list_item, inst_ref);
}

/* GFunc */
static void
add_inst_ref_to_store (gpointer data, gpointer user_data)
{
	FlatpakInstalledRef *inst_ref = FLATPAK_INSTALLED_REF(data);
	GListStore *store = G_LIST_STORE(user_data);
	g_autoptr(GBytes) installed_ref_data = NULL;
	g_autoptr(GKeyFile) keyfile = NULL;
	g_autofree char *tmp_prettyname = NULL;
	char *app_prettyname = NULL;
	char *app_xdgname = NULL;
	char *app_desc = NULL;
	char *app_latest_version = NULL;

	g_return_if_fail (FLATPAK_IS_INSTALLED_REF (inst_ref));
	g_return_if_fail (G_IS_LIST_STORE (store));

	installed_ref_data = flatpak_installed_ref_load_metadata (inst_ref,
			NULL, NULL);

	keyfile = g_key_file_new ();
	g_key_file_load_from_bytes (keyfile, installed_ref_data,
			G_KEY_FILE_NONE, NULL);

	/* See if we have a genuine application and if so, store its name in a 
	 * string. This will be our backup if we can't get the nicer name (see
	 * below).
	 */
	app_xdgname = get_name_from_keyfile (keyfile, KEYFILE_NAME_APPLICATION);

	if (! app_xdgname)
		return;

	/* See if a prettier name exists. */
	app_prettyname = g_strdup (flatpak_installed_ref_get_appdata_name (inst_ref));

	/* Try to grab the app description from .desktop file */
	app_desc = app_desc_from_xdg_id (app_xdgname);

	/* Plug these strings into the inst_ref as gobject data
	 */
	g_object_set_data_full (G_OBJECT(inst_ref),
			"app_xdgname", app_xdgname, g_free);

	if (app_prettyname)
		g_object_set_data_full (G_OBJECT(inst_ref),
				"app_prettyname", app_prettyname, g_free);

	if (app_desc)
		g_object_set_data_full (G_OBJECT(inst_ref),
				"app_desc", app_desc, g_free);

	g_list_store_append (store, inst_ref);
}

static GListModel *
create_application_list (void)
{
	GListStore *store;

	store = g_list_store_new (FLATPAK_TYPE_INSTALLED_REF);

	return G_LIST_MODEL(store);
}

/* This is the function we use for setting up new listitems to display.
 */
static void
setup_listitem_cb (GtkListItemFactory *factory,
		GtkListItem	*list_item,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	GtkWidget *gfi_list_item;

	gfi_list_item = gfi_list_item_new ();
	gtk_list_item_set_child (list_item, gfi_list_item);
}

static void
refresh_listitem_text (GfiListWidget *self,
		GfiListItem *gfi_list_item,
		FlatpakInstalledRef *inst_ref)
{
	const char *app_prettyname = NULL;
	const char *app_xdgname = NULL;
	g_autofree char *app_desc = NULL;

	app_xdgname = g_object_get_data (G_OBJECT(inst_ref), "app_xdgname");
	/* We must have the xdg name by now. If not, a function has failed. */
	g_return_if_fail (app_xdgname);

	app_prettyname = g_object_get_data (G_OBJECT(inst_ref), "app_prettyname");

	switch (self->type)
	{
		case GFI_LIST_WIDGET_TYPE_REMOVE:
			app_desc = g_strdup (
					g_object_get_data (G_OBJECT(inst_ref), "app_desc"));
			break;

		case GFI_LIST_WIDGET_TYPE_UPGRADE:
			{
				const char *app_latest_version;

				app_latest_version = g_object_get_data (G_OBJECT(inst_ref),
							"app_latest_version");

				if (app_latest_version)
					app_desc = g_strdup_printf ("Upgrade available: %s",
							app_latest_version);
				else
					app_desc = g_strdup ("Upgrade available");
			}
			break;

		default:
			g_error ("%s: Programmer error - invalid or unhandled "
					"GfiListWidgetType. Abort.", __func__);
			break;
	}
			
	gfi_list_item_set_label (GFI_LIST_ITEM(gfi_list_item),
			app_prettyname ? app_prettyname : app_xdgname);

	if (app_xdgname)
		gfi_list_item_set_icon_name (GFI_LIST_ITEM(gfi_list_item), app_xdgname);
	

	if (app_desc)
		gfi_list_item_set_app_desc (GFI_LIST_ITEM(gfi_list_item), app_desc);
	
}

/* Here we need to prepare the listitem for displaying its item.
 */
static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem *list_item,
				  gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	GtkWidget *gfi_list_item;
	FlatpakInstalledRef *inst_ref;
	UserDataPair *pair;

	gfi_list_item = gtk_list_item_get_child (list_item);
	g_return_if_fail (GFI_IS_LIST_ITEM (gfi_list_item));

	/* Setup action button based on type (removal/upgrade list_widget) */
	gfi_list_item_set_action_button_text (GFI_LIST_ITEM(gfi_list_item),
			self->type == GFI_LIST_WIDGET_TYPE_REMOVE ?
			GFI_LIST_ITEM_ACTION_BUTTON_TYPE_REMOVE :
			GFI_LIST_ITEM_ACTION_BUTTON_TYPE_UPGRADE,
			NULL);

	/* This is kinda lame but right now I can't think of a better solution */
	pair = g_new (UserDataPair, 1);
	pair->data1 = self;
	pair->data2 = gfi_list_item;

	/* Setup signal when GfiListItem's action button (eg, Remove) is clicked */
	g_signal_connect (gfi_list_item, "action-button-clicked",
			G_CALLBACK(action_button_clicked_cb), self);
	
	/* There is no way to cross-reference a widget to get its parent
	 * GtkListItem, so we cache it here like this.
	 */
	gfi_list_item_set_parent_list_item (GFI_LIST_ITEM(gfi_list_item),
			list_item);
	
	inst_ref = gtk_list_item_get_item (list_item);
	g_return_if_fail (FLATPAK_IS_INSTALLED_REF (inst_ref));

	/* Try to grab the latest version of the app */
	newest_version_of_app_from_installed_ref_async (inst_ref,
			NULL,
			newest_version_async_ready_cb,
			pair);

	refresh_listitem_text (self, GFI_LIST_ITEM(gfi_list_item), inst_ref);
}

static void
unbind_listitem_cb (GtkSignalListItemFactory *factory,
		GtkListItem *list_item,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	GtkWidget* gfi_list_item;

	gfi_list_item = gtk_list_item_get_child (list_item);

	/* Disconnect signals connected in bind (important) */

	g_signal_handlers_disconnect_by_func (gfi_list_item,
			(gpointer)action_button_clicked_cb, self);
}

/* This function is called whenever an item in the list is activated (eg
 * clicked on).
 */
static void
activate_cb (GtkListView  *list,
		guint position,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	FlatpakInstalledRef *inst_ref;
	const char *app_xdgname;

	inst_ref = g_list_model_get_item (self->list_model, position);
	g_return_if_fail (FLATPAK_IS_INSTALLED_REF (inst_ref));

	create_app_info_dialog (inst_ref, PARENT_WINDOW (self));
}

/* METHOD DEFINITIONS */

static void
gfi_list_widget_init (GfiListWidget *self)
{
	GtkWidget *widget = GTK_WIDGET(self);

	gtk_widget_set_hexpand (widget, TRUE);
	gtk_widget_set_vexpand (widget, TRUE);

	/* SETUP LISTVIEW */

	/* Setup factory and models */
	self->factory = gtk_signal_list_item_factory_new ();

	g_signal_connect (self->factory, "setup",
			G_CALLBACK(setup_listitem_cb), self);
	
	g_signal_connect (self->factory, "bind",
			G_CALLBACK(bind_listitem_cb), self);

	g_signal_connect (self->factory, "unbind",
			G_CALLBACK(unbind_listitem_cb), self);

	/* Create list model */
	self->list_model = create_application_list ();

	self->selection_model = GTK_SELECTION_MODEL(gtk_single_selection_new (
				self->list_model));

	/* Setup list widget */
	self->list_view = gtk_list_view_new (self->selection_model, self->factory);

	/* listview - setup signals */
	g_signal_connect (self->list_view, "activate",
			G_CALLBACK(activate_cb), self);

	/* put listview in scrolled window (reco'd by TFM) */
	self->scrolled_window = gtk_scrolled_window_new ();
	gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW(self->scrolled_window),
			self->list_view);

	gtk_widget_set_parent (self->scrolled_window, widget);
}

static void
gfi_list_widget_dispose (GObject *object)
{
	GfiListWidget *self = GFI_LIST_WIDGET(object);

	g_clear_pointer (&self->scrolled_window, gtk_widget_unparent);
	g_clear_object (&self->user_inst);

	/* Chain up */
	G_OBJECT_CLASS(gfi_list_widget_parent_class)->dispose (object);
}

static void
gfi_list_widget_finalize (GObject *object)
{
	/* Chain up */
	G_OBJECT_CLASS(gfi_list_widget_parent_class)->finalize (object);
}

static void
gfi_list_widget_class_init (GfiListWidgetClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GParamFlags default_flags =
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY;

	object_class->dispose = gfi_list_widget_dispose;
	object_class->finalize = gfi_list_widget_finalize;
	object_class->set_property = gfi_list_widget_set_property;
	object_class->get_property = gfi_list_widget_get_property;

	/* Properties */

	/* FIXME - I should really use glib-mkenum at some point for this. Notice
	 * how it's error-prone to manually specify the min/max like this.
	 */
	properties[PROP_TYPE] = g_param_spec_int ("type",
			"List type",
			"GFI list widget type",
			GFI_LIST_WIDGET_TYPE_REMOVE,	/* min */
			GFI_LIST_WIDGET_TYPE_UPGRADE,	/* max */
			GFI_LIST_WIDGET_TYPE_REMOVE,	/* default */
			default_flags);

	g_object_class_install_properties (object_class, N_PROPERTIES, properties);

	/* Signals */

	signals[REMOVE_REQUESTED] = g_signal_new_class_handler (
			"remove-requested",
			G_OBJECT_CLASS_TYPE (object_class),
			G_SIGNAL_RUN_LAST,
		/* no default C function */
			NULL,
		/* defaults for accumulator, marshaller &c. */
			NULL, NULL, NULL,	
		/* No return type */
			G_TYPE_NONE,
		/* 1 parameter: flatpakref for removal */
			1,
			FLATPAK_TYPE_REF);

	signals[UPGRADE_REQUESTED] = g_signal_new_class_handler (
			"upgrade-requested",
			G_OBJECT_CLASS_TYPE (object_class),
			G_SIGNAL_RUN_LAST,
			NULL,
			NULL, NULL, NULL,	
		/* No return type */
			G_TYPE_NONE,
		/* 1 parameter: flatpakref for upgrade */
			1,
			FLATPAK_TYPE_REF);

	signals[REFRESH_DONE] = g_signal_new_class_handler (
			"refresh-done",
			G_OBJECT_CLASS_TYPE (object_class),
			G_SIGNAL_RUN_LAST,
			NULL,
			NULL, NULL, NULL,	
		/* No return type or params. */
			G_TYPE_NONE, 0);

	/* Layout Manager */

	gtk_widget_class_set_layout_manager_type (GTK_WIDGET_CLASS(klass),
			GTK_TYPE_BIN_LAYOUT);
}

/* PUBLIC METHOD DEFINITIONS */

GtkWidget *
gfi_list_widget_new (GfiListWidgetType type)
{
	GfiListWidget *self = g_object_new (GFI_TYPE_LIST_WIDGET,
			"type", type,
			NULL);

	gfi_list_widget_refresh (self);

	return GTK_WIDGET(self);
}

/* TODO - use a macro to keep the next 2 functions in sync. */

static void
list_installed_for_update_async_ready_cb (GObject *source_object,
		GAsyncResult *res,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	FlatpakInstallation *installation = FLATPAK_INSTALLATION(source_object);
	GListStore *store = G_LIST_STORE(self->list_model);
	GPtrArray *user_refs;

	user_refs = list_installed_refs_for_update_finish (installation, res);

	g_ptr_array_foreach (user_refs, add_inst_ref_to_store, store);
	g_ptr_array_unref (user_refs);

	g_signal_emit (self, signals[REFRESH_DONE], 0);
}

static void
list_installed_async_ready_cb (GObject *source_object,
		GAsyncResult *res,
		gpointer user_data)
{
	GfiListWidget *self = GFI_LIST_WIDGET(user_data);
	FlatpakInstallation *installation = FLATPAK_INSTALLATION(source_object);
	GListStore *store = G_LIST_STORE(self->list_model);
	GPtrArray *user_refs;

	user_refs = list_installed_refs_finish (installation, res);

	g_ptr_array_foreach (user_refs, add_inst_ref_to_store, store);
	g_ptr_array_unref (user_refs);

	g_signal_emit (self, signals[REFRESH_DONE], 0);
}

void
gfi_list_widget_refresh (GfiListWidget *self)
{
	GListStore *store = G_LIST_STORE(self->list_model);

	g_return_if_fail (GFI_IS_LIST_WIDGET (self));
	g_return_if_fail (G_IS_LIST_STORE (store));

	g_list_store_remove_all (store);
	g_clear_object (&self->user_inst);
	
	self->user_inst = flatpak_installation_new_user (NULL, NULL);

	switch (self->type)
	{
		case GFI_LIST_WIDGET_TYPE_UPGRADE:
			list_installed_refs_for_update_async (self->user_inst,
					NULL,	/* cancellable */
					list_installed_for_update_async_ready_cb,
					self);
			break;

		case GFI_LIST_WIDGET_TYPE_REMOVE:
			list_installed_refs_async (self->user_inst,
					NULL,	/* cancellable */
					list_installed_async_ready_cb,
					self);
			break;

		default:
			g_error ("%s: Programmer error - invalid GfiListWidgetType",
					__func__);
			break;
	}
}
