/*
 * 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 <errno.h>
#include <string.h>
#include <libusb.h>

static inline guint16 ryos_usb_command(guint8 command) { return 0x300 | command; }

libusb_device_handle *ryos_libusb_open_first_device(void) {
	libusb_device_handle *handle;

	handle = libusb_open_device_with_vid_pid(NULL, USB_VENDOR_ID_ROCCAT, USB_DEVICE_ID_ROCCAT_RYOS_MK_PRO);
	if (handle)
		return handle;
	handle = libusb_open_device_with_vid_pid(NULL, USB_VENDOR_ID_ROCCAT, USB_DEVICE_ID_ROCCAT_RYOS_MK_GLOW);
	if (handle)
		return handle;
	return libusb_open_device_with_vid_pid(NULL, USB_VENDOR_ID_ROCCAT, USB_DEVICE_ID_ROCCAT_RYOS_MK);
}

libusb_device_handle *ryos_libusb_open_from_roccat_device(RoccatDevice const *roccat_device) {
	return libusb_open_device_with_vid_pid(NULL, USB_VENDOR_ID_ROCCAT, roccat_device_get_product_id(roccat_device));
}

enum {
	RYOS_USB_TIMEOUT = 1000,
};

gboolean ryos_libusb_check_firmware_write(libusb_device_handle *handle) {
	RyosControl buffer;
	int len;

	for (;;) {
		g_usleep(100000);
		len = libusb_control_transfer(
				handle,
				LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
				LIBUSB_REQUEST_CLEAR_FEATURE,
				ryos_usb_command(RYOS_COMMAND_FIRMWARE_WRITE_CONTROL),
				0,
				(unsigned char *)&buffer,
				sizeof(RyosControl),
				RYOS_USB_TIMEOUT
		);

		if (len < 0) {
			g_critical("error checking firmware write: %i", len);
			return FALSE;
		} else if (len != sizeof(RyosControl)) {
			g_critical("got wrong length checking firmware write: %i instead of %zu", len, sizeof(RyosControl));
			return FALSE;
		} else { // read right length
			switch (buffer.value) {
			case RYOS_CONTROL_VALUE_STATUS_OK:
				return TRUE;
			case RYOS_CONTROL_VALUE_STATUS_BUSY:
				continue;
			case RYOS_CONTROL_VALUE_STATUS_CRITICAL:
				g_critical("error checking firmware write: critical");
				return FALSE;
			default:
				g_critical("got wrong status checking firmware write: %i", buffer.value);
				return FALSE;
			}
		}
	}
}

guint ryos_libusb_firmware_version_read(libusb_device_handle *handle) {
	RyosInfo buffer;
	int len;

	len = libusb_control_transfer(
			handle,
			LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
			LIBUSB_REQUEST_CLEAR_FEATURE,
			ryos_usb_command(RYOS_COMMAND_INFO),
			0,
			(unsigned char *)&buffer,
			sizeof(RyosInfo),
			RYOS_USB_TIMEOUT
	);

	if (len < 0) {
		g_critical("error reading firmware version: %i", len);
		return 0;
	} else if (len != sizeof(RyosInfo)) {
		g_critical("got wrong length reading firmware version: %i instead of %zu", len, sizeof(RyosInfo));
		return 0;
	}

	return buffer.firmware_version;
}

static gboolean ryos_libusb_firmware_write(libusb_device_handle *handle, RyosFirmwareWrite const *data) {
	int len;

	len = libusb_control_transfer(handle,
			LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
			LIBUSB_REQUEST_SET_CONFIGURATION,
			ryos_usb_command(RYOS_COMMAND_FIRMWARE_WRITE),
			0,
			(unsigned char *)data,
			sizeof(RyosFirmwareWrite),
			RYOS_USB_TIMEOUT
	);

	if (len < 0) {
		g_critical("error writing firmware: %i", len);
		return FALSE;
	} else if (len != sizeof(RyosFirmwareWrite)) {
		g_critical("sent wrong length writing firmware: %i instead of %zu", len, sizeof(RyosFirmwareWrite));
		return FALSE;
	}

	return ryos_libusb_check_firmware_write(handle);
}

static guint16 calc_chunk_bytesum(guint8 const *data) {
	guint i;
	guint16 result = 0;

	for (i = 0; i < RYOS_FIRMWARE_CHUNK_SIZE; ++i)
		result += data[i];

	return result;
}

gboolean ryos_libusb_firmware_write_0(libusb_device_handle *handle) {
	RyosFirmwareWrite data;

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_0;
	memset(&data.data, 0, RYOS_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 ryos_libusb_firmware_write(handle, &data);
}

gboolean ryos_libusb_firmware_write_1(libusb_device_handle *handle) {
	RyosFirmwareWrite data;

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_1;
	memset(&data.data, 0, RYOS_FIRMWARE_CHUNK_SIZE);
	data.data[2] = (guint8)RYOS_FIRMWARE_SIZE;
	data.data[3] = (guint8)(RYOS_FIRMWARE_SIZE >> 8);
	data.data[4] = (guint8)(RYOS_FIRMWARE_SIZE >> 16);
	data.data[5] = (guint8)(RYOS_FIRMWARE_SIZE >> 24);
	data.data[6] = 0x08;
	data.data[7] = 0xAC;
	data.data[8] = 0x22;
	data.data[9] = 0x88;

	return ryos_libusb_firmware_write(handle, &data);
}

gboolean ryos_libusb_firmware_write_2(libusb_device_handle *handle) {
	RyosFirmwareWrite data;

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_2;
	memset(&data.data, 0, RYOS_FIRMWARE_CHUNK_SIZE);
	data.data[2] = 0xEC;

	return ryos_libusb_firmware_write(handle, &data);
}

gboolean ryos_libusb_firmware_write_3(libusb_device_handle *handle, guint16 number, guint8 const *chunk) {
	guint16 bytesum;
	RyosFirmwareWrite data;

	bytesum = calc_chunk_bytesum(chunk);

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_3;
	memset(&data.data, 0, RYOS_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 ryos_libusb_firmware_write(handle, &data);
}

gboolean ryos_libusb_firmware_write_4(libusb_device_handle *handle, guint8 const *chunk) {
	RyosFirmwareWrite data;

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_4;
	memcpy(&data.data, chunk, RYOS_FIRMWARE_CHUNK_SIZE);

	return ryos_libusb_firmware_write(handle, &data);
}

gboolean ryos_libusb_firmware_write_f(libusb_device_handle *handle) {
	RyosFirmwareWrite data;

	data.command = RYOS_COMMAND_FIRMWARE_WRITE;
	data.type = RYOS_FIRMWARE_WRITE_TYPE_F;
	memset(&data.data, 0, RYOS_FIRMWARE_CHUNK_SIZE);

	return ryos_libusb_firmware_write(handle, &data);
}
