/*
 * SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <string.h>
#include "esp_log.h"
#include "esp_check.h"
#include "esp_err.h"
#include "descriptors_control.h"
#include "usb_descriptors.h"

#define MAX_DESC_BUF_SIZE 32               // Max length of string descriptor (can be extended, USB supports lengths up to 255 bytes)

static const char *TAG = "tusb_desc";

// =============================================================================
// STRUCTS
// =============================================================================

/**
 * @brief Descriptor pointers for tinyusb descriptor requests callbacks
 *
 */
typedef struct {
    const tusb_desc_device_t *dev;      /*!< Pointer to device descriptor */
    union {
        const uint8_t *cfg;             /*!< Pointer to FullSpeed configuration descriptor when device one-speed only */
        const uint8_t *fs_cfg;          /*!< Pointer to FullSpeed configuration descriptor when device support HighSpeed */
    };
#if (TUD_OPT_HIGH_SPEED)
    const uint8_t *hs_cfg;              /*!< Pointer to HighSpeed configuration descriptor */
    const tusb_desc_device_qualifier_t *qualifier;            /*!< Pointer to Qualifier descriptor */
    uint8_t *other_speed;               /*!< Pointer for other speed configuration descriptor */
#endif // TUD_OPT_HIGH_SPEED
    const char *str[USB_STRING_DESCRIPTOR_ARRAY_SIZE];  /*!< Pointer to array of UTF-8 strings */
    int str_count;                      /*!< Number of descriptors in str */
} tinyusb_descriptor_config_t;

static tinyusb_descriptor_config_t s_desc_cfg;

// =============================================================================
// CALLBACKS
// =============================================================================

/**
 * @brief Invoked when received GET DEVICE DESCRIPTOR.
 * Descriptor contents must exist long enough for transfer to complete
 *
 * @return Pointer to device descriptor
 */
uint8_t const *tud_descriptor_device_cb(void)
{
    assert(s_desc_cfg.dev);
    return (uint8_t const *)s_desc_cfg.dev;
}

/**
 * @brief Invoked when received GET CONFIGURATION DESCRIPTOR.
 * Descriptor contents must exist long enough for transfer to complete
 *
 * @param[in] index Index of required configuration
 * @return Pointer to configuration descriptor
 */
uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
{
    (void)index; // Unused, this driver supports only 1 configuration
    assert(s_desc_cfg.cfg);

#if (TUD_OPT_HIGH_SPEED)
    // HINT: cfg and fs_cfg are union, no need to assert(fs_cfg)
    assert(s_desc_cfg.hs_cfg);
    // Return configuration descriptor based on Host speed
    return (TUSB_SPEED_HIGH == tud_speed_get())
           ? s_desc_cfg.hs_cfg
           : s_desc_cfg.fs_cfg;
#else
    return s_desc_cfg.cfg;
#endif // TUD_OPT_HIGH_SPEED
}

#if (TUD_OPT_HIGH_SPEED)
/**
 * @brief Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request
 * Descriptor contents must exist long enough for transfer to complete
 * If not highspeed capable stall this request
 */
uint8_t const *tud_descriptor_device_qualifier_cb(void)
{
    assert(s_desc_cfg.qualifier);
    return (uint8_t const *)s_desc_cfg.qualifier;
}

/**
 * @brief Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request
 * Descriptor contents must exist long enough for transfer to complete
 * Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa
 */
uint8_t const *tud_descriptor_other_speed_configuration_cb(uint8_t index)
{
    assert(s_desc_cfg.other_speed);

    const uint8_t *other_speed = (TUSB_SPEED_HIGH == tud_speed_get())
                                 ? s_desc_cfg.fs_cfg
                                 : s_desc_cfg.hs_cfg;

    memcpy(s_desc_cfg.other_speed,
           other_speed,
           ((tusb_desc_configuration_t *)other_speed)->wTotalLength);

    ((tusb_desc_configuration_t *)s_desc_cfg.other_speed)->bDescriptorType = TUSB_DESC_OTHER_SPEED_CONFIG;
    return s_desc_cfg.other_speed;
}
#endif // TUD_OPT_HIGH_SPEED

