#include "usb.h"
#include "usbd_desc.h"
#include "usbd_core.h"

USBD_HandleTypeDef hUsbDeviceFS;
extern PCD_HandleTypeDef hpcd_USB_FS;

#define EP_DATA_IN_ADDRESS		0x81
#define EP_DATA_OUT_ADDRESS		0x01
#define EP_LOG_IN_ADDRESS		0x82

static uint8_t  USBD_Class_Init (USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t  USBD_Class_DeInit (USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t  USBD_Class_Setup(USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef  *req);
static uint8_t  USBD_Class_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t  USBD_Class_DataOut (USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t  *USBD_Class_GetFSCfgDesc (uint16_t *length);
static uint8_t  *USBD_Class_GetDeviceQualifierDescriptor (uint16_t *length);

static usbd_recv_callback_t cb;
static uint8_t usb_receive_buffer[1024];
static uint8_t usb_transmit_fifo[6144];
static uint16_t usb_transmit_read_index = 0;
static uint16_t usb_transmit_fifo_level = 0;
static bool data_transmission_active = false;
static bool log_transmission_active = true;
static uint32_t last_transmission = 0;

#define USB_TRANMISSION_TIMEOUT		100

USBD_ClassTypeDef  USBD_ClassDriver =
{
  USBD_Class_Init,
  USBD_Class_DeInit,
  USBD_Class_Setup,
  NULL,
  NULL,
  USBD_Class_DataIn,
  USBD_Class_DataOut,
  NULL,
  NULL,
  NULL,
  NULL,
  USBD_Class_GetFSCfgDesc,
  NULL,
  USBD_Class_GetDeviceQualifierDescriptor,
};

/* USB Standard Device Descriptor */
__ALIGN_BEGIN static uint8_t USBD_DeviceQualifierDesc[USB_LEN_DEV_QUALIFIER_DESC] __ALIGN_END =
{
  USB_LEN_DEV_QUALIFIER_DESC,
  USB_DESC_TYPE_DEVICE_QUALIFIER,
  0x00,
  0x02,
  0x00,
  0x00,
  0x00,
  0x40,
  0x01,
  0x00,
};

#define USB_CONFIG_DESC_SIZ		39

/* USB CDC device Configuration Descriptor */
__ALIGN_BEGIN uint8_t USBD_CfgFSDesc[USB_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09,                          /* bLength: Configuation Descriptor size */
  USB_DESC_TYPE_CONFIGURATION,   /* bDescriptorType: Configuration */
  USB_CONFIG_DESC_SIZ,  /* wTotalLength: Bytes returned */
  0x00,
  0x01,                          /*bNumInterfaces: 1 interface*/
  0x01,                          /*bConfigurationValue: Configuration value*/
  0x00,                          /*iConfiguration */
  0xC0,                          /*bmAttributes: bus powered and Supports Remote Wakeup */
  0x32,                          /*MaxPower 100 mA: this current is used for detecting Vbus*/
  /* Interface */
  0x09,                     /* bLength */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
  0x00,                     /* bInterfaceNumber */
  0x00,                     /* bAlternateSetting */
  0x03,                     /* bNumEndpoints */
  0xFF,                     /* bInterfaceClass */
  0x00,                     /* bInterfaceSubClass */
  0x00,                     /* bInterfaceProtocol */
  0x00,                     /* iInterface */
  /* Endpoint Data OUT */
  0x07,                            /* bLength */
  USB_DESC_TYPE_ENDPOINT,          /* bDescriptorType */
  EP_DATA_OUT_ADDRESS,			           /* bEndpointAddress */
  0x02,                            /* bmAttributes */
  LOBYTE(USB_FS_MAX_PACKET_SIZE),  /* wMaxPacketSize */
  HIBYTE(USB_FS_MAX_PACKET_SIZE),
  0x00,                            /* bInterval */
  /* Endpoint Data IN */
  0x07,                             /* bLength */
  USB_DESC_TYPE_ENDPOINT,           /* bDescriptorType */
  EP_DATA_IN_ADDRESS,               /* bEndpointAddress */
  0x02,                             /* bmAttributes */
  LOBYTE(USB_FS_MAX_PACKET_SIZE),   /* wMaxPacketSize */
  HIBYTE(USB_FS_MAX_PACKET_SIZE),
  0x00,                              /* bInterval */
  /* Endpoint logging IN */
  0x07,                             /* bLength */
  USB_DESC_TYPE_ENDPOINT,           /* bDescriptorType */
  EP_LOG_IN_ADDRESS,               	/* bEndpointAddress */
  0x02,                             /* bmAttributes */
  LOBYTE(USB_FS_MAX_PACKET_SIZE),   /* wMaxPacketSize */
  HIBYTE(USB_FS_MAX_PACKET_SIZE),
  0x00                              /* bInterval */
};

// See https://github.com/pbatard/libwdi/wiki/WCID-Devices for descriptor data
// This requests to load the WinUSB driver for this device
__ALIGN_BEGIN const char USBD_MicrosoftCompatibleID[40] __ALIGN_END =
{
	0x28, 0x00, 0x00, 0x00,
	0x00, 0x01,
	0x04, 0x00,
	0x01,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00,
	0x01,
	'W','I','N','U','S','B','\0','\0',
	0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

static uint8_t  USBD_Class_Init (USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{
	// Open endpoints and start reception
	USBD_LL_OpenEP(pdev, EP_DATA_IN_ADDRESS, USBD_EP_TYPE_BULK, USB_FS_MAX_PACKET_SIZE);
	USBD_LL_OpenEP(pdev, EP_DATA_OUT_ADDRESS, USBD_EP_TYPE_BULK, USB_FS_MAX_PACKET_SIZE);
	USBD_LL_OpenEP(pdev, EP_LOG_IN_ADDRESS, USBD_EP_TYPE_BULK, USB_FS_MAX_PACKET_SIZE);
	USBD_LL_PrepareReceive(pdev, EP_DATA_OUT_ADDRESS, usb_receive_buffer,	USB_FS_MAX_PACKET_SIZE);
	return USBD_OK;
}
static uint8_t  USBD_Class_DeInit(USBD_HandleTypeDef *pdev,
                                     uint8_t cfgidx)
{
  USBD_LL_CloseEP(pdev, EP_DATA_IN_ADDRESS);
  USBD_LL_CloseEP(pdev, EP_DATA_OUT_ADDRESS);
  USBD_LL_CloseEP(pdev, EP_LOG_IN_ADDRESS);
  return USBD_OK;
}
static uint8_t USBD_Class_Setup(USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef  *req) {
	if(req->wIndex == 0x0004 && req->bRequest == USB_WCID_VENDOR_CODE && req->bmRequest == 0xC0) {
		// This is a request for the Microsoft Compatible IF Feature Descriptor
		USBD_CtlSendData (pdev, (uint8_t *)(void *)USBD_MicrosoftCompatibleID, req->wLength);
	}
	return USBD_OK;
}

static bool trigger_next_fifo_transmission() {
	data_transmission_active = true;
	uint16_t continous_length = sizeof(usb_transmit_fifo) - usb_transmit_read_index;
	if(continous_length > usb_transmit_fifo_level) {
		continous_length = usb_transmit_fifo_level;
	}
	if(continous_length > sizeof(usb_transmit_fifo)/ 2) {
		continous_length = sizeof(usb_transmit_fifo) / 2;
	}
	hUsbDeviceFS.ep_in[EP_DATA_IN_ADDRESS & 0x7F].total_length = continous_length;
	return USBD_LL_Transmit(&hUsbDeviceFS, EP_DATA_IN_ADDRESS, &usb_transmit_fifo[usb_transmit_read_index], continous_length) == USBD_OK;
}

static bool connection_okay() {
	if(data_transmission_active && HAL_GetTick() - last_transmission > USB_TRANMISSION_TIMEOUT) {
		return false;
	} else {
		return true;
	}
}

static uint8_t USBD_Class_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) {
	// A bulk transfer is complete when the endpoint does on of the following:
	// - Has transferred exactly the amount of data expected
	// - Transfers a packet with a payload size less than wMaxPacketSize or transfers a zero-length packet
	if(epnum == (EP_DATA_IN_ADDRESS & 0x7F)) {
		// transmission of fifo data, mark as empty
		__disable_irq();
		usb_transmit_fifo_level -= pdev->ep_in[epnum].total_length;
		usb_transmit_read_index += pdev->ep_in[epnum].total_length;
		usb_transmit_read_index %= sizeof(usb_transmit_fifo);
		__enable_irq();
	}
	if (pdev->ep_in[epnum].total_length
			&& !(pdev->ep_in[epnum].total_length % USB_FS_MAX_PACKET_SIZE)) {
		pdev->ep_in[epnum].total_length = 0;
		USBD_LL_Transmit(pdev, epnum, NULL, 0);
	} else {
		if(epnum == (EP_DATA_IN_ADDRESS & 0x7F)) {
			if(usb_transmit_fifo_level > 0) {
				trigger_next_fifo_transmission();
			} else {
				data_transmission_active = false;
			}
		} else {
			log_transmission_active = false;
		}
	}
	last_transmission = HAL_GetTick();
	return USBD_OK;
}
static uint8_t  USBD_Class_DataOut(USBD_HandleTypeDef *pdev,
                                      uint8_t epnum)
{
  if(cb) {
	  cb(usb_receive_buffer, USBD_LL_GetRxDataSize (pdev, epnum));
  }
  USBD_LL_PrepareReceive(pdev, EP_DATA_OUT_ADDRESS, usb_receive_buffer, USB_FS_MAX_PACKET_SIZE);
  return USBD_OK;
}
static uint8_t  *USBD_Class_GetFSCfgDesc(uint16_t *length)
{
  *length = sizeof(USBD_CfgFSDesc);
  return USBD_CfgFSDesc;
}
static uint8_t  *USBD_Class_GetDeviceQualifierDescriptor(uint16_t *length)
{
  *length = sizeof(USBD_DeviceQualifierDesc);
  return USBD_DeviceQualifierDesc;
}

void usb_init(usbd_recv_callback_t receive_callback) {
	cb = receive_callback;
	USBD_Init(&hUsbDeviceFS, &FS_Desc, 0);
	USBD_RegisterClass(&hUsbDeviceFS, &USBD_ClassDriver);
	USBD_Start(&hUsbDeviceFS);
    HAL_NVIC_SetPriority(USB_HP_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USB_HP_IRQn);
    HAL_NVIC_SetPriority(USB_LP_IRQn, 5, 0);
	HAL_NVIC_EnableIRQ(USB_LP_IRQn);
}
bool usb_transmit(const uint8_t *data, uint16_t length) {
	if(!connection_okay()) {
		// clear buffer and attempt to restart the transfer
		data_transmission_active = false;
		usb_transmit_fifo_level = 0;
	}
	// attempt to add data to fifo
	if(length > usb_available_buffer()) {
		// data won't fit, abort
		return false;
	}
	// grab pointer to write position
	__disable_irq();
	uint16_t write_index = usb_transmit_read_index + usb_transmit_fifo_level;
	write_index %= sizeof(usb_transmit_fifo);
	// copy the data to the fifo
	uint16_t continous_length = sizeof(usb_transmit_fifo) - write_index;
	if(continous_length > length) {
		// can copy all data at once
		memcpy(&usb_transmit_fifo[write_index], data, length);
	} else {
		// needs to copy two data segments
		memcpy(&usb_transmit_fifo[write_index], data, continous_length);
		memcpy(&usb_transmit_fifo[0], data + continous_length, length - continous_length);
	}
	// increment fifo level
	usb_transmit_fifo_level += length;

	static bool first = true;
	if(first) {
		log_transmission_active = false;
		first = false;
	}
	bool ret = true;
	if(!data_transmission_active) {
		ret = trigger_next_fifo_transmission();
	}
	__enable_irq();
	return ret;
}

void usb_log(const char *log, uint16_t length) {
	if(!log_transmission_active) {
		static uint8_t buffer[256];
		memcpy(buffer, log, length);
		log_transmission_active = true;
		hUsbDeviceFS.ep_in[EP_LOG_IN_ADDRESS & 0x7F].total_length = length;
		USBD_LL_Transmit(&hUsbDeviceFS, EP_LOG_IN_ADDRESS, buffer, length);
	} else {
		// still busy, unable to send log
	}
}

void USB_HP_IRQHandler(void)
{
  HAL_PCD_IRQHandler(&hpcd_USB_FS);
}
void USB_LP_IRQHandler(void)
{
  HAL_PCD_IRQHandler(&hpcd_USB_FS);
}

uint16_t usb_available_buffer() {
	return sizeof(usb_transmit_fifo) - usb_transmit_fifo_level;
}

void usb_clear_buffer() {
	usb_transmit_fifo_level = 0;
	data_transmission_active = 0;
}
