#include <Arduino.h>

#include "USB.h"
#include "powermgt.h"
#include "serialconsole.h"
#include "settings.h"
#include "zbs_interface.h"

USBCDC USBSerial;

QueueHandle_t flasherCmdQueue;

uint32_t usbConnectedStartTime = 0;

#define FLASHER_WAIT_A 0
#define FLASHER_WAIT_T 1
#define FLASHER_WAIT_CMD 2
#define FLASHER_WAIT_LEN 3
#define FLASHER_WAIT_DATA 4
#define FLASHER_WAIT_CRCH 5
#define FLASHER_WAIT_CRCL 6

struct flasherCommand {
    uint8_t command = 0;
    uint8_t len = 0;
    uint8_t* data = nullptr;
};

#define FLASHER_MODE_UNKNOWN 0
#define FLASHER_MODE_FLASHER 1
#define FLASHER_MODE_CONSOLE 2
volatile uint8_t usbFlasherMode = FLASHER_MODE_UNKNOWN;

void enterConsoleMode() {
    usbFlasherMode = FLASHER_MODE_CONSOLE;
    xTaskCreate(consoleTask, "consoleTask", 10000, NULL, 2, &consoleTaskHandle);
}

void sendFlasherAnswer(uint8_t answer_cmd, uint8_t* ans_buff, uint8_t len) {
    uint8_t* answer_buffer = (uint8_t*)calloc(2 + 2 + len + 2, 1);
    if (answer_buffer == nullptr) return;
    uint32_t CRC_value = 0xAB34;
    answer_buffer[0] = 'A';
    answer_buffer[1] = 'T';
    answer_buffer[2] = answer_cmd;
    CRC_value += answer_cmd;
    answer_buffer[3] = len;
    CRC_value += len;
    for (int i = 0; i < len; i++) {
        answer_buffer[4 + i] = ans_buff[i];
        CRC_value += ans_buff[i];
    }
    answer_buffer[2 + 2 + len] = CRC_value >> 8;
    answer_buffer[2 + 2 + len + 1] = CRC_value;
    USBSerial.write(answer_buffer, 2 + 2 + len + 2);
    free(answer_buffer);
}

void flasherUartHandler(uint8_t* data, uint8_t len) {
    static struct flasherCommand* cmd;
    static uint8_t flasherSerialState = FLASHER_WAIT_A;
    static uint8_t flasherCmdDataIndex = 0;
    static uint16_t flasherCRC = 0xAB34;
    static uint32_t flasherLastCmd = 0;

    if ((flasherSerialState != FLASHER_WAIT_A) && (millis() - flasherLastCmd >= 225)) {
        flasherSerialState = FLASHER_WAIT_A;
    }

    while (len--) {
        uint8_t usbbyte = *(data++);
        switch (flasherSerialState) {
            case FLASHER_WAIT_A:
                if (usbbyte == 'A') {
                    flasherSerialState = FLASHER_WAIT_T;
                    flasherLastCmd = millis();
                } else {
                    enterConsoleMode();
                }
                break;
            case FLASHER_WAIT_T:
                if (usbbyte == 'T') {
                    flasherSerialState = FLASHER_WAIT_CMD;
                    cmd = new flasherCommand;
                    flasherCRC = 0xAB34;
                    flasherCmdDataIndex = 0;
                } else {
                    flasherSerialState = FLASHER_WAIT_A;
                }
                break;
            case FLASHER_WAIT_CMD:
                cmd->command = usbbyte;
                flasherCRC += usbbyte;
                flasherSerialState = FLASHER_WAIT_LEN;
                break;
            case FLASHER_WAIT_LEN:
                flasherCRC += usbbyte;
                if (usbbyte) {
                    cmd->len = usbbyte;
                    cmd->data = (uint8_t*)calloc(usbbyte, 1);
                    flasherSerialState = FLASHER_WAIT_DATA;
                } else {
                    flasherSerialState = FLASHER_WAIT_CRCH;
                }
                break;
            case FLASHER_WAIT_DATA:
                flasherCRC += usbbyte;
                cmd->data[flasherCmdDataIndex++] = usbbyte;
                if (flasherCmdDataIndex == cmd->len) {
                    flasherSerialState = FLASHER_WAIT_CRCH;
                }
                break;
            case FLASHER_WAIT_CRCH:
                flasherCRC -= ((uint16_t)usbbyte << 8);
                flasherSerialState = FLASHER_WAIT_CRCL;
                break;
            case FLASHER_WAIT_CRCL:
                flasherCRC -= ((uint16_t)usbbyte);
                if (flasherCRC) {
                    Serial.printf("CRC failed for flasher command :( %04X\n", flasherCRC);
                    cmd = nullptr;
                } else {
                    if (usbFlasherMode == FLASHER_MODE_UNKNOWN) usbFlasherMode = FLASHER_MODE_FLASHER;
                    BaseType_t queuestatus = xQueueSend(flasherCmdQueue, &cmd, 0);
                    if (queuestatus == pdFALSE) {
                        if (cmd->data != nullptr) free(cmd->data);
                        delete cmd;
                    }
                    cmd = nullptr;
                }
                flasherSerialState = FLASHER_WAIT_A;
                break;
        }
    }
}

