/*
 * This file is part of roccat-tools.
 *
 * roccat-tools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * roccat-tools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with roccat-tools. If not, see <http://www.gnu.org/licenses/>.
 */

#include "roccat_update_assistant.h"
#include "roccat_warning_dialog.h"
#include "roccat_firmware.h"
#include "g_roccat_helper.h"

#define ROCCAT_UPDATE_ASSISTANT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), ROCCAT_UPDATE_ASSISTANT_TYPE, RoccatUpdateAssistantPrivate))

struct _RoccatUpdateAssistantPrivate {
	gint file_page;
	GtkLabel *file_page_label;
	GtkFileChooserButton *file_chooser;

	gint check_page;
	GtkLabel *check_page_device_label;
	GtkLabel *actual_firmware_info;
	GtkLabel *new_firmware_info;

	gint action_page;
	GtkProgressBar *progress_bar;

	gint success_page;

	gint failure_page;
	GtkLabel *failure_label;

	gchar *actual_firmware_version;

	gdouble fraction_size;
	gdouble fraction;

	RoccatFirmwareState *state;
	RoccatDevice *device;
	guint firmware_size;
};

G_DEFINE_TYPE(RoccatUpdateAssistant, roccat_update_assistant, GTK_TYPE_ASSISTANT);

enum {
	PREPARE_ACTION,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

enum {
	PROP_0,
	PROP_DEVICE,
	PROP_ACTUAL_FIRMWARE_VERSION,
	PROP_FIRMWARE_SIZE,
};

static void roccat_update_assistant_set_ok(RoccatUpdateAssistant *assistant) {
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant),
			gtk_assistant_get_nth_page(GTK_ASSISTANT(assistant), assistant->priv->file_page), TRUE);
}

static void roccat_update_assistant_set_new_firmware_info(RoccatUpdateAssistant *assistant, gchar const *path) {
	gchar *string;
	string = g_path_get_basename(path);
	gtk_label_set_text(assistant->priv->new_firmware_info, string);
	g_free(string);
}

static void file_set_cb(GtkFileChooserButton *button, gpointer user_data) {
	RoccatUpdateAssistant *assistant = ROCCAT_UPDATE_ASSISTANT(user_data);
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	gchar *path;
	GError *error = NULL;

	path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(priv->file_chooser));

	if (!roccat_firmware_state_read_firmware(priv->state, path, &error)) {
		roccat_handle_error_dialog(NULL, "Error reading firmware", &error);
	} else {
		roccat_update_assistant_set_new_firmware_info(assistant, path);
		roccat_update_assistant_set_ok(assistant);
	}

	g_free(path);
}

static void add_file_filter(GtkFileChooser *chooser) {
	GtkFileFilter *filter;
	filter = gtk_file_filter_new();
	gtk_file_filter_set_name(filter, "Roccat firmware images");
	gtk_file_filter_add_pattern(filter, "*.bin");
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);

	filter = gtk_file_filter_new();
	gtk_file_filter_set_name(filter, "All files");
	gtk_file_filter_add_pattern(filter, "*");
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
}

static void append_file_page(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GtkWidget *page;
	GtkWidget *label;

	page = gtk_vbox_new(FALSE, 0);

	priv->file_page_label = GTK_LABEL(gtk_label_new(NULL));
	gtk_label_set_justify(priv->file_page_label, GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(priv->file_page_label), TRUE, TRUE, 0);

	label = gtk_label_new("Please select a firmware image that fits your device.");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, TRUE, 10);

	priv->file_chooser = GTK_FILE_CHOOSER_BUTTON(gtk_file_chooser_button_new("Select firmware image", GTK_FILE_CHOOSER_ACTION_OPEN));
	add_file_filter(GTK_FILE_CHOOSER(priv->file_chooser));
	gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(priv->file_chooser), TRUE, FALSE, 0);
	g_signal_connect(G_OBJECT(priv->file_chooser), "file-set", G_CALLBACK(file_set_cb), assistant);

	priv->file_page = gtk_assistant_append_page(GTK_ASSISTANT(assistant), page);
	gtk_assistant_set_page_title(GTK_ASSISTANT(assistant), page, "Select firmware");
	gtk_assistant_set_page_type(GTK_ASSISTANT(assistant), page, GTK_ASSISTANT_PAGE_CONTENT);
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant), page, FALSE);

	gtk_widget_show_all(page);
}

