#include <furi.h>
#include <cli/cli.h>
#include <cli/cli_vcp.h>
#include <dialogs/dialogs.h>
#include <gui/gui.h>
#include <gui/elements.h>
#include <storage/storage.h>
#include <furi_hal_resources.h>
#include <furi_hal_light.h>
#include <furi_hal_console.h>
#include <math.h>

/* generated by fbt from .png files in images folder */
#include <tas_playback_icons.h>

#include "WString.h"

typedef enum {
    O_READ = FSAM_READ,
    O_WRITE = FSAM_WRITE,
    O_CREAT = ((uint16_t)FSOM_OPEN_ALWAYS << 8),
    O_APPEND = ((uint16_t)FSOM_OPEN_APPEND << 8),
    O_EXCL = ((uint16_t)FSOM_CREATE_NEW << 8),
    O_TRUNC = ((uint16_t)FSOM_CREATE_ALWAYS << 8),
} FileOpenMode;

#define SD_CARD_ERROR_ACMD41 0x16

class SdFs {
public:
    bool begin(int) {
        m_storage = (Storage*)furi_record_open(RECORD_STORAGE);

        return m_storage != nullptr;
    }

    bool mkdir(const char* path) {
        return storage_simply_mkdir(m_storage, path);
    }

    ~SdFs() {
        if(m_storage) furi_record_close(RECORD_STORAGE);
    }

    uint8_t sdErrorCode() const {
        return 0;
    }
    uint8_t sdErrorData() const {
        return 0;
    }

private:
    Storage* m_storage = nullptr;

    friend class SdFile;
};

template <class T>
static inline void printSdErrorSymbol(T*, uint8_t) {
}

class SdFile {
public:
    ~SdFile() {
        if(m_file) storage_file_free(m_file);
    }

    void close() {
        if(m_file) storage_file_close(m_file);
    }

    bool open(SdFs* from, const char* path, uint16_t mode) {
        m_fs = from;

        initFile();

        m_path = path;

        return storage_file_open(
            m_file, path, FS_AccessMode((uint16_t)mode & 0xFF), FS_OpenMode((uint16_t)mode >> 8));
    }

    bool open(SdFs* from, const char* path, FileOpenMode mode) {
        return open(from, path, (uint16_t)mode);
    }

    void getName(char* dest, uint16_t size) {
        m_path.toCharArray(dest, size);
    }

    unsigned read(void* buff, int bytes_to_read) {
        return storage_file_read(m_file, buff, bytes_to_read);
    }

    unsigned write(const void* buff, int bytes_to_read) {
        return storage_file_write(m_file, buff, bytes_to_read);
    }

    bool available() {
        return !storage_file_eof(m_file);
    }

    uint64_t fileSize() {
        return storage_file_size(m_file);
    }

    uint64_t curPosition() {
        return storage_file_tell(m_file);
    }

    bool seekSet(uint32_t pos) {
        return storage_file_seek(m_file, pos, true);
    }

    bool isOpen() {
        return m_file && storage_file_is_open(m_file);
    }

    bool openNext(SdFile* from) {
        char name[64];

        bool ret = storage_dir_read(from->m_file, NULL, name, sizeof(name));
        if(!ret) return false;

        String path = from->m_path;
        path += "/";
        path += name;

        return open(from->m_fs, path.c_str(), O_READ);
    }

    bool rewind() {
        return false;
        //    return storage_dir_rewind(m_file);
    }

private:
    void initFile() {
        close();

        if(!m_file) m_file = storage_file_alloc(m_fs->m_storage);
    }

    File* m_file = nullptr;
    SdFs* m_fs = nullptr;
    String m_path;
};

#define F(x) x

#define noInterrupts() uint32_t intLevel = ulPortRaiseBASEPRI()
#define interrupts() vPortSetBASEPRI(intLevel)

#define F_CPU /*SystemCoreClock*/ 64000000

typedef enum { HEX = 16 } BaseType;

