#include <string.h>

#include "esp_err.h"
#include "esp_log.h"

#include "tinyusb.h"
#include "tusb.h"
#include "class/vendor/vendor_device.h"

#include "webusb_transport.h"

#define WEBUSB_VENDOR_REQUEST_WEBUSB     1
#define WEBUSB_VENDOR_REQUEST_MICROSOFT  2

#define WEBUSB_EP_OUT                    0x01
#define WEBUSB_EP_IN                     0x81
#define WEBUSB_ITF_VENDOR                0
#define WEBUSB_ITF_TOTAL                 1

#define WEBUSB_MAX_FRAME                 4096
#define WEBUSB_RX_RING_CAP               (WEBUSB_MAX_FRAME + 16)

#define WEBUSB_BOS_TOTAL_LEN             (TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + TUD_BOS_MICROSOFT_OS_DESC_LEN)
#define WEBUSB_MS_OS_20_DESC_LEN         0xB2

#define WEBUSB_URL "example.com/nsigner/feather"

static const char *TAG = "webusb";

static bool s_web_serial_connected = false;
static bool s_inited = false;

static uint8_t s_rx_ring[WEBUSB_RX_RING_CAP];
static size_t s_rx_have = 0;

static const tusb_desc_device_t s_desc_device = {
    .bLength            = sizeof(tusb_desc_device_t),
    .bDescriptorType    = TUSB_DESC_DEVICE,
    .bcdUSB             = 0x0210,
    .bDeviceClass       = TUSB_CLASS_MISC,
    .bDeviceSubClass    = MISC_SUBCLASS_COMMON,
    .bDeviceProtocol    = MISC_PROTOCOL_IAD,
    .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor           = 0x303A,
    .idProduct          = 0x4001,
    .bcdDevice          = 0x0100,
    .iManufacturer      = 0x01,
    .iProduct           = 0x02,
    .iSerialNumber      = 0x03,
    .bNumConfigurations = 0x01,
};

static const uint8_t s_desc_configuration[] = {
    TUD_CONFIG_DESCRIPTOR(1, WEBUSB_ITF_TOTAL, 0, TUD_CONFIG_DESC_LEN + TUD_VENDOR_DESC_LEN, 0x00, 100),
    TUD_VENDOR_DESCRIPTOR(WEBUSB_ITF_VENDOR, 4, WEBUSB_EP_OUT, WEBUSB_EP_IN, TUD_OPT_HIGH_SPEED ? 512 : 64),
};

static const uint8_t s_desc_bos[] = {
    TUD_BOS_DESCRIPTOR(WEBUSB_BOS_TOTAL_LEN, 2),
    TUD_BOS_WEBUSB_DESCRIPTOR(WEBUSB_VENDOR_REQUEST_WEBUSB, 1),
    TUD_BOS_MS_OS_20_DESCRIPTOR(WEBUSB_MS_OS_20_DESC_LEN, WEBUSB_VENDOR_REQUEST_MICROSOFT),
};

static const uint8_t s_desc_ms_os_20[] = {
    U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN),
    U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A),
    U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), WEBUSB_ITF_VENDOR, 0,
    U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A - 0x08),
    U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A - 0x08 - 0x08 - 0x14), U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY),
    U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A),
    'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00,
    't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00,
    'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00,
    U16_TO_U8S_LE(0x0050),
    '{', 0x00, '9', 0x00, '7', 0x00, '5', 0x00, 'F', 0x00, '4', 0x00, '4', 0x00, 'D', 0x00,
    '9', 0x00, '-', 0x00, '0', 0x00, 'D', 0x00, '0', 0x00, '8', 0x00, '-', 0x00, '4', 0x00,
    '3', 0x00, 'F', 0x00, 'D', 0x00, '-', 0x00, '8', 0x00, 'B', 0x00, '3', 0x00, 'E', 0x00,
    '-', 0x00, '1', 0x00, '2', 0x00, '7', 0x00, 'C', 0x00, 'A', 0x00, '8', 0x00, 'A', 0x00,
    'F', 0x00, 'F', 0x00, 'F', 0x00, '9', 0x00, 'D', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00,
};

static const tusb_desc_webusb_url_t s_desc_url = {
    .bLength = 3 + sizeof(WEBUSB_URL) - 1,
    .bDescriptorType = 3,
    .bScheme = 1,
    .url = WEBUSB_URL,
};

static const char *s_string_desc_arr[] = {
    (const char[]) { 0x09, 0x04 },
    "n_signer",
    "Feather S3 TFT n_signer",
    "123456",
    "n_signer WebUSB",
};

