#include "common/fmt_binary.h"
#include "common/op_str.h"
#include "common/video_character_display.h"
#include "vm.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_error.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_keycode.h>
#include <SDL2/SDL_pixels.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_video.h>
#include <bits/pthreadtypes.h>
#include <errno.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    Interrupt* data;
    size_t capacity;
    size_t front;
    size_t back;
} InterruptQueue;

void int_queue_construct(InterruptQueue* queue, size_t capacity)
{
    *queue = (InterruptQueue) {
        .data = malloc(sizeof(Interrupt) * capacity),
        .capacity = capacity,
        .back = 0,
        .front = 0,
    };
}

void int_queue_destroy(InterruptQueue* queue)
{
    free(queue->data);
}

void int_queue_push(InterruptQueue* queue, Interrupt req)
{
    size_t front = queue->front + 1;
    if (front >= queue->capacity) {
        front = 0;
    }
    if (front == queue->back) {
        fprintf(stderr, "error: queue overflow\n");
        exit(1);
    }
    queue->data[queue->front] = req;
    queue->front = front;
}

size_t int_queue_size(const InterruptQueue* queue)
{
    return queue->front >= queue->back
        ? queue->front - queue->back
        : (queue->capacity - queue->back) + queue->front;
}

Interrupt int_queue_pop(InterruptQueue* queue)
{
    if (queue->back == queue->front) {
        fprintf(stderr, "error: queue underflow\n");
        exit(1);
    }
    Interrupt val = queue->data[queue->back];
    size_t back = queue->back + 1;
    if (back >= queue->capacity) {
        back = 0;
    }
    queue->back = back;
    return val;
}

typedef struct {
    IODevice io_device;

    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Texture* buffer_texture;

    pthread_t render_thread;

    atomic_bool should_run;
    pthread_mutex_t mutex;
    pthread_cond_t interrupt_waiter;

    InterruptQueue int_queue;
} SdlDevice;

int sdldevice_construct(SdlDevice* device);
void sdldevice_destroy(SdlDevice* device);
void sdldevice_set_char(
    IODevice* device_device, uint16_t offset, uint8_t value);
void sdldevice_wait_for_interrupt(IODevice* io_device);
void* sdldevice_thread_entry(void* data);
void sdldevice_poll_events(SdlDevice* device);
Interrupt sdldevice_maybe_next_interrupt(IODevice* io_device);

int sdldevice_construct(SdlDevice* device)
{
    int res;
    res = SDL_Init(SDL_INIT_VIDEO);
    if (res != 0) {
        fprintf(stderr, "error: could not init sdl: %s\n", SDL_GetError());
        return -1;
    }
    SDL_Window* window;
    SDL_Renderer* renderer;
    res = SDL_CreateWindowAndRenderer(
        vcd_width_in_px, vcd_height_in_px, 0, &window, &renderer);
    if (res != 0) {
        fprintf(stderr,
            "error: could not create window/renderer: %s\n",
            SDL_GetError());
        return -1;
    }

    SDL_Texture* buffer_texture = SDL_CreateTexture(renderer,
        SDL_PIXELFORMAT_RGBA32,
        SDL_TEXTUREACCESS_STREAMING,
        vcd_width_in_px,
        vcd_height_in_px);
    if (buffer_texture == NULL) {
        fprintf(stderr,
            "error: could not create buffer texture: %s\n",
            SDL_GetError());
        return -1;
    }

    *device = (SdlDevice) {
        .io_device = (IODevice) { 
            .self = device,
            .set_char = sdldevice_set_char,
            .wait_for_interrupt = sdldevice_wait_for_interrupt,
            .maybe_next_interrupt = sdldevice_maybe_next_interrupt,
        },
        .window = window,
        .renderer = renderer,
        .buffer_texture = buffer_texture,
        .render_thread = (pthread_t) { 0 },
        .should_run = true,
        .mutex = PTHREAD_MUTEX_INITIALIZER,
        .interrupt_waiter = PTHREAD_COND_INITIALIZER,
        .int_queue = {0},
    };

    SDL_RenderPresent(device->renderer);

    pthread_create(
        &device->render_thread, NULL, sdldevice_thread_entry, device);

    int_queue_construct(&device->int_queue, 128);
    return 0;
}

void sdldevice_destroy(SdlDevice* device)
{
    device->should_run = false;

    pthread_join(device->render_thread, NULL);
    pthread_mutex_destroy(&device->mutex);
    pthread_cond_destroy(&device->interrupt_waiter);

    int_queue_destroy(&device->int_queue);

    SDL_DestroyTexture(device->buffer_texture);
    SDL_DestroyRenderer(device->renderer);
    SDL_DestroyWindow(device->window);
    SDL_Quit();
}

extern const uint64_t charset[];