class SerialObj {
public:
    template <class... T>
    void print(const T&... v) {
        String out(v...);
        furi_hal_console_tx((uint8_t*)out.c_str(), out.length());
    }

    void println() {
        furi_hal_console_tx((uint8_t*)"\r\n", 2);
    }
    template <class... T>
    void println(const T&... v) {
        String out(v...);
        furi_hal_console_tx((uint8_t*)out.c_str(), out.length());
        println();
    }

    template <class... T>
    void write(const T&... v) {
        print(v...);
    }

    int read() {
        return -1;
        /*
    uint8_t buf;
    if (!cli_read_timeout(m_cli, &buf, 1, 0))
      return -1;
    return buf;*/
    }

    bool begin() {
        /*
    m_cli = (Cli*)furi_record_open(RECORD_CLI);

//    if (m_cli)
  //    cli_session_open(m_cli, &cli_vcp);
    */

        return (bool)*this;
    }

    ~SerialObj() {
        if(m_cli) furi_record_close(RECORD_CLI);
    }

    explicit operator bool() {
        return true; // m_cli && cli_is_connected(m_cli);
    }

    void flush() {
    }

private:
    Cli* m_cli = nullptr;
};

#define SERIAL_BAUD_RATE

#define BUILTIN_SDCARD 0

#define NO_CONSOLE_GPIO

#define ARM_DWT_CYCCNT DWT->CYCCNT

SerialObj Serial;

typedef void (*ArduinoISR)();

void gpioCallback(void* ctx) {
    ArduinoISR cb = (ArduinoISR)ctx;
    cb();
}

#define LOW false
#define HIGH true

typedef enum {
    INPUT = GpioModeInput,
    OUTPUT = GpioModeOutputOpenDrain,
    INPUT_PULLUP = (uint16_t)GpioModeInput | ((uint16_t)GpioPullUp << 8),
    INPUT_PULLDOWN = (uint16_t)GpioModeInput | ((uint16_t)GpioPullDown << 8),
    FALLING = (uint16_t)GpioModeInterruptFall | ((uint16_t)GpioPullUp << 8),
} PinMode;

void pinMode(const GpioPin* pin, PinMode mode) {
    furi_hal_gpio_init(
        pin, GpioMode((uint16_t)mode & 0xFF), GpioPull((uint16_t)mode >> 8), GpioSpeedVeryHigh);
}

void attachInterrupt(const GpioPin* pin, ArduinoISR cb, PinMode mode) {
    pinMode(pin, mode);
    furi_hal_gpio_remove_int_callback(pin);
    furi_hal_gpio_add_int_callback(pin, gpioCallback, (void*)cb);
}

static inline void digitalWrite(const GpioPin* pin, bool state) {
    furi_hal_gpio_write(pin, state);
}

static inline bool digitalRead(const GpioPin* pin) {
    return furi_hal_gpio_read(pin);
}

#define N64_PIN &gpio_ext_pc0
#define N64_PIN_PREFIX PC0
#define CONCAT1(a, b) a##b
#define CONCAT(a, b) CONCAT1(a, b)
#define N64_PORT CONCAT(N64_PIN_PREFIX, _GPIO_Port)
#define N64_Pin CONCAT(N64_PIN_PREFIX, _Pin)
#define N64_IRQ EXTI0_IRQn
#define N64_PIN_LINE LL_SYSCFG_EXTI_LINE0

#define DISABLE_N64_INTERRUPT() LL_EXTI_DisableFallingTrig_0_31(N64_PIN_LINE)
#define ENABLE_N64_INTERRUPT() LL_EXTI_EnableFallingTrig_0_31(N64_PIN_LINE)

#define N64_HIGH LL_GPIO_SetPinMode((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_MODE_INPUT)
#define N64_LOW LL_GPIO_SetPinMode((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_MODE_OUTPUT)
#define N64_QUERY (N64_PORT->IDR & N64_Pin)

