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

#include "pico/stdlib.h"
#include "bsp/board_api.h"
#include "tusb.h"

#include "aw410k.h"
#include "usb_host.h"
#include "usb_server.h"

#include "usb_device.h"

// reserve space for CDC messages
char cdc_buf[64];
uint16_t cdc_len;
size_t cdc_count;

device_state_t device_state;

// reserve space for HID descriptor
static uint8_t desc_configuration[DESC_CFG_MAX];
static uint16_t _desc_str[32+1];

// device descriptor
static tusb_desc_device_t const desc_device =
{
	.bLength            = sizeof(tusb_desc_device_t),
	.bDescriptorType    = TUSB_DESC_DEVICE,
	.bcdUSB             = USB_BCD,

	// Use Interface Association Descriptor (IAD) for CDC
	// As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1)
	.bDeviceClass       = TUSB_CLASS_MISC,
	.bDeviceSubClass    = MISC_SUBCLASS_COMMON,
	.bDeviceProtocol    = MISC_PROTOCOL_IAD,

	.bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,

	.idVendor           = USB_VID,
	.idProduct          = USB_PID,
	.bcdDevice          = 0x0100,

	.iManufacturer      = 0x01,
	.iProduct           = 0x02,
	.iSerialNumber      = 0x03,

	.bNumConfigurations = 0x01
};
// string labels for device
static char const* string_desc_arr [] =
{
	(const char[]) { 0x09, 0x04 },	// 0: is supported language is English (0x0409)
	"Raspberry Pi",					// 1: Manufacturer
	"Pico AW410K RGB Controller",	// 2: Product
	NULL,							// 3: Serials, should use chip ID
	"Pico AW410K CDC",				// 4: CDC
	"Pico AWK410K NCM",				// 5: NCM
	NULL,							// 6: MAC address for NCM
	"Pico AW410K HID",				// 7: HID
};

static void usb_device_init(void);

// main task for USB device
void usb_device_main(void) {
	// start ADC for reading LDR
	startADC();

	// initialize the TinyUSB device
	device_state = DEVICE_INACTIVE;
	usb_device_init();

	// start the web server on USB
	usb_server_init();

	while (true) {
		switch ( device_state ) {
			case DEVICE_ACTIVE:
				break;
			case DEVICE_INACTIVE:
				break;
			case DEVICE_RESTART:
				if (tud_disconnect()) {
					sleep_ms(10);
					if (tud_connect()) {
						if ( host_state == HOST_INACTIVE ) {
							device_state = DEVICE_INACTIVE;
						} else {
							device_state = DEVICE_ACTIVE;
						}
					}
				}
				break;
			default:
				break;
		}
		get_light(); // get the ADC LDR reading
		tud_task();
		tud_cdc_write_flush();
	}
}

// initialize the TinyUSB device
static void usb_device_init(void) {
	// run TinyUSB device
	tusb_rhport_init_t dev_init = {
		.role = TUSB_ROLE_DEVICE,
		.speed = TUSB_SPEED_AUTO,
	};
	tusb_init(BOARD_TUH_RHPORT, &dev_init);
}

// Invoked when received GET DEVICE DESCRIPTOR
// Application return pointer to descriptor
uint8_t const * tud_descriptor_device_cb(void)
{
	return (uint8_t const *) &desc_device;
}