void sdldevice_set_char(IODevice* io_device, uint16_t offset, uint8_t value)
{
    if (!((value >= 'A' && value <= 'Z') || value == ' ')) {
        printf("sdldevice: invalid char value = %d\n", value);
        return;
    }

    SdlDevice* device = io_device->self;
    pthread_mutex_lock(&device->mutex);

    SDL_Color* buffer;
    int pitch;
    int res = SDL_LockTexture(
        device->buffer_texture, NULL, (void**)&buffer, &pitch);
    if (res != 0) {
        fprintf(stderr, "error: could not lock texture: %s\n", SDL_GetError());
        pthread_mutex_unlock(&device->mutex);
        return;
    }

    for (int ch_y = 0; ch_y < vcd_ch_height; ++ch_y) {
        for (int ch_x = 0; ch_x < vcd_ch_width; ++ch_x) {
            bool ch = charset[value]
                    >> (ch_y * vcd_ch_width + (vcd_ch_width - ch_x - 1))
                & 1;

            for (int px_y = 0; px_y < vcd_px_height; ++px_y) {
                for (int px_x = 0; px_x < vcd_px_width; ++px_x) {

                    int x = (offset % vcd_width_in_ch * vcd_ch_width + ch_x)
                            * vcd_px_width
                        + px_x;
                    int y = (offset / vcd_width_in_ch * vcd_ch_height + ch_y)
                            * vcd_px_height
                        + px_y;

                    buffer[y * vcd_width_in_px + x] = ch
                        ? (SDL_Color) { 0xff, 0xff, 0xff, 0xff }
                        : (SDL_Color) { 0x00, 0x00, 0x00, 0xff };
                }
            }
        }
    }

    SDL_UnlockTexture(device->buffer_texture);
    SDL_RenderCopy(device->renderer, device->buffer_texture, NULL, NULL);

    SDL_RenderPresent(device->renderer);

    pthread_mutex_unlock(&device->mutex);
}

void sdldevice_wait_for_interrupt(IODevice* io_device)
{
    SdlDevice* device = io_device->self;

    pthread_mutex_lock(&device->mutex);

    // printf("vm: waiting for interrupt...\n");
    pthread_cond_wait(&device->interrupt_waiter, &device->mutex);
    // printf("vm: got interrupt!\n");

    pthread_mutex_unlock(&device->mutex);
}

void* sdldevice_thread_entry(void* data)
{
    SdlDevice* device = (SdlDevice*)data;

    while (device->should_run) {
        sdldevice_poll_events(device);
        if (!device->should_run)
            break;

        SDL_Delay(6);
    }

    return NULL;
}

static inline bool is_exit_event(SDL_Event* event)
{
    return event->type == SDL_QUIT
        || (event->type == SDL_KEYDOWN
            && event->key.keysym.scancode == SDL_SCANCODE_ESCAPE
            // && event->key.keysym.mod & KMOD_CTRL
        );
}

void sdldevice_poll_events(SdlDevice* device)
{
    pthread_mutex_lock(&device->mutex);

    bool should_notify = false;

    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (is_exit_event(&event)) {
            device->should_run = false;
            int_queue_push(&device->int_queue,
                (Interrupt) {
                    .type = InterruptType_Shutdown,
                });
            should_notify = true;
            break;
        } else if (event.type == SDL_KEYDOWN) {
            int_queue_push(&device->int_queue,
                (Interrupt) {
                    .type = InterruptType_KeyEvent,
                    .keycode = (uint16_t)event.key.keysym.scancode,
                });
            should_notify = true;
        }
    }

    pthread_mutex_unlock(&device->mutex);

    if (should_notify) {
        // printf("sdldevice: interrupt occured!\n");
        pthread_cond_signal(&device->interrupt_waiter);
    }
}

Interrupt sdldevice_maybe_next_interrupt(IODevice* io_device)
{
    SdlDevice* device = io_device->self;

    pthread_mutex_lock(&device->mutex);

    Interrupt val = {
        .type = InterruptType_None,
    };

    if (int_queue_size(&device->int_queue) > 0) {
        val = int_queue_pop(&device->int_queue);
    }

    pthread_mutex_unlock(&device->mutex);
    return val;
}

typedef struct {
    Drive drive;
    uint64_t* mem;
} MemDrive;

void memdrive_construct(MemDrive* drive, uint64_t* mem, size_t mem_size);
void memdrive_drive_read(Drive* in_drive, uint8_t* block, uint16_t i);
void memdrive_drive_write(Drive* in_drive, const uint8_t* block, uint16_t i);

void memdrive_construct(MemDrive* drive, uint64_t* mem, size_t mem_size)
{
    const uint16_t block_size = 512;
    *drive = (MemDrive) {
        .drive = (Drive) {
            .self = drive,
            .block_size = block_size,
            .block_amount = (uint16_t)(mem_size / block_size),
            .read = memdrive_drive_read,
            .write = memdrive_drive_write,
        },
        .mem = mem,
    };
}

void memdrive_drive_read(Drive* in_drive, uint8_t* block, uint16_t i)
{
    MemDrive* drive = in_drive->self;
    memcpy(block,
        &drive->mem[i * drive->drive.block_size],
        drive->drive.block_size);
}

