/*
 * 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/isku.h"
#include "libusb_roccat_helper.h"
#include <string.h>
#include <stdlib.h>
#include <libusb.h>

static GOptionContext *commandline_parse(int *argc, char ***argv) {
	GOptionContext *context = NULL;
	GError *error = NULL;

	context = g_option_context_new("FILE - update Roccat Isku firmware");

	if (!g_option_context_parse(context, argc, argv, &error)) {
		g_critical("error parsing options: %s", error->message);
		exit(EXIT_FAILURE);
	}
	return context;
}

static void commandline_free(GOptionContext *context) {
	if (context)
		g_option_context_free(context);
}

static guint8 const *firmware_read(gchar const *filename) {
	gchar *result;
	gsize bytes;
	GError *error = NULL;
	guint i;

	if (!g_file_get_contents(filename, &result, &bytes, &error)) {
		g_critical("Error reading firmware: %s",error->message);
		return NULL;
	}

	if (bytes != ISKU_FIRMWARE_SIZE) {
		g_critical("Firmware has wrong size: %zu instead of %i", bytes, ISKU_FIRMWARE_SIZE);
		return NULL;
	}

	result = g_realloc(result, ISKU_FIRMWARE_CHUNK_SIZE * ISKU_FIRMWARE_CHUNKS);

	for (i = ISKU_FIRMWARE_SIZE; i < ISKU_FIRMWARE_CHUNK_SIZE * ISKU_FIRMWARE_CHUNKS; ++i)
		result[i] = 0;

	return (guint8 const *)result;
}

static guint16 firmware_calc_chunk_bytesum(guint8 const *chunk) {
	guint i;
	guint16 result = 0;

	for (i = 0; i < ISKU_FIRMWARE_CHUNK_SIZE; ++i)
		result += chunk[i];

	return result;
}

static gboolean firmware_write_0(libusb_device_handle *handle) {
	IskuFirmwareWrite data;

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 0;
	memset(&data.data, 0, ISKU_FIRMWARE_CHUNK_SIZE);
	data.data[2] = 0x08;
	data.data[3] = 0xAC;
	data.data[4] = 0x32;
	data.data[5] = 0x10;
	data.data[6] = 0x03;
	data.data[7] = 0xCB;
	data.data[9] = 0x01;

	return isku_libusb_firmware_write(handle, &data);
}

static gboolean firmware_write_1(libusb_device_handle *handle) {
	IskuFirmwareWrite data;

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 1;
	memset(&data.data, 0, ISKU_FIRMWARE_CHUNK_SIZE);
	data.data[2] = (guint8)ISKU_FIRMWARE_SIZE;
	data.data[3] = (guint8)(ISKU_FIRMWARE_SIZE >> 8);
	data.data[4] = (guint8)(ISKU_FIRMWARE_SIZE >> 16);
	data.data[5] = (guint8)(ISKU_FIRMWARE_SIZE >> 24);
	data.data[6] = 0x08;
	data.data[7] = 0xAC;
	data.data[8] = 0x22;
	data.data[9] = 0x88;

	return isku_libusb_firmware_write(handle, &data);
}

// Takes longer
static gboolean firmware_write_2(libusb_device_handle *handle) {
	IskuFirmwareWrite data;

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 2;
	memset(&data.data, 0, ISKU_FIRMWARE_CHUNK_SIZE);
	data.data[2] = 0xEC;

	return isku_libusb_firmware_write(handle, &data);
}

static gboolean firmware_write_3(libusb_device_handle *handle, guint16 number, guint8 const *chunk) {
	guint16 bytesum;
	IskuFirmwareWrite data;

	bytesum = firmware_calc_chunk_bytesum(chunk);

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 3;
	memset(&data.data, 0, ISKU_FIRMWARE_CHUNK_SIZE);
	data.data[2] = (guint8)number;
	data.data[3] = (guint8)(number >> 8);
	data.data[4] = (guint8)bytesum;
	data.data[5] = (guint8)(bytesum >> 8);
	data.data[6] = 0x08;
	data.data[7] = 0xAC;
	data.data[8] = 0x22;
	data.data[9] = 0x88;

	return isku_libusb_firmware_write(handle, &data);
}

static gboolean firmware_write_4(libusb_device_handle *handle, guint8 const *chunk) {
	IskuFirmwareWrite data;

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 4;
	memcpy(&data.data, chunk, ISKU_FIRMWARE_CHUNK_SIZE);

	return isku_libusb_firmware_write(handle, &data);
}

static gboolean firmware_write_f(libusb_device_handle *handle) {
	IskuFirmwareWrite data;

	data.command = ISKU_COMMAND_FIRMWARE_WRITE;
	data.number = 0x0F;
	memset(&data.data, 0, ISKU_FIRMWARE_CHUNK_SIZE);

	return isku_libusb_firmware_write(handle, &data);
}

static gboolean print_actual_firmware_version(libusb_device_handle *handle) {
	guint version;
	gchar *version_string;

	version = isku_libusb_firmware_version_read(handle);
	if (version == 0)
		return FALSE;

	version_string = isku_firmware_version_to_string(version);
	g_print("Actual firmware: %s\n", version_string);
	g_free(version_string);
	return TRUE;
}

static gboolean user_decision(gchar *text) {
	gchar decision;
	int retval;

	g_print("%s", text);
	g_print(" (y|n)");
	retval = scanf("%c", &decision);
	if (retval != 1)
		return FALSE;
	fflush(stdin);
	return g_ascii_tolower(decision) == 'y';
}

int main(int argc, char **argv) {
	struct libusb_device_handle *handle;
	GOptionContext *option_context;
	guint8 const *firmware;
	guint8 const *chunk;
	guint i;
	int err;
	gboolean retval;
	int result = EXIT_FAILURE;

	roccat_secure();

	option_context = commandline_parse(&argc, &argv);
	if (argc != 2) {
		g_critical("Error parsing options: To much/few arguments");
		goto exit;
	}

	firmware = firmware_read(argv[1]);
	if (firmware == NULL)
		goto free_context;

	err = libusb_init(NULL);
	if (err) {
		g_critical("error initialising libusb: %i\n", err);
		goto free_firmware;
	}

	handle = isku_libusb_open_first_device();
	if (!handle) {
		g_critical("no Isku found");
		goto free_usb;
	}

	retval = roccat_libusb_claim_interface(handle, 0);
	if (!retval) {
		g_critical("can't claim device");
		goto free_handle;
	}

	print_actual_firmware_version(handle);

	if (!user_decision("Continue?")) {
		result = EXIT_SUCCESS;
		goto free_interface;
	}

	if (!firmware_write_0(handle))
		goto free_interface;
	g_print(".");

	if (!firmware_write_1(handle))
		goto free_interface;
	g_print(".");

	if (!firmware_write_2(handle))
		goto free_interface;
	g_print(".");

	for (i = 0; i < ISKU_FIRMWARE_CHUNKS; ++i) {
		chunk = firmware + i * ISKU_FIRMWARE_CHUNK_SIZE;

		if (!firmware_write_3(handle, i, chunk))
			goto free_interface;
		g_print(".");

		if (!firmware_write_4(handle, chunk))
			goto free_interface;
		g_print(".");
	}

	if (!firmware_write_f(handle))
		goto free_interface;
	g_print(".\n");

	g_print("Now replug\n");

	result = EXIT_SUCCESS;
free_interface:
	roccat_libusb_release_interface(handle, 0);
free_handle:
	libusb_close(handle);
free_usb:
	libusb_exit(NULL);
free_firmware:
	g_free((void *)firmware);
free_context:
	commandline_free(option_context);
exit:
	exit(result);
}