// Invoked when received GET CONFIGURATION DESCRIPTOR
// Application return pointer to descriptor
// Descriptor contents must exist long enough for transfer to complete
uint8_t const * tud_descriptor_configuration_cb(uint8_t index)
{
	(void) index; // for multiple configurations

	// set configuration descriptor and CDC descriptor
	memset(desc_configuration, 0, sizeof(desc_configuration));
	uint8_t desc_initial[TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+TUD_CDC_NCM_DESC_LEN+1] = {
			TUD_CONFIG_DESCRIPTOR(1, 4+num_mounted, 0, TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+TUD_CDC_NCM_DESC_LEN+num_mounted*TUD_HID_DESC_LEN, 0x00, 100),
			TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64),
			TUD_CDC_NCM_DESCRIPTOR(ITF_NUM_NCM, 5, 6, EPNUM_NCM_NOTIF, 64, EPNUM_NCM_OUT, EPNUM_NCM_IN, CFG_TUD_NET_ENDPOINT_SIZE, CFG_TUD_NET_MTU)
		};
	memcpy(desc_configuration, desc_initial, TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+TUD_CDC_NCM_DESC_LEN);
	
	// add a HID descriptor for each interface mounted on host
	if ( descriptors != NULL) {
		struct report_desc *descriptor;
		for (uint8_t i=0; i<num_mounted; i++) {
			descriptor = report_desc_find(descriptors->dev_addr, i);
			uint8_t hid_desc[TUD_HID_DESC_LEN+1] = {
					TUD_HID_DESCRIPTOR(ITF_NUM_HID+i, 7, HID_ITF_PROTOCOL_NONE, descriptor->desc_len, EPNUM_HID+i, CFG_TUD_HID_EP_BUFSIZE, 1)
				};
			memcpy(&desc_configuration[TUD_CONFIG_DESC_LEN+TUD_CDC_DESC_LEN+TUD_CDC_NCM_DESC_LEN+i*TUD_HID_DESC_LEN], hid_desc, TUD_HID_DESC_LEN);
		}
	}

	return desc_configuration;
}

// Invoked when received GET STRING DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
	(void) langid;

	uint8_t chr_count;

	switch (index) {
		case 0: // langid
			memcpy(&_desc_str[1], string_desc_arr[0], 2);
			chr_count = 1;
			break;
		case 3: // serial
			chr_count = board_usb_get_serial(_desc_str+1, 32);	
			break;
		case 6: // MAC address: link-local prefix of 02 and last 10 digits from board serial
			chr_count = board_usb_get_serial(_desc_str+1, 32);
			if (chr_count > 12) {
				_desc_str[1] = '0';
				_desc_str[2] = '2';
				for (uint8_t i=0; i<10; i++) {
					_desc_str[3+i] = _desc_str[chr_count-9+i];
				}
				chr_count=12;
			}
			break;
		default:
			// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
			// https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors

			if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL;

			char* str = string_desc_arr[index];

			// Cap at max char
			chr_count = (uint8_t) strlen(str);
			if ( chr_count > 31 ) chr_count = 31;

			// Convert ASCII string into UTF-16
			for(uint8_t i=0; i<chr_count; i++) {
				_desc_str[1+i] = str[i];
			}
			break;
	}

	// first byte is length (including header), second byte is string type
	_desc_str[0] = (TUSB_DESC_STRING << 8 ) | (2*chr_count + 2);

	return _desc_str;
}

// Invoked when received GET HID REPORT DESCRIPTOR
// Application return pointer to descriptor
// Descriptor contents must exist long enough for transfer to complete
uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf)
{
	// find HID report descriptor for indicated interface on the host and forward to device
	if ( descriptors != NULL) {
		struct report_desc * descriptor = report_desc_find(descriptors->dev_addr, itf);
		if ( descriptor != NULL ) {
			return descriptor->descriptor;
		}
	}

	return NULL;
}


// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) {
	// forward to device connected to host
	if (descriptors != NULL) {
		tuh_hid_set_report(descriptors->dev_addr, instance, report_id, report_type, buffer, bufsize);
	}
}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
	(void) instance;
	(void) report_id;
	(void) report_type;
	(void) buffer;
	(void) reqlen;

	return 0;
}

// print message to CDC in raw hex
void cdc_print_hex(uint8_t const* msg, uint16_t msg_len) {
	(void) msg;
	(void) msg_len;
	for (int i=0; i<msg_len; i++) {
		cdc_count=sprintf(cdc_buf, "%02X ", msg[i]);
		tud_cdc_write(cdc_buf, cdc_count);
	}
	tud_cdc_write_str("\n");
}
