#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pio_usb.h"
#include "tusb.h"

#include "aw410k.h"
#include "usb_device.h"

#include "usb_host.h"

host_state_t host_state;
struct report_desc *descriptors;
uint8_t num_mounted=0;
static absolute_time_t request_time;
static bool enabled=false;
static uint8_t kb_addr=0;

static void usb_host_init(void);
static void host_ready(void);
static bool request_hid_report(uint8_t dev_addr, uint8_t instance);
static bool stop_hid_report(uint8_t dev_addr, uint8_t instance);
static bool request_hid_reports_all(void);
static bool stop_hid_reports_all(void);
static struct report_desc* report_desc_alloc(void);
static void report_desc_init(struct report_desc *descriptor);
static void report_desc_free(struct report_desc *descriptor);
static bool add_descriptor(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len);
static void remove_instance(uint8_t dev_addr, uint8_t instance);

// initialize usb host
static void usb_host_init(void) {
	// configure PIO USB for TinyUSB host
	pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG;
	pio_cfg.alarm_pool = (void*) alarm_pool_create(2,1);
	tuh_configure(1, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg);

	// run TinyUSB host
	tusb_rhport_init_t host_init = {
		.role = TUSB_ROLE_HOST,
		.speed = TUSB_SPEED_AUTO,
	};
	tuh_hid_set_default_protocol(HID_PROTOCOL_REPORT);
	tusb_init(BOARD_TUH_RHPORT, &host_init);

	host_state=HOST_INACTIVE;
}

void usb_host_main(void) {
	// allow other core to pause host process - required for saving to flash
	multicore_lockout_victim_init();

	usb_host_init();

	if (load_rgb_config()) {
		tud_cdc_write_str("found previous RGB configuration\n");
	}

	while (true) {
		switch ( host_state ) {
			case HOST_MOUNTED:
				device_state = DEVICE_RESTART;
				host_ready();
				break;
			case HOST_UNMOUNTED:
				host_state = HOST_MOUNTED;
				break;
			case HOST_START_LISTEN:
				request_hid_reports_all();
				break;
			case HOST_STOP_LISTEN:
				stop_hid_reports_all();
				break;
			case HOST_LISTENING:
				if (enabled) {
					rgb_task(kb_addr);
				}
				break;
			default:
				break;
		}
		tuh_task();
	}
}

// Invoked when device with hid interface is mounted
void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len)
{
	uint16_t vid, pid;
	tuh_vid_pid_get(dev_addr, &vid, &pid);
	uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);

	/// send device vid:pid information to CDC for debugging
	cdc_count = sprintf(cdc_buf, "Mount: [%04x:%04x][%u:%u] Protocol = %u\n", vid, pid, dev_addr, instance, itf_protocol);
	tud_cdc_write(cdc_buf, cdc_count);

	// enable RGB control if AW410K is mounted
	if (vid == AW410K_VID && pid == AW410K_PID) {
		kb_addr = dev_addr;
		enabled = true;
	}

	// add to HID report descriptor
	if ( add_descriptor(dev_addr, instance, desc_report, desc_len)) {
		num_mounted++;
		host_state=HOST_MOUNTED;
		request_time=get_absolute_time();
	}
}

// Invoked when device with hid interface is un-mounted
void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance)
{
	// send device address:instance to CDC for debugging
	cdc_count = sprintf(cdc_buf, "Unmount: [%u:%u]\n", dev_addr, instance);
	tud_cdc_write(cdc_buf, cdc_count);

	if (stop_hid_report(dev_addr, instance)) {
		tud_cdc_write_str("Successfully stopped receiving reports\n");
	}

	// remove record of interface and disable RGB control if AW410K is unmounted
	remove_instance(dev_addr, instance);
	if (dev_addr == kb_addr) {
		enabled = false;
		kb_addr=0;
	}
	num_mounted--;
	host_state=HOST_UNMOUNTED;
	request_time=get_absolute_time();
}

// Invoked when received report from device via interrupt endpoint
void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len)
{
	if (len > 0 ) {
		cdc_count = sprintf(cdc_buf, "[%u:%u](%u) ", dev_addr, instance, len);
		tud_cdc_write(cdc_buf,cdc_count);
		cdc_print_hex(report, len);
	}

	forward_report(instance, report, len);

	// continue to request to receive report
	if ( !tuh_hid_receive_report(dev_addr, instance) )
	{
		tud_cdc_write_str("Error: cannot request report\r\n");
	}
}