void resetFlasherState() {
    if (usbFlasherMode != FLASHER_MODE_UNKNOWN) {
        if (usbFlasherMode == FLASHER_MODE_CONSOLE) consoleStopTask();
        Serial.print("Resetting flasher state");
        usbFlasherMode = FLASHER_MODE_UNKNOWN;
        usbConnectedStartTime = millis();
    }
}

static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    if (event_base == ARDUINO_USB_EVENTS) {
        arduino_usb_event_data_t* data = (arduino_usb_event_data_t*)event_data;
        switch (event_id) {
            case ARDUINO_USB_STARTED_EVENT:
                Serial.println("USB PLUGGED");
                resetFlasherState();
                break;
            case ARDUINO_USB_STOPPED_EVENT:
                Serial.println("USB UNPLUGGED");
                resetFlasherState();
                break;
            case ARDUINO_USB_SUSPEND_EVENT:
                Serial.printf("USB SUSPENDED: remote_wakeup_en: %u\n", data->suspend.remote_wakeup_en);
                break;
            case ARDUINO_USB_RESUME_EVENT:
                Serial.println("USB RESUMED");
                break;
            default:
                ets_printf("other USB event %d\n", event_id);
                break;
        }
    } else if (event_base == ARDUINO_USB_CDC_EVENTS) {
        arduino_usb_cdc_event_data_t* data = (arduino_usb_cdc_event_data_t*)event_data;
        switch (event_id) {
            case ARDUINO_USB_CDC_CONNECTED_EVENT:
                ets_printf("CDC CONNECTED\n");
                resetFlasherState();
                usbConnectedStartTime = millis();
                break;
            case ARDUINO_USB_CDC_DISCONNECTED_EVENT:
                ets_printf("CDC DISCONNECTED\n");
                resetFlasherState();
                break;
            case ARDUINO_USB_CDC_LINE_STATE_EVENT:
                ets_printf("CDC LINE STATE: dtr: %u, rts: %u\n", data->line_state.dtr, data->line_state.rts);
                if (data->line_state.dtr == 0) resetFlasherState();
                break;
            case ARDUINO_USB_CDC_LINE_CODING_EVENT:
                ets_printf("CDC LINE CODING: bit_rate: %u, data_bits: %u, stop_bits: %u, parity: %u\n", data->line_coding.bit_rate, data->line_coding.data_bits, data->line_coding.stop_bits, data->line_coding.parity);
                resetFlasherState();
                break;
            case ARDUINO_USB_CDC_RX_EVENT:
                // Serial.printf("CDC RX [%u]:", data->rx.len);
                {
                    uint8_t buf[data->rx.len];
                    size_t len = USBSerial.read(buf, data->rx.len);
                    if (usbFlasherMode != FLASHER_MODE_CONSOLE) {
                        flasherUartHandler(buf, len);
                    } else {
                        consoleUartHandler(buf, len);
                    }
                }
                break;
            case ARDUINO_USB_CDC_RX_OVERFLOW_EVENT:
                Serial.printf("CDC RX Overflow of %d bytes", data->rx_overflow.dropped_bytes);
                break;

            default:
                break;
        }
    }
}

typedef enum {
    CMD_GET_VERSION = 1,
    CMD_RESET_ESP = 2,
    CMD_ZBS_BEGIN = 10,
    CMD_RESET_ZBS = 11,
    CMD_SELECT_PAGE = 12,
    CMD_SET_POWER = 13,
    CMD_READ_RAM = 20,
    CMD_WRITE_RAM = 21,
    CMD_READ_FLASH = 22,
    CMD_WRITE_FLASH = 23,
    CMD_READ_SFR = 24,
    CMD_WRITE_SFR = 25,
    CMD_ERASE_FLASH = 26,
    CMD_ERASE_INFOBLOCK = 27,
    CMD_SAVE_MAC_FROM_FW = 40,
    CMD_PASS_THROUGH = 50,
} ZBS_UART_PROTO;
uint32_t FLASHER_VERSION = 0x0000002F;

static class ZBS_interface* zbs = nullptr;