static void append_check_page(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GtkWidget *page;
	GtkWidget *label;
	GtkTable *table;

	page = gtk_vbox_new(FALSE, 0);

	table = GTK_TABLE(gtk_table_new(3, 2, FALSE));
	gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(table), FALSE, FALSE, 0);

	label = gtk_label_new("Device:");
	gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
	gtk_table_attach(table, label, 0, 1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
	priv->check_page_device_label = GTK_LABEL(gtk_label_new(NULL));
	gtk_misc_set_alignment(GTK_MISC(priv->check_page_device_label), 0.0, 0.5);
	gtk_table_attach(table, GTK_WIDGET(priv->check_page_device_label), 1, 2, 0, 1, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);

	label = gtk_label_new("Actual firmware:");
	gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
	gtk_table_attach(table,label, 0, 1, 1, 2, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
	priv->actual_firmware_info = GTK_LABEL(gtk_label_new(NULL));
	gtk_misc_set_alignment(GTK_MISC(priv->actual_firmware_info), 0.0, 0.5);
	gtk_table_attach(table, GTK_WIDGET(priv->actual_firmware_info), 1, 2, 1, 2, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);

	label = gtk_label_new("New firmware:");
	gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
	gtk_table_attach(table, label, 0, 1, 2, 3, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
	priv->new_firmware_info = GTK_LABEL(gtk_label_new(NULL));
	gtk_misc_set_alignment(GTK_MISC(priv->new_firmware_info), 0.0, 0.5);
	gtk_table_attach(table, GTK_WIDGET(priv->new_firmware_info), 1, 2, 2, 3, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);

	label = gtk_label_new(NULL);
	gtk_label_set_markup(GTK_LABEL(label), "<b>Please do not unplug the device\n"
			"or power down your computer while the update\nprocess is running.</b>");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, TRUE, 10);

	label = gtk_label_new("Please continue to update the firmware now.");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, TRUE, 10);

	priv->check_page = gtk_assistant_append_page(GTK_ASSISTANT(assistant), page);
	gtk_assistant_set_page_title(GTK_ASSISTANT(assistant), page, "Initiate update");
	gtk_assistant_set_page_type(GTK_ASSISTANT(assistant), page, GTK_ASSISTANT_PAGE_CONFIRM);
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant), page, TRUE);
	gtk_widget_show_all(page);
}

static void append_action_page(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GtkWidget *label;
	GtkWidget *page;

	page = gtk_vbox_new(FALSE, 0);

	label = gtk_label_new("Please wait while the update process is running.");
	priv->progress_bar = GTK_PROGRESS_BAR(gtk_progress_bar_new());

	gtk_box_pack_start(GTK_BOX(page), label, TRUE, FALSE, 0);
	gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(priv->progress_bar), TRUE, FALSE, 0);

	priv->action_page = gtk_assistant_append_page(GTK_ASSISTANT(assistant), page);
	gtk_assistant_set_page_title(GTK_ASSISTANT(assistant), page, "Updating...");
	gtk_assistant_set_page_type(GTK_ASSISTANT(assistant), page, GTK_ASSISTANT_PAGE_PROGRESS);
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant), page, FALSE);
	gtk_widget_show_all(page);
}

static void roccat_update_assistant_set_success(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	gtk_assistant_set_current_page(GTK_ASSISTANT(assistant), priv->success_page);
}

static void append_success_page(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GtkWidget *page;
	GtkWidget *label;

	page = gtk_vbox_new(FALSE, 0);

	label = gtk_label_new("Firmware update successful!");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, FALSE, 0);

	label = gtk_label_new("Please unplug and replug your device now.");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, FALSE, 0);

	priv->success_page = gtk_assistant_append_page(GTK_ASSISTANT(assistant), page);
	gtk_assistant_set_page_title(GTK_ASSISTANT(assistant), page, "Success");
	gtk_assistant_set_page_type(GTK_ASSISTANT(assistant), page, GTK_ASSISTANT_PAGE_SUMMARY);
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant), page, TRUE);
	gtk_widget_show_all(page);
}

