// clang-format off
// Don't format because the order is important here
#include <windows.h>
#include <setupapi.h>
// clang-format on

#include "ezusb/ezusb.h"

#include "ezusb2/cyioctl.h"
#include "ezusb2/ezusb2.h"

#include "util/crc.h"
#include "util/fs.h"
#include "util/log.h"
#include "util/str.h"

#define REQ_TYPE_HOST_TO_DEV 0x40
#define REQ_TYPE_DEV_TO_HOST 0x80

#define REQ_TIMEOUT_SEC 5
#define REQ_CODE_EP0 0x00

#define EP0_GET_DESCRIPTOR 0x06
#define EP0_EZUSB_WRITE_RAM 0xA0

/* nope, it's not 0x01, probably an endianess mistake (?) */
#define EP0_DESCRIPTOR_TYPE_DEVICE 0x0100
/* Also not 0x03, 0x0301 from DeviceIoCtl dump */
#define EP0_DESCRIPTOR_TYPE_STRING 0x0301

#define EP0_EZUSB_RAM_CPU_RESET_ADDR 0xE600

static int ezusb2_ctrl_transfer(
    HANDLE handle,
    uint32_t ioctl_code,
    uint8_t endpoint,
    uint8_t req_type,
    uint8_t req,
    uint16_t value,
    uint16_t index,
    void *buf,
    uint32_t buf_len)
{
    int xmit_buf_size = sizeof(SINGLE_TRANSFER) + buf_len;
    char xmit_buf[xmit_buf_size];
    DWORD ret_len;

    memset(xmit_buf, 0, xmit_buf_size);

    PSINGLE_TRANSFER transfer = (PSINGLE_TRANSFER) xmit_buf;

    memset(transfer, 0, sizeof(SINGLE_TRANSFER));

    transfer->SetupPacket.bmRequest = req_type;
    transfer->SetupPacket.bRequest = req;
    transfer->SetupPacket.wValue = value;
    transfer->SetupPacket.wIndex = index;
    transfer->SetupPacket.wLength = buf_len;
    /* Timeout is in seconds, wtf ?! */
    transfer->SetupPacket.ulTimeOut = REQ_TIMEOUT_SEC;
    transfer->ucEndpointAddress = endpoint;
    transfer->BufferOffset = sizeof(SINGLE_TRANSFER);
    transfer->BufferLength = buf_len;
    /* All other attributes 0 */

    if (buf) {
        memcpy(xmit_buf + sizeof(SINGLE_TRANSFER), buf, buf_len);
    }

    if (!DeviceIoControl(
            handle,
            ioctl_code,
            xmit_buf,
            xmit_buf_size,
            xmit_buf,
            xmit_buf_size,
            &ret_len,
            NULL)) {
        return -1;
    }

    if (buf) {
        memcpy(buf, xmit_buf + sizeof(SINGLE_TRANSFER), buf_len);
    }

    return ret_len - sizeof(SINGLE_TRANSFER);
}

static int ezusb2_ep0_transfer(
    HANDLE handle,
    uint8_t req_type,
    uint8_t req,
    uint16_t value,
    uint16_t index,
    void *buf,
    uint32_t buf_len)
{
    return ezusb2_ctrl_transfer(
        handle,
        IOCTL_ADAPT_SEND_EP0_CONTROL_TRANSFER,
        REQ_CODE_EP0,
        req_type,
        req,
        value,
        index,
        buf,
        buf_len);
}

/* Any other endpoint than 0 */
static int ezusb2_epx_transfer(
    HANDLE handle,
    uint8_t endpoint,
    uint8_t req_type,
    uint8_t req,
    uint16_t value,
    uint16_t index,
    void *buf,
    uint32_t buf_len)
{
    return ezusb2_ctrl_transfer(
        handle,
        IOCTL_ADAPT_SEND_NON_EP0_TRANSFER,
        endpoint,
        req_type,
        req,
        value,
        index,
        buf,
        buf_len);
}

static char *ezusb2_get_device_name(HANDLE handle)
{
    const int len = 256;
    char buf[len];
    DWORD received;

    memset(&buf, 0, len);

    if (!DeviceIoControl(
            handle,
            IOCTL_ADAPT_GET_FRIENDLY_NAME,
            &buf,
            len,
            &buf,
            len,
            &received,
            NULL)) {
        return NULL;
    }

    return str_dup((const char *) &buf);
}

static bool
ezusb2_get_device_descriptor(HANDLE handle, USB_DEVICE_DESCRIPTOR *desc)
{
    int res;

    memset(desc, 0, sizeof(USB_DEVICE_DESCRIPTOR));

    /* 0: not used */
    res = ezusb2_ep0_transfer(
        handle,
        REQ_TYPE_DEV_TO_HOST,
        EP0_GET_DESCRIPTOR,
        EP0_DESCRIPTOR_TYPE_DEVICE,
        0,
        desc,
        sizeof(USB_DEVICE_DESCRIPTOR));

    if (res != sizeof(USB_DEVICE_DESCRIPTOR)) {
        return false;
    }

    return true;
}