void processFlasherCommand(struct flasherCommand* cmd) {
    uint8_t* tempbuffer;
    uint8_t temp_buff[16];
    uint32_t spi_speed = 0;
    static uint32_t curspeed = 0;

    switch (cmd->command) {
        case CMD_GET_VERSION:
            temp_buff[0] = FLASHER_VERSION >> 24;
            temp_buff[1] = FLASHER_VERSION >> 16;
            temp_buff[2] = FLASHER_VERSION >> 8;
            temp_buff[3] = FLASHER_VERSION;
            sendFlasherAnswer(cmd->command, temp_buff, 4);
            break;
        case CMD_RESET_ESP:
            sendFlasherAnswer(cmd->command, NULL, 0);
            delay(100);
            ESP.restart();
            break;
        case CMD_ZBS_BEGIN:
            if (zbs != nullptr) {
                delete zbs;
            }
            zbs = new ZBS_interface;
            if (cmd->data[0] & 1) {
                spi_speed = 1000000;
            } else {
                spi_speed = 8000000;
            }
            curspeed = spi_speed;
            if (cmd->data[0] & 2) {
                temp_buff[0] = zbs->begin(FLASHER_AP_SS, FLASHER_AP_CLK, FLASHER_AP_MOSI, FLASHER_AP_MISO, FLASHER_AP_RESET, FLASHER_AP_POWER, spi_speed);
            } else if (cmd->data[0] & 4) {
                #ifdef OPENEPAPERLINK_PCB
                temp_buff[0] = zbs->begin(FLASHER_ALT_SS, FLASHER_ALT_CLK, FLASHER_ALT_MOSI, FLASHER_ALT_MISO, FLASHER_ALT_RESET, 255, spi_speed);
                #endif
            } else {
                #ifdef OPENEPAPERLINK_PCB
                temp_buff[0] = zbs->begin(FLASHER_EXT_SS, FLASHER_EXT_CLK, FLASHER_EXT_MOSI, FLASHER_EXT_MISO, FLASHER_EXT_RESET, FLASHER_EXT_POWER, spi_speed);
                #endif
            }
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_RESET_ZBS:
            zbs->reset();
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_SELECT_PAGE:
            temp_buff[0] = zbs->select_flash(cmd->data[0] ? 1 : 0);
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_SET_POWER:
            zbs->set_power(cmd->data[0] ? 1 : 0);
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_READ_RAM:
            temp_buff[0] = zbs->read_ram(cmd->data[0]);
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_WRITE_RAM:
            zbs->write_ram(cmd->data[0], cmd->data[1]);
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_READ_FLASH:
            tempbuffer = (uint8_t*)calloc(cmd->data[0], 1);
            // cmd_buff[0] = len
            // cmd_buff[1] << 8 | cmd_buff[2] = position
            // Serial.printf("Loading %d bytes from %04X \n", cmd->data[0], (cmd->data[1] << 8 | cmd->data[2]));
            for (int i = 0; i < cmd->data[0]; i++) {
                tempbuffer[i] = zbs->read_flash((cmd->data[1] << 8 | cmd->data[2]) + i);
            }
            sendFlasherAnswer(cmd->command, tempbuffer, cmd->data[0]);
            free(tempbuffer);
            break;
        case CMD_WRITE_FLASH:
            // cmd_buff[0] = len
            // cmd_buff[1] << 8 | cmd_buff[2] = position
            // cmd_buff[3+i] = data
            if (cmd->data[0] >= (0xff - 3)) {  // Len too high, only 0xFF - header len possible
                temp_buff[0] = 0xEE;
                sendFlasherAnswer(cmd->command, temp_buff, 1);
                break;
            }
            // Serial.printf("Writing %d bytes to %04X \n", cmd->data[0], (cmd->data[1] << 8 | cmd->data[2]));
            for (int i = 0; i < cmd->data[0]; i++) {
                if (cmd->data[3 + i] != 0xff) {
                    for (uint8_t attempts = 0; attempts < 10; attempts++) {
                        zbs->write_flash((cmd->data[1] << 8 | cmd->data[2]) + i, cmd->data[3 + i]);
                        if (zbs->read_flash((cmd->data[1] << 8 | cmd->data[2]) + i) == cmd->data[3 + i]) {
                            goto flash_pass;
                        }
                        curspeed -= 100000;
                        zbs->setSpeed(curspeed);
                    }
                flash_fail:
                    temp_buff[0] = 0;
                    sendFlasherAnswer(cmd->command, temp_buff, 1);
                    break;
                flash_pass:
                    continue;
                }
            }
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_READ_SFR:
            temp_buff[0] = zbs->read_sfr(cmd->data[0]);
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_WRITE_SFR:
            zbs->write_sfr(cmd->data[0], cmd->data[1]);
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_ERASE_FLASH:
            zbs->erase_flash();
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_ERASE_INFOBLOCK:
            zbs->erase_infoblock();
            temp_buff[0] = 1;
            sendFlasherAnswer(cmd->command, temp_buff, 1);
            break;
        case CMD_SAVE_MAC_FROM_FW:
        case CMD_PASS_THROUGH:
            break;
    }
}

void usbFlasherTask(void* parameter) {
    flasherCmdQueue = xQueueCreate(10, sizeof(struct flasherCommand*));
    consoleCmdQueue = xQueueCreate(10, sizeof(struct consoleCommand*));
#if ARDUINO_USB_MODE
#warning Wrong USB mode is in use, check settings in platformio.ini
#endif
    USB.onEvent(usbEventCallback);
    USBSerial.onEvent(usbEventCallback);
    USBSerial.setTimeout(1000);
    USB.begin();
    USBSerial.begin();

    struct flasherCommand* cmd;
    while (true) {
        BaseType_t queuereceive = xQueueReceive(flasherCmdQueue, &cmd, portMAX_DELAY);
        if (queuereceive == pdTRUE) {
            processFlasherCommand(cmd);
            if (cmd->data != nullptr) {
                free(cmd->data);
            }
            delete cmd;
        }
    }
}