// start listening on host for HID events
static void host_ready(void) {
	if (absolute_time_diff_us(request_time, get_absolute_time()) >= 500000){
		if( descriptors != NULL ) {
			host_state = HOST_START_LISTEN;
		} else {
			host_state = HOST_INACTIVE;
		}
	}
}

// request HID input reports on specified device address and instance
static bool request_hid_report(uint8_t dev_addr, uint8_t instance) {
	// request to receive reports HID devices
	if ( !tuh_hid_receive_report(dev_addr, instance) ) {
		cdc_count = sprintf(cdc_buf, "Error: cannot request report on [%u:%u]\n", dev_addr, instance);
		tud_cdc_write(cdc_buf, cdc_count);
		return false;
	}
	return true;
}

// stop receiving HID input reports on specified device address and instance
static bool stop_hid_report(uint8_t dev_addr, uint8_t instance) {
	if (!tuh_hid_receive_abort(dev_addr, instance)) {
		tud_cdc_write_str("Error: could not stop receiving reports\n");
		return false;
	}

	return true;
}

// start listening to HID input reports on all mounted devices
static bool request_hid_reports_all(void) {
	// send request to receive reports on all mounted devices
	struct report_desc * current;
	for (current=descriptors; current != NULL; current=current->next) {
		if (! current->listening) {
			if(request_hid_report(current->dev_addr, current->instance)) {
				cdc_count = sprintf(cdc_buf, "Listening to input reports on [%u:%u]\n", current->dev_addr, current->instance);
				tud_cdc_write(cdc_buf, cdc_count);
				current->listening = true;
			} else {
				tud_cdc_write_str("Error listening to input report(s)\n");
				stop_hid_reports_all();
				return false;
			}
		}
	}

	host_state = HOST_LISTENING;
	return true;
}

// stop listening to HID input reports on all mounted devices
static bool stop_hid_reports_all(void) {
	// send request to stop reports on all mounted devices
	struct report_desc * current;
	for (current=descriptors; current != NULL; current=current->next) {
		if (current->listening) {
			if(stop_hid_report(current->dev_addr, current->instance)) {
				cdc_count = sprintf(cdc_buf, "Stopping input reports on [%u:%u]\n", current->dev_addr, current->instance);
				tud_cdc_write(cdc_buf, cdc_count);
				current->listening = false;
			} else {
				tud_cdc_write_str("Error stopping input report(s)\n");
				return false;
			}
		}
	}

	host_state = HOST_INACTIVE;
	return true;
}

// allocate memory for USB interface report descriptor
static struct report_desc * report_desc_alloc(void) {
	struct report_desc *ret = REPORT_DESC_ALLOC();

	if (ret != NULL) {
		report_desc_init(ret);
		if (descriptors == NULL) {
			descriptors = ret;
		} else {
			struct report_desc *last;
			for (last = descriptors; last->next != NULL; last=last->next);
			last->next = ret;
		}
	}

	return ret;
}

// initialize report descriptor struct
static void report_desc_init(struct report_desc *descriptor) {
	memset(descriptor, 0, sizeof(struct report_desc));
	descriptor->next = NULL;
}

// free memory for report descriptor struct
static void report_desc_free(struct report_desc *descriptor) {
	if (descriptor != NULL) {
		if (descriptors == descriptor) {
			descriptors = descriptor->next;
		} else {
			struct report_desc *last;
			for (last = descriptors; last->next != NULL; last = last->next) {
				if ((last->next) == descriptor) {
					last->next = descriptor->next;
					break;
				}
			}
		}
		free(descriptor);
	}
}

// add report descriptor for new HID interface
static bool add_descriptor(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) {
	struct report_desc * descriptor = report_desc_alloc();
	if (descriptor == NULL) {
		return false;
	}
	
	memcpy(descriptor->descriptor, desc_report, desc_len);
	descriptor->desc_len = desc_len;
	descriptor->dev_addr = dev_addr;
	descriptor->instance = instance;
	descriptor->listening = false;

	return true;

}

// remove report descriptor for HID interface
static void remove_instance(uint8_t dev_addr, uint8_t instance) {
	struct report_desc *descriptor = report_desc_find(dev_addr, instance);

	if (descriptor != NULL) {
		report_desc_free(descriptor);
	}
}

// find report descriptor by device address and instance
struct report_desc * report_desc_find(uint8_t dev_addr, uint8_t instance) {
	struct report_desc *descriptor;
	for (descriptor = descriptors; descriptor != NULL; descriptor = descriptor->next) {
		if (descriptor->dev_addr==dev_addr && descriptor->instance==instance) {
			break;
		}
	}

	return descriptor;
}

