#include "pxl8_repl.h"

#include <poll.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <SDL3/SDL.h>
#include <linenoise.h>

#define PXL8_MAX_REPL_COMMAND_SIZE 4096
#define PXL8_REPL_QUEUE_SIZE 8

struct pxl8_repl_command {
    char buffer[PXL8_MAX_REPL_COMMAND_SIZE];
};

struct pxl8_repl {
    char commands[PXL8_REPL_QUEUE_SIZE][PXL8_MAX_REPL_COMMAND_SIZE];
    atomic_uint cmd_write_idx;
    atomic_uint cmd_read_idx;
    atomic_bool cmd_complete;

    char logs[PXL8_REPL_QUEUE_SIZE][PXL8_MAX_REPL_COMMAND_SIZE];
    atomic_uint log_write_idx;
    atomic_uint log_read_idx;

    atomic_bool should_quit;
    SDL_Thread* thread;
    char accumulator[PXL8_MAX_REPL_COMMAND_SIZE];
    pxl8_repl_command command;
};

static pxl8_repl* g_repl = NULL;

static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc) {
    const char* fennel_keywords[] = {
        "fn", "let", "var", "set", "global", "local",
        "if", "when", "do", "while", "for", "each",
        "lambda", "λ", "partial", "macro", "macros",
        "require", "include", "import-macros",
        "values", "select", "table", "length",
        ".", "..", ":", "->", "->>", "-?>", "-?>>",
        "doto", "match", "case", "pick-values",
        "collect", "icollect", "accumulate"
    };

    const char* pxl8_functions[] = {
        "pxl8.clr", "pxl8.pixel", "pxl8.get_pixel",
        "pxl8.line", "pxl8.rect", "pxl8.rect_fill",
        "pxl8.circle", "pxl8.circle_fill", "pxl8.text",
        "pxl8.get_screen", "pxl8.info", "pxl8.warn",
        "pxl8.error", "pxl8.debug", "pxl8.trace"
    };

    size_t buf_len = strlen(buf);

    for (size_t i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) {
        if (strncmp(buf, fennel_keywords[i], buf_len) == 0) {
            linenoiseAddCompletion(lc, fennel_keywords[i]);
        }
    }

    for (size_t i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) {
        if (strncmp(buf, pxl8_functions[i], buf_len) == 0) {
            linenoiseAddCompletion(lc, pxl8_functions[i]);
        }
    }
}

static char* pxl8_repl_hints(const char* buf, int* color, int* bold) {
    if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) {
        *color = 35;
        *bold = 0;
        return "clr|pixel|line|rect|circle|text|get_screen";
    }

    if (strcmp(buf, "(fn") == 0) {
        *color = 36;
        *bold = 0;
        return " [args] body)";
    }

    if (strcmp(buf, "(let") == 0) {
        *color = 36;
        *bold = 0;
        return " [bindings] body)";
    }

    return NULL;
}

static void pxl8_repl_flush_logs(pxl8_repl* repl) {
    u32 log_read_idx = atomic_load(&repl->log_read_idx);
    u32 log_write_idx = atomic_load(&repl->log_write_idx);
    while (log_read_idx != log_write_idx) {
        printf("%s", repl->logs[log_read_idx]);
        atomic_store(&repl->log_read_idx, (log_read_idx + 1) % PXL8_REPL_QUEUE_SIZE);
        log_read_idx = atomic_load(&repl->log_read_idx);
        log_write_idx = atomic_load(&repl->log_write_idx);
    }
    fflush(stdout);
}