bool webusb_transport_connected(void)
{
    return s_web_serial_connected;
}

int webusb_transport_init(void)
{
    tinyusb_config_t tusb_cfg = {
        .device_descriptor = &s_desc_device,
        .string_descriptor = s_string_desc_arr,
        .string_descriptor_count = (int)(sizeof(s_string_desc_arr) / sizeof(s_string_desc_arr[0])),
        .configuration_descriptor = s_desc_configuration,
        .self_powered = false,
        .vbus_monitor_io = -1,
    };

    if (s_inited) {
        return 0;
    }

    if (tinyusb_driver_install(&tusb_cfg) != ESP_OK) {
        ESP_LOGE(TAG, "tinyusb_driver_install failed");
        return -1;
    }

    s_inited = true;
    s_web_serial_connected = false;
    s_rx_have = 0;
    ESP_LOGI(TAG, "WebUSB transport initialized");
    return 0;
}

int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
{
    uint8_t tmp[128];

    if (payload == NULL || out_len == NULL) {
        return -1;
    }

    if (!s_inited || !tud_ready()) {
        return 1;
    }

    while (tud_vendor_available() > 0) {
        uint32_t n = tud_vendor_read(tmp, sizeof(tmp));
        if (n == 0) {
            break;
        }
        if (s_rx_have + n > sizeof(s_rx_ring)) {
            s_rx_have = 0;
        }
        memcpy(s_rx_ring + s_rx_have, tmp, n);
        s_rx_have += n;
    }

    if (s_rx_have < 4) {
        return 1;
    }

    for (;;) {
        uint32_t len = ((uint32_t)s_rx_ring[0] << 24) |
                       ((uint32_t)s_rx_ring[1] << 16) |
                       ((uint32_t)s_rx_ring[2] << 8) |
                       (uint32_t)s_rx_ring[3];

        if (len == 0 || len > payload_max) {
            memmove(s_rx_ring, s_rx_ring + 1, s_rx_have - 1);
            s_rx_have -= 1;
            if (s_rx_have < 4) {
                return 1;
            }
            continue;
        }

        if (s_rx_have < (size_t)len + 4U) {
            return 1;
        }

        memcpy(payload, s_rx_ring + 4, len);
        *out_len = (size_t)len;

        {
            size_t rem = s_rx_have - ((size_t)len + 4U);
            if (rem > 0) {
                memmove(s_rx_ring, s_rx_ring + 4U + len, rem);
            }
            s_rx_have = rem;
        }

        return 0;
    }
}

int webusb_send_frame(const uint8_t *payload, size_t len)
{
    uint8_t hdr[4];

    if (!s_inited || !tud_ready() || payload == NULL || len == 0 || len > UINT32_MAX) {
        return -1;
    }

    hdr[0] = (uint8_t)((len >> 24) & 0xFFU);
    hdr[1] = (uint8_t)((len >> 16) & 0xFFU);
    hdr[2] = (uint8_t)((len >> 8) & 0xFFU);
    hdr[3] = (uint8_t)(len & 0xFFU);

    if (tud_vendor_write(hdr, sizeof(hdr)) != sizeof(hdr)) {
        return -1;
    }
    if (tud_vendor_write(payload, len) != len) {
        return -1;
    }
    if (tud_vendor_write_flush() == 0) {
        return -1;
    }

    return 0;
}

uint8_t const *tud_descriptor_bos_cb(void)
{
    return s_desc_bos;
}

bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request)
{
    if (stage != CONTROL_STAGE_SETUP) {
        return true;
    }

    switch (request->bmRequestType_bit.type) {
    case TUSB_REQ_TYPE_VENDOR:
        if (request->bRequest == WEBUSB_VENDOR_REQUEST_WEBUSB) {
            return tud_control_xfer(rhport, request, (void *)(uintptr_t)&s_desc_url, s_desc_url.bLength);
        }
        if (request->bRequest == WEBUSB_VENDOR_REQUEST_MICROSOFT && request->wIndex == 7) {
            uint16_t total_len;
            memcpy(&total_len, s_desc_ms_os_20 + 8, 2);
            return tud_control_xfer(rhport, request, (void *)(uintptr_t)s_desc_ms_os_20, total_len);
        }
        break;

    case TUSB_REQ_TYPE_CLASS:
        if (request->bRequest == 0x22) {
            s_web_serial_connected = (request->wValue != 0);
            return tud_control_status(rhport, request);
        }
        break;

    default:
        break;
    }

    return false;
}

void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize)
{
    (void)itf;
    (void)buffer;
    (void)bufsize;
}