static bool ezusb2_write_ram(
    HANDLE handle, const void *buffer, uint16_t ram_offset, uint32_t size)
{
    return ezusb2_ep0_transfer(
               handle,
               REQ_TYPE_HOST_TO_DEV,
               EP0_EZUSB_WRITE_RAM,
               ram_offset,
               0,
               (void *) buffer,
               size) == size;
}

static bool ezusb2_reset(HANDLE handle, bool hold)
{
    uint8_t flag;

    /* Write CPU state */
    flag = hold ? 1 : 0;

    return ezusb2_write_ram(
        handle, &flag, EP0_EZUSB_RAM_CPU_RESET_ADDR, sizeof(uint8_t));
}

char *ezusb2_find(const GUID *guid)
{
    HDEVINFO info;
    SP_DEVICE_INTERFACE_DATA iface;
    BOOL result;
    ULONG required_len;
    PSP_DEVICE_INTERFACE_DETAIL_DATA detail;
    ULONG length;
    char *res;

    log_assert(guid);

    result = FALSE;
    required_len = 0;
    detail = NULL;

    info = SetupDiGetClassDevs(
        guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

    if (info == INVALID_HANDLE_VALUE) {
        return NULL;
    }

    /* Enum devices that support the GUID, get first only */
    memset(&iface, 0, sizeof(iface));
    iface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);

    result = SetupDiEnumDeviceInterfaces(info, NULL, guid, 0, &iface);

    if (!result) {
        SetupDiDestroyDeviceInfoList(info);
        return NULL;
    }

    /* Get information about the found device interface. We don't
       know how much memory to allocate to get this information, so
       we will ask by passing in a null buffer and location to
       receive the size of the buffer needed. */
    result = SetupDiGetDeviceInterfaceDetail(
        info, &iface, NULL, 0, &required_len, NULL);

    if (!required_len) {
        return NULL;
    }

    /* Okay, we got a size back, so let's allocate memory
       for the interface detail information we want. */
    detail =
        (PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LMEM_FIXED, required_len);

    if (detail == NULL) {
        SetupDiDestroyDeviceInfoList(info);
        return NULL;
    }

    detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
    length = required_len;

    result = SetupDiGetDeviceInterfaceDetail(
        info, &iface, detail, length, &required_len, NULL);

    if (!result) {
        SetupDiDestroyDeviceInfoList(info);
        LocalFree(detail);
        return NULL;
    }

    res = str_dup((const char *) &detail->DevicePath);
    LocalFree(detail);

    SetupDiDestroyDeviceInfoList(info);

    return res;
}

HANDLE
ezusb2_open(const char *device_path)
{
    log_assert(device_path);

    return CreateFileA(
        device_path,
        GENERIC_WRITE,
        FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);
}

bool ezusb2_get_ident(HANDLE handle, struct ezusb_ident *ident)
{
    char *name;
    USB_DEVICE_DESCRIPTOR desc;

    log_assert(handle != INVALID_HANDLE_VALUE);
    log_assert(handle);
    log_assert(ident);

    memset(ident, 0, sizeof(struct ezusb_ident));

    name = ezusb2_get_device_name(handle);

    if (!name) {
        return false;
    }

    memcpy(ident->name, name, strlen(name));
    free(name);

    if (!ezusb2_get_device_descriptor(handle, &desc)) {
        return false;
    }

    ident->vid = desc.idVendor;
    ident->pid = desc.idProduct;

    return true;
}

bool ezusb2_download_firmware(HANDLE handle, struct ezusb_firmware *fw)
{
    log_assert(handle != INVALID_HANDLE_VALUE);
    log_assert(handle);
    log_assert(fw);

    if (!ezusb2_reset(handle, true)) {
        return false;
    }

    for (uint16_t i = 0; i < fw->segment_count; i++) {
        /* Important: Writing the full binary does NOT work on the FX2 device
           compared to the legacy ezusb device */
        if (!ezusb2_write_ram(
                handle,
                fw->segments[i]->data,
                fw->segments[i]->offset,
                fw->segments[i]->size)) {
            return false;
        }
    }

    if (!ezusb2_reset(handle, false)) {
        return false;
    }

    return true;
}

bool ezusb2_endpoint_transfer(
    HANDLE handle, uint8_t endpoint, void *data, uint32_t size)
{
    if (endpoint == 0) {
        return false;
    }

    return ezusb2_epx_transfer(handle, endpoint, 0, 0, 0, 0, data, size);
}

void ezusb2_close(HANDLE handle)
{
    log_assert(handle != INVALID_HANDLE_VALUE);
    log_assert(handle);

    CloseHandle(handle);
}