#define LED_HIGH (void)0; //furi_hal_light_set(LightGreen, 0xFF)
#define LED_LOW (void)0; //furi_hal_light_set(LightGreen, 0x00)

template <class T1, class T2>
auto min(T1 x, T2 y) {
    return x < y ? x : y;
}

//#define USE_COMPARE_ABSOLUTE

FuriMessageQueue* msg_queue;

Gui* gui = nullptr;
ViewPort* view_port = nullptr;

typedef struct {
    enum {
        Input,
        ISRLog,
    } msg_type;
    union {
        InputEvent input;
    };
} QueueMessage;

void isrLogCb() {
    QueueMessage msg = {QueueMessage::ISRLog, {}};
    furi_message_queue_put(msg_queue, &msg, 0);
}

#define ISR_LOG_CB isrLogCb

unsigned char frameDat[4] = {0};

void frameCb(const unsigned char* dat, size_t count) {
    memcpy(frameDat, dat, count);
}

#define FRAME_CB frameCb

#include "teensy/teensy.ino"

static void input_callback(InputEvent* input_event, void* ctx) {
    (void)ctx;
    QueueMessage msg = {QueueMessage::Input, {.input = *input_event}};
    furi_message_queue_put(msg_queue, &msg, 0);
}

static void render_callback(Canvas* canvas, void* ctx) {
    (void)ctx;

    elements_text_box(canvas, 0, 0, 128, 10, AlignCenter, AlignTop, filename.c_str(), true);

#define INVERT "\e!"

    static char inputStr[128] = "";

#define GET_BIT(byte, bit) (frameDat[byte] & (1 << bit))
#define INV_IF(x) ((x) ? INVERT : "")
#define FMT_BTN(byte, bit) INV_IF(GET_BIT(byte, bit)), INV_IF(GET_BIT(byte, bit))
#define PLUS_MIN_ZERO(x) (((x) == 0) ? '0' : (((x) > 127) ? '-' : '+'))
#define UN_SIGN(x) (((x) <= 127) ? x : (-((int)(x)-256)))
#define FMT_JOY(byte)                                                                    \
    INV_IF(frameDat[byte] != 0), PLUS_MIN_ZERO(frameDat[byte]), UN_SIGN(frameDat[byte]), \
        INV_IF(frameDat[byte] != 0)

    if(console == N64)
        snprintf(
            inputStr,
            sizeof(inputStr),
            "\e*\e!X\e!:%s%c%02x%s \e!Y\e!:%s%c%02x%s %s<%s%s>%s%s^%s%sv%s\n%sA%s%sB%s%sZ%s%sS%s%sL%s%sR%s C(%s<%s%s>%s%s^%s%sv%s)",
            FMT_JOY(2),
            FMT_JOY(3),
            FMT_BTN(0, 1),
            FMT_BTN(0, 0),
            FMT_BTN(0, 3),
            FMT_BTN(0, 2),
            FMT_BTN(0, 7),
            FMT_BTN(0, 6),
            FMT_BTN(0, 5),
            FMT_BTN(0, 4),
            FMT_BTN(1, 5),
            FMT_BTN(1, 4),
            FMT_BTN(1, 1),
            FMT_BTN(1, 0),
            FMT_BTN(1, 3),
            FMT_BTN(1, 2));

    elements_text_box(canvas, 0, 15, 128, 32, AlignCenter, AlignTop, inputStr, false);

    int denom = numFrames ? numFrames : 1;
    float progress = (float)curFrame / denom;
    char progStr[32];
    snprintf(
        progStr, sizeof(progStr), "%07lu/%07lu:%03i%%", curFrame, numFrames, (int)(progress * 100));

    uint8_t progress_y = 41;
    uint8_t progress_h = 12;

    uint8_t progress_length = roundf(progress * (126));
    canvas_set_color(canvas, ColorWhite);
    canvas_draw_box(canvas, 1, progress_y + 1, 126, progress_h - 2);
    canvas_set_color(canvas, ColorBlack);
    canvas_draw_rframe(canvas, 0, progress_y, 128, progress_h, 3);
    canvas_draw_box(canvas, 1, progress_y + 1, progress_length, progress_h - 2);

    canvas_set_color(canvas, ColorXOR);
    canvas_set_font(canvas, FontKeyboard);
    canvas_draw_str_aligned(canvas, 64, progress_y + 2, AlignCenter, AlignTop, progStr);

    canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8);
    canvas_set_font(canvas, FontSecondary);
    elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit");
}

