#include <stdint.h>
#include <stddef.h>
#include <lib/cio.h>
#include <drivers/vga_textmode.h>

#define VIDEO_BOTTOM ((VD_ROWS * VD_COLS) - 1)
#define VD_COLS (80 * 2)
#define VD_ROWS 25

static void escape_parse(char c);
static void text_putchar(char c);

static char *video_mem = (char *)0xb8000;
static size_t cursor_offset = 0;
static int cursor_status = 1;
static uint8_t text_palette = 0x07;
static uint8_t cursor_palette = 0x70;
static int escape = 0;
static int esc_value0 = 0;
static int esc_value1 = 0;
static int *esc_value = &esc_value0;
static int esc_default0 = 1;
static int esc_default1 = 1;
static int *esc_default = &esc_default0;

static void clear_cursor(void) {
    video_mem[cursor_offset + 1] = text_palette;
    return;
}

static void draw_cursor(void) {
    if (cursor_status) {
        video_mem[cursor_offset + 1] = cursor_palette;
    }
    return;
}

static void scroll(void) {
    // move the text up by one row
    for (size_t i = 0; i <= VIDEO_BOTTOM - VD_COLS; i++)
        video_mem[i] = video_mem[i + VD_COLS];
    // clear the last line of the screen
    for (size_t i = VIDEO_BOTTOM; i > VIDEO_BOTTOM - VD_COLS; i -= 2) {
        video_mem[i] = text_palette;
        video_mem[i - 1] = ' ';
    }
    return;
}

static void text_clear(void) {
    clear_cursor();
    for (size_t i = 0; i < VIDEO_BOTTOM; i += 2) {
        video_mem[i] = ' ';
        video_mem[i + 1] = text_palette;
    }
    cursor_offset = 0;
    draw_cursor();
    return;
}

static void text_clear_no_move(void) {
    clear_cursor();
    for (size_t i = 0; i < VIDEO_BOTTOM; i += 2) {
        video_mem[i] = ' ';
        video_mem[i + 1] = text_palette;
    }
    draw_cursor();
    return;
}

void init_vga_textmode(void) {
    port_out_b(0x3d4, 0x0a);
    port_out_b(0x3d5, 0x20);
    text_clear();
    return;
}

static void text_enable_cursor(void) {
    cursor_status = 1;
    draw_cursor();
    return;
}

static void text_disable_cursor(void) {
    cursor_status = 0;
    clear_cursor();
    return;
}

static void text_set_cursor_palette(uint8_t c) {
    cursor_palette = c;
    draw_cursor();
    return;
}

static uint8_t text_get_cursor_palette(void) {
    return cursor_palette;
}

static void text_set_text_palette(uint8_t c) {
    text_palette = c;
    return;
}

static uint8_t text_get_text_palette(void) {
    return text_palette;
}

static int text_get_cursor_pos_x(void) {
    return (cursor_offset % VD_COLS) / 2;
}

static int text_get_cursor_pos_y(void) {
    return cursor_offset / VD_COLS;
}

static void text_set_cursor_pos(int x, int y) {
    clear_cursor();
    cursor_offset = y * VD_COLS + x * 2;
    draw_cursor();
    return;
}

void text_write(const char *buf, size_t count) {
    for (size_t i = 0; i < count; i++)
        text_putchar(buf[i]);
}

static void text_putchar(char c) {
    if (escape) {
        escape_parse(c);
        return;
    }
    switch (c) {
        case 0x00:
            break;
        case 0x1B:
            escape = 1;
            return;
        case 0x0A:
            if (text_get_cursor_pos_y() == (VD_ROWS - 1)) {
                clear_cursor();
                scroll();
                text_set_cursor_pos(0, (VD_ROWS - 1));
            } else {
                text_set_cursor_pos(0, (text_get_cursor_pos_y() + 1));
            }
            break;
        case 0x08:
            if (cursor_offset) {
                clear_cursor();
                cursor_offset -= 2;
                video_mem[cursor_offset] = ' ';
                draw_cursor();
            }
            break;
        default:
            clear_cursor();
            video_mem[cursor_offset] = c;
            if (cursor_offset >= (VIDEO_BOTTOM - 1)) {
                scroll();
                cursor_offset = VIDEO_BOTTOM - (VD_COLS - 1);
            } else
                cursor_offset += 2;
            draw_cursor();
    }
    return;
}

