#include <Arduino.h>
#include <usb/usb_host.h>
#include "show_desc.hpp"
#include "usbhhelp.hpp"

bool isMIDI = false;
bool isMIDIReady = false;

usb_transfer_t *MIDIOut = NULL;
usb_transfer_t *MIDIIn = NULL;

// -------- UART TO OTHER ESP32 --------
HardwareSerial MidiOutSerial(1);   // UART1

// ------------ PIN MAP FOR NATURAL NOTES ------------
struct NotePin {
    uint8_t noteMod;   // note % 12
    uint8_t gpio;      // output pin
};

// first is Event is note modulo 12, second is output pin
NotePin NotePins[] = {
    {0,  4}, // C
    {2,  5}, // D
    {4,  6}, // E
    {5,  7}, // F
    {7,  8}, // G
    {9,  9}, // A
    {11, 10} // B
};

const int NUM_NOTES = sizeof(NotePins) / sizeof(NotePins[0]);

// Get GPIO pin
int getNotePin(uint8_t note)
{
    uint8_t mod = note % 12;
    for (int i = 0; i < NUM_NOTES; i++) {
        if (NotePins[i].noteMod == mod) return NotePins[i].gpio;
    }
    return -1;
}

// MIDI transfer callback
static void midi_transfer_cb(usb_transfer_t *transfer)
{
    if (Device_Handle != transfer->device_handle) return;

    bool is_in = transfer->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK;

    if (transfer->status == 0 && is_in) {
        uint8_t *p = transfer->data_buffer;

        for (int i = 0; i < transfer->actual_num_bytes; i += 4) {
            if (p[i] == 0 && p[i+1] == 0 && p[i+2] == 0 && p[i+3] == 0)
                break;

            uint8_t status = p[i+1];
            uint8_t data1  = p[i+2];
            uint8_t data2  = p[i+3];
            uint8_t cmd = status & 0xF0;

            // DEBUG: print MIDI data
            Serial.printf("Status: %02X Note: %d Vel: %d\n", status, data1, data2);

            // ---- SEND MIDI TO OTHER ESP32 ----
            MidiOutSerial.write(status);
            MidiOutSerial.write(data1);
            MidiOutSerial.write(data2);

            int pin = getNotePin(data1);
            bool isSpecified = (pin != -1);

            if (cmd == 0x90) { // NOTE ON
                if (data2 == 0) { // velocity 0 = Note OFF
                    if (isSpecified) digitalWrite(pin, LOW);
                } else {
                    if (isSpecified) digitalWrite(pin, HIGH);
                }
            } else if (cmd == 0x80) { // NOTE OFF
                if (isSpecified) digitalWrite(pin, LOW);
            }
        }

        usb_host_transfer_submit(transfer);
    }
}

// ------------ USB HOOKS ------------
void check_interface_desc_MIDI(const void *p)
{
    const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;

    if (intf->bInterfaceClass == USB_CLASS_AUDIO &&
        intf->bInterfaceSubClass == 3 &&
        intf->bInterfaceProtocol == 0)
    {
        isMIDI = true;
        usb_host_interface_claim(Client_Handle, Device_Handle,
                                 intf->bInterfaceNumber,
                                 intf->bAlternateSetting);
    }
}

void prepare_endpoints(const void *p)
{
    const usb_ep_desc_t *ep = (const usb_ep_desc_t *)p;

    if ((ep->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_BULK)
        return;

    if (ep->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
        usb_host_transfer_alloc(ep->wMaxPacketSize, 0, &MIDIIn);
        MIDIIn->device_handle = Device_Handle;
        MIDIIn->bEndpointAddress = ep->bEndpointAddress;
        MIDIIn->callback = midi_transfer_cb;
        MIDIIn->num_bytes = ep->wMaxPacketSize;
        usb_host_transfer_submit(MIDIIn);
    }

    isMIDIReady = (MIDIIn != NULL);
}

void show_config_desc_full(const usb_config_desc_t *cfg)
{
    const uint8_t *p = cfg->val;
    uint8_t len = 0;

    for (int i = 0; i < cfg->wTotalLength; i += len, p += len) {
        len = p[0];
        if (len == 0) return;

        uint8_t type = p[1];
        if (type == USB_B_DESCRIPTOR_TYPE_INTERFACE) check_interface_desc_MIDI(p);
        if (type == USB_B_DESCRIPTOR_TYPE_ENDPOINT && isMIDI && !isMIDIReady)
            prepare_endpoints(p);
    }
}

// ------------ SETUP & LOOP ------------
void setup()
{
    Serial.begin(115200);
    delay(1000);
    Serial.println("USB MIDI Reader Starting...");

    // ---- UART INIT (TO OTHER ESP32) ----
    MidiOutSerial.begin(1000000, SERIAL_8N1, 16, 17); // RX, TX

    // init note pins
    for (int i = 0; i < NUM_NOTES; i++) {
        pinMode(NotePins[i].gpio, OUTPUT);
        digitalWrite(NotePins[i].gpio, LOW);
    }

    usbh_setup(show_config_desc_full);
}

void loop()
{
    usbh_task();
}