/**
 * @brief Invoked when received GET STRING DESCRIPTOR request
 *
 * @param[in] index   Index of required descriptor
 * @param[in] langid  Language of the descriptor
 * @return Pointer to UTF-16 string descriptor
 */
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
    (void) langid; // Unused, this driver supports only one language in string descriptors
    assert(s_desc_cfg.str);
    uint8_t chr_count;
    static uint16_t _desc_str[MAX_DESC_BUF_SIZE];

    if (index == 0) {
        memcpy(&_desc_str[1], s_desc_cfg.str[0], 2);
        chr_count = 1;
    } else {
        if (index >= USB_STRING_DESCRIPTOR_ARRAY_SIZE) {
            ESP_LOGW(TAG, "String index (%u) is out of bounds, check your string descriptor", index);
            return NULL;
        }

        if (s_desc_cfg.str[index] == NULL) {
            ESP_LOGW(TAG, "String index (%u) points to NULL, check your string descriptor", index);
            return NULL;
        }

        const char *str = s_desc_cfg.str[index];
        chr_count = strnlen(str, MAX_DESC_BUF_SIZE - 1); // Buffer len - header

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

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

    return _desc_str;
}

// =============================================================================
// Driver functions
// =============================================================================
esp_err_t tinyusb_set_descriptors(const tinyusb_config_t *config)
{
    esp_err_t ret = ESP_FAIL;
    assert(config);
    const char **pstr_desc;
    // Flush descriptors control struct
    memset(&s_desc_cfg, 0x00, sizeof(tinyusb_descriptor_config_t));
    // Parse configuration and save descriptors's pointer
    // Select Device Descriptor
    if (config->device_descriptor == NULL) {
        ESP_LOGW(TAG, "No Device descriptor provided, using default.");
        s_desc_cfg.dev = &descriptor_dev_default;
    } else {
        s_desc_cfg.dev = config->device_descriptor;
    }

    // Select FullSpeed configuration descriptor
    if (config->configuration_descriptor == NULL) {
        // Default configuration descriptor is provided only for CDC, MSC and NCM classes
#if (CFG_TUD_HID > 0 || CFG_TUD_MIDI > 0 || CFG_TUD_CUSTOM_CLASS > 0 || CFG_TUD_ECM_RNDIS > 0 || CFG_TUD_DFU > 0 || CFG_TUD_DFU_RUNTIME > 0 || CFG_TUD_BTH > 0)
        ESP_GOTO_ON_FALSE(config->configuration_descriptor, ESP_ERR_INVALID_ARG, fail, TAG, "Configuration descriptor must be provided for this device");
#else
        ESP_LOGW(TAG, "No FullSpeed configuration descriptor provided, using default.");
        s_desc_cfg.cfg = descriptor_fs_cfg_default;
#endif
    } else {
        s_desc_cfg.cfg = config->configuration_descriptor;
    }

#if (TUD_OPT_HIGH_SPEED)
    // High Speed
    if (config->hs_configuration_descriptor == NULL) {
        // Default configuration descriptor is provided only for CDC, MSC and NCM classes
#if (CFG_TUD_HID > 0 || CFG_TUD_MIDI > 0 || CFG_TUD_CUSTOM_CLASS > 0 || CFG_TUD_ECM_RNDIS > 0 || CFG_TUD_DFU > 0 || CFG_TUD_DFU_RUNTIME > 0 || CFG_TUD_BTH > 0)
        ESP_GOTO_ON_FALSE(config->hs_configuration_descriptor, ESP_ERR_INVALID_ARG, fail, TAG, "HighSpeed configuration descriptor must be provided for this device");
#else
        ESP_LOGW(TAG, "No HighSpeed configuration descriptor provided, using default.");
        s_desc_cfg.hs_cfg = descriptor_hs_cfg_default;
#endif
    } else {
        s_desc_cfg.hs_cfg = config->hs_configuration_descriptor;
    }

    // HS and FS cfg desc should be equal length
    ESP_GOTO_ON_FALSE(((tusb_desc_configuration_t *)s_desc_cfg.hs_cfg)->wTotalLength ==
                      ((tusb_desc_configuration_t *)s_desc_cfg.fs_cfg)->wTotalLength,
                      ESP_ERR_INVALID_ARG, fail, TAG, "HighSpeed and FullSpeed configuration descriptors must be same length");

    // Qualifier Descriptor
    if (config->qualifier_descriptor == NULL) {
        ESP_GOTO_ON_FALSE((s_desc_cfg.dev == &descriptor_dev_default), ESP_ERR_INVALID_ARG, fail, TAG, "Qualifier descriptor must be present (Device Descriptor not default).");
        // Get default qualifier if device descriptor is default
        ESP_LOGW(TAG, "No Qulifier descriptor provided, using default.");
        s_desc_cfg.qualifier = &descriptor_qualifier_default;
    } else {
        s_desc_cfg.qualifier = config->qualifier_descriptor;
    }

    // Other Speed buffer allocate
    s_desc_cfg.other_speed = calloc(1, ((tusb_desc_configuration_t *)s_desc_cfg.hs_cfg)->wTotalLength);
    ESP_GOTO_ON_FALSE(s_desc_cfg.other_speed, ESP_ERR_NO_MEM, fail, TAG, "Other speed memory allocation error");
#endif // TUD_OPT_HIGH_SPEED

    // Select String Descriptors and count them
    if (config->string_descriptor == NULL) {
        ESP_LOGW(TAG, "No String descriptors provided, using default.");
        pstr_desc = descriptor_str_default;
        while (descriptor_str_default[++s_desc_cfg.str_count] != NULL);
    } else {
        pstr_desc = config->string_descriptor;
        s_desc_cfg.str_count = (config->string_descriptor_count != 0)
                               ? config->string_descriptor_count
                               : 8; // '8' is for backward compatibility with esp_tinyusb v1.0.0. Do NOT remove!
    }

    ESP_GOTO_ON_FALSE(s_desc_cfg.str_count <= USB_STRING_DESCRIPTOR_ARRAY_SIZE, ESP_ERR_NOT_SUPPORTED, fail, TAG, "String descriptors exceed limit");
    memcpy(s_desc_cfg.str, pstr_desc, s_desc_cfg.str_count * sizeof(pstr_desc[0]));

    ESP_LOGI(TAG, "\n"
             "┌─────────────────────────────────┐\n"
             "│  USB Device Descriptor Summary  │\n"
             "├───────────────────┬─────────────┤\n"
             "│bDeviceClass       │ %-4u        │\n"
             "├───────────────────┼─────────────┤\n"
             "│bDeviceSubClass    │ %-4u        │\n"
             "├───────────────────┼─────────────┤\n"
             "│bDeviceProtocol    │ %-4u        │\n"
             "├───────────────────┼─────────────┤\n"
             "│bMaxPacketSize0    │ %-4u        │\n"
             "├───────────────────┼─────────────┤\n"
             "│idVendor           │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│idProduct          │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│bcdDevice          │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│iManufacturer      │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│iProduct           │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│iSerialNumber      │ %-#10x  │\n"
             "├───────────────────┼─────────────┤\n"
             "│bNumConfigurations │ %-#10x  │\n"
             "└───────────────────┴─────────────┘",
             s_desc_cfg.dev->bDeviceClass, s_desc_cfg.dev->bDeviceSubClass,
             s_desc_cfg.dev->bDeviceProtocol, s_desc_cfg.dev->bMaxPacketSize0,
             s_desc_cfg.dev->idVendor, s_desc_cfg.dev->idProduct, s_desc_cfg.dev->bcdDevice,
             s_desc_cfg.dev->iManufacturer, s_desc_cfg.dev->iProduct, s_desc_cfg.dev->iSerialNumber,
             s_desc_cfg.dev->bNumConfigurations);

    return ESP_OK;

fail:
#if (TUD_OPT_HIGH_SPEED)
    free(s_desc_cfg.other_speed);
#endif // TUD_OPT_HIGH_SPEED
    return ret;
}

void tinyusb_set_str_descriptor(const char *str, int str_idx)
{
    assert(str_idx < USB_STRING_DESCRIPTOR_ARRAY_SIZE);
    s_desc_cfg.str[str_idx] = str;
}

void tinyusb_free_descriptors(void)
{
#if (TUD_OPT_HIGH_SPEED)
    assert(s_desc_cfg.other_speed);
    free(s_desc_cfg.other_speed);
#endif // TUD_OPT_HIGH_SPEED
}
