/*
 * 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/ryos.h"
#include "ryosconfig_update_assistant.h"
#include "g_roccat_helper.h"
#include "libusb_roccat_helper.h"
#include <libusb.h>

#define RYOSCONFIG_UPDATE_ASSISTANT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), RYOSCONFIG_UPDATE_ASSISTANT_TYPE, RyosconfigUpdateAssistantPrivate))

static gdouble const fraction_size = 1.0 / (RYOS_FIRMWARE_CHUNKS * 2.0 + 4.0);

struct _RyosconfigUpdateAssistantPrivate {
	RoccatDevice **ryos;
	struct libusb_device_handle *handle;
	guint update_state;
	guint chunk_number;
	gboolean chunk_init;
	gdouble fraction;
	guint8 *firmware;
};

G_DEFINE_TYPE(RyosconfigUpdateAssistant, ryosconfig_update_assistant, ROCCAT_UPDATE_ASSISTANT_TYPE);

static gchar *firmware_version(RoccatDevice *ryos) {
	guint version;
	gchar *result;
	GError *local_error = NULL;

	version = ryos_firmware_version_read(ryos, &local_error);
	if (local_error) {
		g_clear_error(&local_error);
		result = "unknown";
	} else
		result = ryos_firmware_version_to_string(version);

	return result;
}

static void ryos_update_assistant_set_new_firmware_info(RyosconfigUpdateAssistant *assistant, gchar const *path) {
	gchar *string;
	string = g_path_get_basename(path);
	roccat_update_assistant_set_new_firmware_info(ROCCAT_UPDATE_ASSISTANT(assistant), string);
	g_free(string);
}

GtkWidget *ryosconfig_update_assistant_new(GtkWindow *parent, RoccatDevice **ryos) {
	RyosconfigUpdateAssistant *assistant;
	gchar *name;
	gchar *version;

	name = ryos_device_name(*ryos);
	version = firmware_version(*ryos);

	assistant = RYOSCONFIG_UPDATE_ASSISTANT(g_object_new(RYOSCONFIG_UPDATE_ASSISTANT_TYPE,
			"transient-for", parent,
			"modal", TRUE,
			"device-name", name,
			"actual-firmware-version", version,
			NULL));

	assistant->priv->ryos = ryos;

	g_free(name);
	g_free(version);

	return GTK_WIDGET(assistant);
}

static void file_selected_cb(RoccatUpdateAssistant *roccat_update_assistant, gpointer user_data) {
	RyosconfigUpdateAssistant *assistant = RYOSCONFIG_UPDATE_ASSISTANT(roccat_update_assistant);
	RyosconfigUpdateAssistantPrivate *priv = assistant->priv;
	gchar *path;

	path = roccat_update_assistant_get_filename(roccat_update_assistant);
	priv->firmware = roccat_update_assistant_firmware_read(path, RYOS_FIRMWARE_SIZE, RYOS_FIRMWARE_CHUNKS, RYOS_FIRMWARE_CHUNK_SIZE);

	if (priv->firmware) {
		ryos_update_assistant_set_new_firmware_info(assistant, path);
		roccat_update_assistant_set_ok(roccat_update_assistant);
	}

	g_free(path);
}

static gboolean timeout_cb(gpointer user_data) {
	RyosconfigUpdateAssistant *assistant = RYOSCONFIG_UPDATE_ASSISTANT(user_data);
	RyosconfigUpdateAssistantPrivate *priv = assistant->priv;
	guint8 const *chunk;

	if (priv->update_state == 0) {
		if (!ryos_libusb_firmware_write_0(priv->handle))
			goto error;
		++priv->update_state;
	} else if (priv->update_state == 1) {
		if (!ryos_libusb_firmware_write_1(priv->handle))
			goto error;
		++priv->update_state;
	} else if (priv->update_state == 2) {
		if (!ryos_libusb_firmware_write_2(priv->handle))
			goto error;
		++priv->update_state;
		priv->chunk_number = 0;
		priv->chunk_init = 1;
	} else if (priv->update_state == 3) {
		if (priv->chunk_number < RYOS_FIRMWARE_CHUNKS) {
			chunk = priv->firmware + priv->chunk_number * RYOS_FIRMWARE_CHUNK_SIZE;

			if (priv->chunk_init) {
				if (!ryos_libusb_firmware_write_3(priv->handle, priv->chunk_number, chunk))
					goto error;
				priv->chunk_init = FALSE;
			} else {
				if (!ryos_libusb_firmware_write_4(priv->handle, chunk))
					goto error;
				++priv->chunk_number;
				priv->chunk_init = TRUE;
			}
		} else
			++priv->update_state;
	} else if (priv->update_state == 4) {
		if (!ryos_libusb_firmware_write_f(priv->handle))
			goto error;
		roccat_update_assistant_set_success(ROCCAT_UPDATE_ASSISTANT(assistant));
		goto ok;
	}

	priv->fraction += fraction_size;
	roccat_update_assistant_set_progress_fraction(ROCCAT_UPDATE_ASSISTANT(assistant), priv->fraction);

	return TRUE;
error:
	roccat_update_assistant_set_failed(ROCCAT_UPDATE_ASSISTANT(assistant), "Error while sending data");
ok:
	roccat_libusb_release_interface(priv->handle, 0);
	libusb_close(priv->handle);
	libusb_exit(NULL);
	return FALSE;
}

static void prepare_action_cb(RoccatUpdateAssistant *roccat_update_assistant, gpointer user_data) {
	RyosconfigUpdateAssistantPrivate *priv = RYOSCONFIG_UPDATE_ASSISTANT(roccat_update_assistant)->priv;
	int err;

	err = libusb_init(NULL);
	if (err) {
		g_critical("error initializing libusb: %i\n", err);
		roccat_update_assistant_set_failed(roccat_update_assistant, "Can't initialize libusb");
		return;
	}

	priv->handle = ryos_libusb_open_from_roccat_device(*priv->ryos);
	if (!priv->handle) {
		g_critical("no device found");
		roccat_update_assistant_set_failed(roccat_update_assistant, "No device found");
		goto free_usb;
	}

	if (!roccat_libusb_claim_interface(priv->handle, 0)) {
		g_critical("can't claim device");
		roccat_update_assistant_set_failed(roccat_update_assistant, "Can't claim device");
		goto free_handle;
	}

	priv->fraction = 0.0;
	priv->update_state = 0;
	g_timeout_add(0.2 * G_ROCCAT_MSEC_PER_SEC, timeout_cb, roccat_update_assistant);

	return;

free_handle:
	libusb_close(priv->handle);
free_usb:
	libusb_exit(NULL);
}

static void ryosconfig_update_assistant_init(RyosconfigUpdateAssistant *assistant) {
	RyosconfigUpdateAssistantPrivate *priv = RYOSCONFIG_UPDATE_ASSISTANT_GET_PRIVATE(assistant);
	assistant->priv = priv;

	g_signal_connect(G_OBJECT(assistant), "file-selected", G_CALLBACK(file_selected_cb), NULL);
	g_signal_connect(G_OBJECT(assistant), "prepare-action", G_CALLBACK(prepare_action_cb), NULL);
}

static void finalize(GObject *object) {
	G_OBJECT_CLASS(ryosconfig_update_assistant_parent_class)->finalize(object);
}

static void ryosconfig_update_assistant_class_init(RyosconfigUpdateAssistantClass *klass) {
	GObjectClass *gobject_class;

	gobject_class = (GObjectClass*)klass;

	gobject_class->finalize = finalize;

	g_type_class_add_private(klass, sizeof(RyosconfigUpdateAssistantPrivate));
}

void ryosconfig_update_assistant(GtkWindow *parent, RoccatDevice **ryos) {
	GtkWidget *assistant;

	if (*ryos == NULL)
		return;

	assistant = ryosconfig_update_assistant_new(parent, ryos);
	gtk_widget_show_all(assistant);
}
