/* libusb/usb_common.c - Common framework for implementing multiple USB devices at once
 *
 * Copyright (C) 2024  Luke T. Shumaker <lukeshu@lukeshu.com>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#include <stdint.h>        /* for uint{n}_t types */
#include <stddef.h>        /* for size_t */
#include <string.h>        /* memcpy() */
#include <stdlib.h>        /* for malloc(), realloc(), reallocarray() */

#include <tusb.h>          /* for various tusb_*_t types */

#include <libmisc/assert.h>

#define LOG_NAME USB_COMMON
#include <libmisc/log.h>

#include <libusb/tusb_helpers.h> /* for LANGID_*, TU_UTF16() */
#include <libusb/usb_common.h>

#include "config.h"

/* Globals ********************************************************************/

uint8_t cfgnum_std = 0;

static struct {
	uint8_t          configc;
	uint8_t        **configv;
	uint16_t        *serial;
	uint8_t          serial_bytelen;
} globals = {0};

/* Strings ********************************************************************/

/**
 * Return a pointer to the USB "String Descriptor" (see USB 2.0 §9.6.7
 * "String").
 */
uint16_t const *tud_descriptor_string_cb(uint8_t strid, uint16_t langid) {
	static struct TU_ATTR_PACKED {
		uint8_t  bLength;
		uint8_t  bDescriptorType;
		uint16_t bString[(0xFF-2)/2];
	} desc;
	TU_VERIFY_STATIC(sizeof desc == (0xFF/2)*2, "incorrect size");

	static uint16_t const langids[] = {
		LANGID_EN_US,
	};

	size_t bytelen = 0;
	if (strid == 0) {
		memcpy(desc.bString, langids, sizeof langids);
		bytelen = sizeof langids;
	} else {
#define CONST_STR(str) bytelen = ((sizeof (str))-1); memcpy(desc.bString, (str), ((sizeof (str))-1));
		switch (langid) {
		case LANGID_EN_US:
			switch (strid) {
			case STRID_MANUF:   CONST_STR(TU_UTF16("Umorpha Systems")); break;
			case STRID_PRODUCT: CONST_STR(TU_UTF16("SBC-Harness Keyboard")); break;
			case STRID_CFG:     CONST_STR(TU_UTF16("Standard Configuration")); break;
			case STRID_KBD_IFC: CONST_STR(TU_UTF16("Keyboard Interface")); break;
			case STRID_SERIAL:
				bytelen = globals.serial_bytelen;
				memcpy(desc.bString, globals.serial, bytelen);
				break;
			default:
				debugf("GET STRING: unknown string id=%"PRIu8, strid);
				return NULL;
			}
			break;
		default:
			debugf("GET STRING: unknown LANGID=%"PRIx16, langid);
			return NULL;
		}
	}
	assert(bytelen <= sizeof desc.bString);
	desc.bLength = bytelen + 2;
	desc.bDescriptorType = TUSB_DESC_STRING;
	return (uint16_t *)&desc;
}

/* Main entrypoints ***********************************************************/

void usb_common_earlyinit(uint16_t *serial, uint8_t serial_bytelen) {
	globals.serial = serial;
	globals.serial_bytelen = serial_bytelen;
	if (cfgnum_std)
		return;
	cfgnum_std = usb_add_config(
		STRID_CFG,                          /* iConfiguration      ; Index of string descriptor describing this configuration */
		TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, /* bmAttributes        ; Bitmap of flags (self-powered and remote-wakeup are the only flags defined in USB 2.0) */
		100);                               /* bMaxPower (in mA)   ; Maximum power consumption of the device when in this configuration */
}

void usb_common_lateinit(void) {
	tud_init(CONFIG_USB_COMMON_RHPORT);
}

COROUTINE usb_common_cr(void *_arg) {
	(void) _arg;
	cr_begin();

	for (;;) {
		tud_task();
		cr_yield();
	}

	cr_end();
}

/* Main utilities *************************************************************/

uint8_t usb_add_config(uint8_t iConfiguration, uint8_t bmAttributes, uint8_t bMaxPower_mA) {
	uint8_t *desc;

	globals.configv = reallocarray(globals.configv, ++globals.configc, sizeof globals.configv[0]);
	desc = globals.configv[globals.configc-1] = malloc(TUD_CONFIG_DESC_LEN);

	memcpy(desc, (uint8_t[]){
			/* USB configuration descriptor header (USB 2.0 §9.6.4 "Configuration") */
			TUD_CONFIG_DESCRIPTOR(
				globals.configc,     /* bConfigurationValue */
				0,                   /* bNumInterfaces (will be incremented by usb_add_interface() */
				iConfiguration,      /* iConfiguration */
				TUD_CONFIG_DESC_LEN, /* wTotalLength (will be incremented by usb_add_interface() */
				bmAttributes,        /* bmAttributes */
				bMaxPower_mA+1),     /* bMaxPower (+1 because tusb just does n/2 instead of (n+1)/2) */
		}, TUD_CONFIG_DESC_LEN);

	return globals.configc;
}

uint8_t usb_add_interface(uint8_t cfg_num, uint16_t ifc_len, uint8_t *ifc_dat) {
	assert(cfg_num > 0 && cfg_num <= globals.configc);
	assert(ifc_len >= 3);
	assert(ifc_dat);

	uint8_t *desc = globals.configv[cfg_num-1];
	// wTotalLength
	uint16_t total_len = TU_U16(desc[3], desc[2]) + ifc_len;
	desc[3] = TU_U16_HIGH(total_len);
	desc[2] = TU_U16_LOW(total_len);
	// bNumInterfaces
	ifc_dat[3] = desc[4]++;

	desc = globals.configv[cfg_num-1] = realloc(desc, total_len);
	memcpy(&desc[total_len-ifc_len], ifc_dat, ifc_len);

	return ifc_dat[3];
}

/**
 * Return a pointer to the USB "Device Descriptor" (see USB 2.0 §9.6.1
 * "Device") as a byte-array.
 */
uint8_t const *tud_descriptor_device_cb(void) {
	/* Our device descriptor is static, so just declare it as a
	 * static const.  */
	static tusb_desc_device_t desc = {
		.bLength            = sizeof(tusb_desc_device_t),
		.bDescriptorType    = TUSB_DESC_DEVICE,
		.bcdUSB             = 0x200, /* USB 2.0 */

		/* Use the "base device class", which means to use information
		 * from interface descriptors instead of a global device
		 * descriptor. */
		.bDeviceClass       = 0x00,
		.bDeviceSubClass    = 0x00,
		.bDeviceProtocol    = 0x00,

		.bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,

		.idVendor           = 0xCAFE,        /* Vendor ID (assigned by the USB-IF) */
		.idProduct          = 0x4001,        /* Product ID (assigned by the manufacturer) */
		.bcdDevice          = 0x0100,        /* Device release number */

		.iManufacturer      = STRID_MANUF,   /* Index of string descriptor describing manufacturer */
		.iProduct           = STRID_PRODUCT, /* Index of string descriptor describing product */
		.iSerialNumber      = STRID_SERIAL,  /* Index of string descriptor describing the device's serial number */

		.bNumConfigurations = 0,             /* Number of possible configurations */
	};
	desc.bNumConfigurations = globals.configc;
	return (uint8_t const *) &desc;
}

/**
 * Return a pointer to the USB config "Configuration Descriptor" (see
 * USB 2.0 §9.6.4 "Configuration") for the given index.
 */
uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
	if (index >= globals.configc)
		return NULL;
	return globals.configv[index];
}