void memdrive_drive_write(Drive* in_drive, const uint8_t* block, uint16_t i)
{
    MemDrive* drive = in_drive->self;
    memcpy(&drive->mem[i * drive->drive.block_size],
        block,
        drive->drive.block_size);
}

typedef struct {
    Drive drive;
    FILE* fp;
} FileDrive;

void filedrive_construct(
    FileDrive* drive, FILE* fp, uint16_t block_size, uint16_t block_amount);
void filedrive_drive_read(Drive* in_drive, uint8_t* block, uint16_t i);
void filedrive_drive_write(Drive* in_drive, const uint8_t* block, uint16_t i);

void filedrive_construct(
    FileDrive* drive, FILE* fp, uint16_t block_size, uint16_t block_amount)
{
    *drive = (FileDrive) {
        .drive = (Drive) {
            .self = drive,
            .block_size = block_size,
            .block_amount = block_amount,
            .read = filedrive_drive_read,
            .write = filedrive_drive_write,
        },
        .fp = fp,
    };
}

void filedrive_drive_read(Drive* in_drive, uint8_t* block, uint16_t i)
{
    FileDrive* drive = in_drive->self;
    fseek(drive->fp, drive->drive.block_size * i, SEEK_SET);
    fread(block, sizeof(uint8_t), drive->drive.block_size, drive->fp);
}

void filedrive_drive_write(Drive* in_drive, const uint8_t* block, uint16_t i)
{
    FileDrive* drive = in_drive->self;
    fseek(drive->fp, drive->drive.block_size * i, SEEK_SET);
    fwrite(block, sizeof(uint8_t), drive->drive.block_size, drive->fp);
}

__attribute__((unused)) static inline void dump_program(uint16_t* program)
{
    for (size_t rip = 20; rip < 60; ++rip) {
        uint16_t val = program[rip];
        printf("[%02lx] = %02x %02x  %c%c%c%c %c%c%c%c %c%c%c%c %c%c%c%c "
               "%s\n",
            rip * 2,
            val >> 8,
            val & 0xff,
            fmt_binary(val >> 8),
            fmt_binary(val & 0xff),
            op_str(val & 0x3f));
    }
}

typedef struct {
    const char* disk_file;
} Args;

static inline Args parse_args(int argc, char** argv);

int main(int argc, char** argv)
{
    Args args = parse_args(argc, argv);

    int res;

    int label_ids = 0;

    SdlDevice io_device;
    res = sdldevice_construct(&io_device);
    if (res != 0) {
        exit(1);
    }

    FILE* fp = fopen(args.disk_file, "rb");
    if (!fp) {
        fprintf(stderr,
            "error: could not open %s: %s\n",
            args.disk_file,
            strerror(errno));
        exit(1);
    }

    FileDrive drive;
    filedrive_construct(&drive, fp, 512, 32);

    vm_start(&drive.drive, &io_device.io_device);

    fclose(fp);
    sdldevice_destroy(&io_device);
}

static inline Args parse_args(int argc, char** argv)
{
    const char* disk_file = "build/image";
    for (int i = 1; i < argc; ++i) {
        if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--disk-file") == 0) {
            if (i + 1 >= argc) {
                fprintf(
                    stderr, "error: expected filename after '%s'\n", argv[i]);
                exit(1);
            }
            disk_file = argv[i + 1];
            i += 1;
        } else {
            fprintf(stderr, "error: unrecognized argument '%s'\n", argv[i]);
            exit(1);
        }
    }
    return (Args) {
        disk_file,
    };
}

const char* __asan_default_options(void)
{
    return "detect_leaks=0";
}

const uint64_t charset[] = {
    [' '] = 0x0000000000000000,
    ['A'] = 0x66667E66667E1800,
    ['B'] = 0x7C7E667C667E7800,
    ['C'] = 0x3C7E6060607E3C00,
    ['D'] = 0x7C7E6666667E7C00,
    ['E'] = 0x7E7E607E607E7E00,
    ['F'] = 0x60607878607E7E00,
    ['G'] = 0x7E7E666E607E7E00,
    ['H'] = 0x6666667E7E666600,
    ['I'] = 0x7E7E1818187E7E00,
    ['J'] = 0x3C7E6606067E7E00,
    ['K'] = 0x666E7C7C6E666600,
    ['L'] = 0x7E7E606060606000,
    ['M'] = 0xC3C3C3C3DBFFE700,
    ['N'] = 0x6666666E7E766600,
    ['O'] = 0x3C66666666663C00,
    ['P'] = 0x60607C7E667C7C00,
    ['Q'] = 0x3F7E6E66667E3C00,
    ['R'] = 0x666C7C7E667E7C00,
    ['S'] = 0x3C66063C60663C00,
    ['T'] = 0x18181818187E7E00,
    ['U'] = 0x3C7E666666666600,
    ['V'] = 0x183C7E6666666600,
    ['W'] = 0xE7FFDBDBC3C3C300,
    ['X'] = 0x6666663C3C666600,
    ['Y'] = 0x181818183C666600,
    ['Z'] = 0x7E7E30180C7E7E00,
};