static void roccat_update_assistant_set_failed(RoccatUpdateAssistant *assistant, gchar const *text) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	gtk_label_set_text(priv->failure_label, text);
	gtk_assistant_set_current_page(GTK_ASSISTANT(assistant), priv->failure_page);
}

/* This page is inserted after success page and not reachable in normal flow */
static void append_failure_page(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GtkWidget *page;
	GtkWidget *label;

	page = gtk_vbox_new(FALSE, 0);

	label = gtk_label_new("Firmware update failed!");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, FALSE, 0);

	priv->failure_label = GTK_LABEL(gtk_label_new(NULL));
	gtk_label_set_justify(priv->failure_label, GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(priv->failure_label), TRUE, FALSE, 0);

	label = gtk_label_new("Please try again.");
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(page), label, TRUE, FALSE, 0);

	priv->failure_page = gtk_assistant_append_page(GTK_ASSISTANT(assistant), page);
	gtk_assistant_set_page_title(GTK_ASSISTANT(assistant), page, "Failure");
	gtk_assistant_set_page_type(GTK_ASSISTANT(assistant), page, GTK_ASSISTANT_PAGE_SUMMARY);
	gtk_assistant_set_page_complete(GTK_ASSISTANT(assistant), page, TRUE);
	gtk_widget_show_all(page);
}

static gboolean timeout_cb(gpointer user_data) {
	RoccatUpdateAssistant *assistant = ROCCAT_UPDATE_ASSISTANT(user_data);
	RoccatUpdateAssistantPrivate *priv = assistant->priv;
	GError *error = NULL;

	if (!roccat_firmware_state_tick(priv->state, &error)) {
		if (error)
			roccat_update_assistant_set_failed(assistant, "Error while sending data");
		else
			roccat_update_assistant_set_success(assistant);
		return FALSE;
	}

	priv->fraction += priv->fraction_size;
	gtk_progress_bar_set_fraction(priv->progress_bar, MIN(1.0, priv->fraction));

	return TRUE;
}

static void prepare_cb(GtkAssistant *assistant, GtkWidget *page, gpointer user_data) {
	RoccatUpdateAssistant *roccat_update_assistant = ROCCAT_UPDATE_ASSISTANT(assistant);
	RoccatUpdateAssistantPrivate *priv = roccat_update_assistant->priv;

	if (page != gtk_assistant_get_nth_page(GTK_ASSISTANT(assistant), priv->action_page))
		return;

#if (GTK_CHECK_VERSION(2,22,0))
	gtk_assistant_commit(assistant);
#endif
	roccat_firmware_state_reset(priv->state);
	priv->fraction = 0.0;
	(void)g_idle_add(timeout_cb, roccat_update_assistant);
}

static void close_cb(GtkAssistant *assistant, gpointer user_data) {
	// FIXME deactivate while updating
	gtk_widget_destroy(GTK_WIDGET(assistant));
}

static void cancel_cb(GtkAssistant *assistant, gpointer user_data) {
	// FIXME deactivate while updating
	gtk_widget_destroy(GTK_WIDGET(assistant));
}

static void apply_cb(GtkAssistant *assistant, gpointer user_data) {
}

static void roccat_update_assistant_init(RoccatUpdateAssistant *assistant) {
	RoccatUpdateAssistantPrivate *priv = ROCCAT_UPDATE_ASSISTANT_GET_PRIVATE(assistant);
	assistant->priv = priv;

	append_file_page(assistant);
	append_check_page(assistant);
	append_action_page(assistant);
	append_success_page(assistant);
	append_failure_page(assistant);

	g_signal_connect(G_OBJECT(assistant), "prepare", G_CALLBACK(prepare_cb), NULL);
	g_signal_connect(G_OBJECT(assistant), "close", G_CALLBACK(close_cb), NULL);
	g_signal_connect(G_OBJECT(assistant), "cancel", G_CALLBACK(cancel_cb), NULL);
	g_signal_connect(G_OBJECT(assistant), "apply", G_CALLBACK(apply_cb), NULL);
}