static int pxl8_repl_thread(void* arg) {
    pxl8_repl* repl = (pxl8_repl*)arg;

    printf("[pxl8 REPL] Fennel 1.6.0 - Tab for completion, Ctrl-D to exit\n");
    fflush(stdout);

    struct linenoiseState ls;
    char input_buf[PXL8_MAX_REPL_COMMAND_SIZE];
    bool editing = false;

    struct pollfd pfd = {
        .fd = STDIN_FILENO,
        .events = POLLIN
    };

    while (!atomic_load(&repl->should_quit)) {
        u32 log_read_idx = atomic_load(&repl->log_read_idx);
        u32 log_write_idx = atomic_load(&repl->log_write_idx);

        if (log_read_idx != log_write_idx) {
            printf("\r\033[K");
            if (editing) {
                linenoiseEditStop(&ls);
                editing = false;
                printf("\033[A\r\033[K");
            }
            while (log_read_idx != log_write_idx) {
                printf("%s", repl->logs[log_read_idx]);
                atomic_store(&repl->log_read_idx, (log_read_idx + 1) % PXL8_REPL_QUEUE_SIZE);
                log_read_idx = atomic_load(&repl->log_read_idx);
                log_write_idx = atomic_load(&repl->log_write_idx);
            }
            fflush(stdout);
            continue;
        }

        if (!editing && !atomic_load(&repl->should_quit)) {
            const char* prompt = (repl->accumulator[0] != '\0') ? ".. " : ">> ";
            if (linenoiseEditStart(&ls, STDIN_FILENO, STDOUT_FILENO, input_buf, sizeof(input_buf), prompt) == -1) {
                atomic_store(&repl->should_quit, true);
                break;
            }
            editing = true;
        }

        if (poll(&pfd, 1, 1) <= 0) continue;

        char* line = linenoiseEditFeed(&ls);

        if (line == NULL) {
            atomic_store(&repl->should_quit, true);
            break;
        }

        if (line == linenoiseEditMore) continue;

        linenoiseEditStop(&ls);
        editing = false;

        bool in_multiline = (repl->accumulator[0] != '\0');

        if (strlen(line) > 0 || in_multiline) {
            if (!in_multiline) {
                linenoiseHistoryAdd(line);
                linenoiseHistorySave(".pxl8_history");
            }

            if (repl->accumulator[0] != '\0') {
                strncat(repl->accumulator, "\n",
                        PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);
            }
            strncat(repl->accumulator, line,
                    PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);

            u32 write_idx = atomic_load(&repl->cmd_write_idx);
            u32 read_idx = atomic_load(&repl->cmd_read_idx);
            u32 next_write = (write_idx + 1) % PXL8_REPL_QUEUE_SIZE;

            if (next_write != read_idx) {
                strncpy(repl->commands[write_idx], repl->accumulator, PXL8_MAX_REPL_COMMAND_SIZE - 1);
                repl->commands[write_idx][PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
                atomic_store(&repl->cmd_write_idx, next_write);
            }
        }

        linenoiseFree(line);

        while (!atomic_load(&repl->should_quit) &&
               (atomic_load(&repl->cmd_write_idx) != atomic_load(&repl->cmd_read_idx) ||
                !atomic_load(&repl->cmd_complete))) {
            u32 lr = atomic_load(&repl->log_read_idx);
            u32 lw = atomic_load(&repl->log_write_idx);
            while (lr != lw) {
                printf("%s", repl->logs[lr]);
                atomic_store(&repl->log_read_idx, (lr + 1) % PXL8_REPL_QUEUE_SIZE);
                lr = atomic_load(&repl->log_read_idx);
                lw = atomic_load(&repl->log_write_idx);
            }
            fflush(stdout);
            SDL_Delay(1);
        }
        atomic_store(&repl->cmd_complete, false);
    }

    if (editing) linenoiseEditStop(&ls);

    pxl8_repl_flush_logs(repl);

    return 0;
}

pxl8_repl* pxl8_repl_create(void) {
    pxl8_repl* repl = (pxl8_repl*)calloc(1, sizeof(pxl8_repl));
    if (!repl) return NULL;

    repl->accumulator[0] = '\0';
    atomic_store(&repl->cmd_write_idx, 0);
    atomic_store(&repl->cmd_read_idx, 0);
    atomic_store(&repl->cmd_complete, true);
    atomic_store(&repl->log_write_idx, 0);
    atomic_store(&repl->log_read_idx, 0);
    atomic_store(&repl->should_quit, false);

    linenoiseHistorySetMaxLen(100);
    linenoiseSetMultiLine(1);
    linenoiseSetCompletionCallback(pxl8_repl_completion);
    linenoiseSetHintsCallback(pxl8_repl_hints);
    linenoiseHistoryLoad(".pxl8_history");

    g_repl = repl;

    repl->thread = SDL_CreateThread(pxl8_repl_thread, "pxl8-repl", repl);
    if (!repl->thread) {
        free(repl);
        g_repl = NULL;
        return NULL;
    }

    return repl;
}

void pxl8_repl_destroy(pxl8_repl* repl) {
    if (!repl) return;

    atomic_store(&repl->should_quit, true);

    SDL_Delay(2);

    printf("\r\033[K");
    fflush(stdout);

    SDL_WaitThread(repl->thread, NULL);
    pxl8_repl_flush_logs(repl);

    g_repl = NULL;

    system("stty sane 2>/dev/null");
    free(repl);
}

pxl8_repl_command* pxl8_repl_pop_command(pxl8_repl* repl) {
    if (!repl) return NULL;

    u32 read_idx = atomic_load(&repl->cmd_read_idx);
    u32 write_idx = atomic_load(&repl->cmd_write_idx);

    if (read_idx == write_idx) return NULL;

    strncpy(repl->command.buffer, repl->commands[read_idx], PXL8_MAX_REPL_COMMAND_SIZE - 1);
    repl->command.buffer[PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';

    atomic_store(&repl->cmd_read_idx, (read_idx + 1) % PXL8_REPL_QUEUE_SIZE);

    return &repl->command;
}

const char* pxl8_repl_command_buffer(pxl8_repl_command* cmd) {
    return cmd ? cmd->buffer : NULL;
}

bool pxl8_repl_should_quit(pxl8_repl* repl) {
    return repl ? atomic_load(&repl->should_quit) : false;
}

bool pxl8_repl_push_log(const char* message) {
    if (!g_repl || !message) return false;

    u32 write_idx = atomic_load(&g_repl->log_write_idx);
    u32 read_idx = atomic_load(&g_repl->log_read_idx);
    u32 next_write = (write_idx + 1) % PXL8_REPL_QUEUE_SIZE;

    if (next_write != read_idx) {
        strncpy(g_repl->logs[write_idx], message, PXL8_MAX_REPL_COMMAND_SIZE - 1);
        g_repl->logs[write_idx][PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
        atomic_store(&g_repl->log_write_idx, next_write);
    }
    return true;
}

void pxl8_repl_clear_accumulator(pxl8_repl* repl) {
    if (!repl) return;
    repl->accumulator[0] = '\0';
}

void pxl8_repl_signal_complete(pxl8_repl* repl) {
    if (!repl) return;
    atomic_store(&repl->cmd_complete, true);
}