extern "C" int32_t tas_playback_app(const char* p) {
    FURI_LOG_I("TAS", "Hello world");
    FURI_LOG_I("TAS", "I'm tas_playback!");

    uint32_t oldPrio = NVIC_GetPriority(N64_IRQ);
    NVIC_SetPriority(N64_IRQ, 0);

    LL_GPIO_SetPinOutputType((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_OUTPUT_PUSHPULL);

    setup();

    FURI_LOG_I("TAS", "SETUP DONE");

    msg_queue = furi_message_queue_alloc(8, sizeof(QueueMessage));

    gui = (Gui*)furi_record_open(RECORD_GUI);

    view_port = view_port_alloc();
    gui_add_view_port(gui, view_port, GuiLayerFullscreen);

    view_port_draw_callback_set(view_port, render_callback, NULL);
    view_port_input_callback_set(view_port, input_callback, NULL);

    FuriString* file_path = furi_string_alloc();

    FURI_LOG_I("TAS", "STARTING LOOP");

    do {
        if(p && strlen(p)) {
            FURI_LOG_I("TAS", "GOT ARG");
            furi_string_set_str(file_path, (const char*)p);
        } else {
            FURI_LOG_I("TAS", "NO ARG");

            Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
            storage_common_migrate(storage, EXT_PATH("tas"), STORAGE_APP_DATA_PATH_PREFIX);
            furi_record_close(RECORD_STORAGE);

            FURI_LOG_I("TAS", "MIGRATED");

            furi_string_set_str(file_path, STORAGE_APP_DATA_PATH_PREFIX);

            DialogsFileBrowserOptions browser_options;
            dialog_file_browser_set_basic_options(&browser_options, "m64", &I_tas_playback);
            browser_options.hide_ext = false;
            browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;

            FURI_LOG_I("TAS", "SETUP OPTIONS");

            DialogsApp* dialogs = (DialogsApp*)furi_record_open(RECORD_DIALOGS);
            bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);

            furi_record_close(RECORD_DIALOGS);

            if(!res) {
                FURI_LOG_E("TAS", "No file selected");
                break;
            }
        }

        if(!openTAS(furi_string_get_cstr(file_path))) continue;

        FURI_LOG_E("TAS", "Got file");

        QueueMessage msg;
        while(furi_message_queue_get(msg_queue, &msg, FuriWaitForever) == FuriStatusOk) {
            if(msg.msg_type == QueueMessage::Input) {
                if(msg.input.type == InputTypeLong) {
                    if(msg.input.key == InputKeyBack) break;
                } else if(msg.input.type == InputTypeShort) {
                    if(msg.input.key == InputKeyUp) {
                    } else if(msg.input.key == InputKeyDown) {
                    }
                }
            }

            view_port_update(view_port);

            loop();
        }

        view_port_update(view_port);

        if(p && strlen(p)) break; // Exit instead of going to browser if launched with arg
    } while(1);

    furi_string_free(file_path);

    gui_remove_view_port(gui, view_port);
    furi_record_close(RECORD_GUI);
    view_port_free(view_port);

    furi_message_queue_free(msg_queue);

    furi_hal_gpio_init(N64_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
    furi_hal_gpio_remove_int_callback(N64_PIN);

    NVIC_SetPriority(N64_IRQ, oldPrio);

    return 0;
}