static void set_property(GObject *object, guint prop_id, GValue const *value, GParamSpec *pspec) {
	RoccatUpdateAssistantPrivate *priv = ROCCAT_UPDATE_ASSISTANT(object)->priv;
	gchar *text;
	gchar const *device_name;

	switch(prop_id) {
	case PROP_DEVICE:
		priv->device = ROCCAT_DEVICE(g_value_get_object(value));
		g_object_ref(G_OBJECT(priv->device));

		device_name = roccat_device_get_name_static(priv->device);
		text = g_strdup_printf("%s firmware update", device_name);
		g_object_set(G_OBJECT(object), "title", text, NULL);
		g_free(text);

		text = g_strdup_printf("You are about to update a %s.", device_name);
		gtk_label_set_text(priv->file_page_label, text);
		g_free(text);

		gtk_label_set_text(priv->check_page_device_label, device_name);
		break;
	case PROP_ACTUAL_FIRMWARE_VERSION:
		g_free(priv->actual_firmware_version);
		priv->actual_firmware_version = g_strdup(g_value_get_string(value));

		gtk_label_set_text(priv->actual_firmware_info, priv->actual_firmware_version);

		break;
	case PROP_FIRMWARE_SIZE:
		priv->firmware_size = g_value_get_uint(value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
	}
}

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

static GObject *constructor(GType gtype, guint n_properties, GObjectConstructParam *properties) {
	GObject *obj;
	RoccatUpdateAssistantPrivate *priv;

	obj = G_OBJECT_CLASS(roccat_update_assistant_parent_class)->constructor(gtype, n_properties, properties);
	priv = ROCCAT_UPDATE_ASSISTANT(obj)->priv;

	priv->state = roccat_firmware_state_new(priv->device, priv->firmware_size);
	priv->fraction_size = 1.0 / (gdouble)roccat_firmware_fractions(priv->state);

	return obj;
}

static void finalize(GObject *object) {
	RoccatUpdateAssistantPrivate *priv = ROCCAT_UPDATE_ASSISTANT(object)->priv;
	g_free(priv->actual_firmware_version);
	g_object_unref(G_OBJECT(priv->device));
	roccat_firmware_state_free(priv->state);
	G_OBJECT_CLASS(roccat_update_assistant_parent_class)->finalize(object);
}

static void roccat_update_assistant_class_init(RoccatUpdateAssistantClass *klass) {
	GObjectClass *gobject_class = (GObjectClass*)klass;

	gobject_class->set_property = set_property;
	gobject_class->get_property = get_property;
	gobject_class->constructor = constructor;
	gobject_class->finalize = finalize;

	g_type_class_add_private(klass, sizeof(RoccatUpdateAssistantPrivate));

	signals[PREPARE_ACTION] = g_signal_new("prepare-action", G_TYPE_FROM_CLASS(klass),
			G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE,
			0);

	g_object_class_install_property(gobject_class, PROP_DEVICE,
			g_param_spec_object("device",
					"device",
					"Device",
					ROCCAT_DEVICE_TYPE,
					G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property(gobject_class, PROP_ACTUAL_FIRMWARE_VERSION,
			g_param_spec_string("actual-firmware-version",
					"actual-firmware-version",
					"Actual firmware version",
					"unknown",
					G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property(gobject_class, PROP_FIRMWARE_SIZE,
			g_param_spec_uint("firmware-size",
					"firmware-size",
					"Firmware size",
					0, G_MAXUINT, 0,
					G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
}

GtkWidget *roccat_update_assistant_new(GtkWindow *parent, RoccatDevice *device,
		gchar const *version, guint firmware_size) {
	RoccatUpdateAssistant *assistant;
	assistant = ROCCAT_UPDATE_ASSISTANT(g_object_new(ROCCAT_UPDATE_ASSISTANT_TYPE,
			"transient-for", parent,
			"modal", TRUE,
			"device", device,
			"actual-firmware-version", version,
			"firmware-size", firmware_size,
			NULL));
	return GTK_WIDGET(assistant);
}