static uint8_t ansi_colours[] = { 0, 4, 2, 6, 1, 5, 3, 7 };

static void sgr(void) {

    if (esc_value0 >= 30 && esc_value0 <= 37) {
        uint8_t pal = text_get_text_palette();
        pal = (pal & 0xf0) + ansi_colours[esc_value0 - 30];
        text_set_text_palette(pal);
        return;
    }

    if (esc_value0 >= 40 && esc_value0 <= 47) {
        uint8_t pal = text_get_text_palette();
        pal = (pal & 0x0f) + ansi_colours[esc_value0 - 40] * 0x10;
        text_set_text_palette(pal);
        return;
    }

    return;
}

static void escape_parse(char c) {

    if (c >= '0' && c <= '9') {
        *esc_value *= 10;
        *esc_value += c - '0';
        *esc_default = 0;
        return;
    }

    switch (c) {
        case '[':
            return;
        case ';':
            esc_value = &esc_value1;
            esc_default = &esc_default1;
            return;
        case 'A':
            if (esc_default0)
                esc_value0 = 1;
            if (esc_value0 > text_get_cursor_pos_y())
                esc_value0 = text_get_cursor_pos_y();
            text_set_cursor_pos(text_get_cursor_pos_x(),
                                text_get_cursor_pos_y() - esc_value0);
            break;
        case 'B':
            if (esc_default0)
                esc_value0 = 1;
            if ((text_get_cursor_pos_y() + esc_value0) > (VD_ROWS - 1))
                esc_value0 = (VD_ROWS - 1) - text_get_cursor_pos_y();
            text_set_cursor_pos(text_get_cursor_pos_x(),
                                text_get_cursor_pos_y() + esc_value0);
            break;
        case 'C':
            if (esc_default0)
                esc_value0 = 1;
            if ((text_get_cursor_pos_x() + esc_value0) > (VD_COLS / 2 - 1))
                esc_value0 = (VD_COLS / 2 - 1) - text_get_cursor_pos_x();
            text_set_cursor_pos(text_get_cursor_pos_x() + esc_value0,
                                text_get_cursor_pos_y());
            break;
        case 'D':
            if (esc_default0)
                esc_value0 = 1;
            if (esc_value0 > text_get_cursor_pos_x())
                esc_value0 = text_get_cursor_pos_x();
            text_set_cursor_pos(text_get_cursor_pos_x() - esc_value0,
                                text_get_cursor_pos_y());
            break;
        case 'H':
            esc_value0--;
            esc_value1--;
            if (esc_default0)
                esc_value0 = 0;
            if (esc_default1)
                esc_value1 = 0;
            if (esc_value1 >= (VD_COLS / 2))
                esc_value1 = (VD_COLS / 2) - 1;
            if (esc_value0 >= VD_ROWS)
                esc_value0 = VD_ROWS - 1;
            text_set_cursor_pos(esc_value1, esc_value0);
            break;
        case 'm':
            sgr();
            break;
        case 'J':
            switch (esc_value0) {
                case 2:
                    text_clear_no_move();
                    break;
                default:
                    break;
            }
            break;
        default:
            escape = 0;
            text_putchar('?');
            break;
    }

    esc_value = &esc_value0;
    esc_value0 = 0;
    esc_value1 = 0;
    esc_default = &esc_default0;
    esc_default0 = 1;
    esc_default1 = 1;
    escape = 0;

    return;
}