#define TFT_BLACK   0x0000
#define TFT_BLUE    0x0014
#define TFT_RED     0xA000
#define TFT_GREEN   0x0500
#define TFT_CYAN    0x0514
#define TFT_MAGENTA 0xA014
#define TFT_YELLOW  0xA500
#define TFT_WHITE   0xA514

#define TFT_BOLD_BLACK   0x8410
#define TFT_BOLD_BLUE    0x001F
#define TFT_BOLD_RED     0xF800
#define TFT_BOLD_GREEN   0x07E0
#define TFT_BOLD_CYAN    0x07FF
#define TFT_BOLD_MAGENTA 0xF81F
#define TFT_BOLD_YELLOW  0xFFE0
#define TFT_BOLD_WHITE   0xFFFF

#include "tintty.h"
#include "font454.h"

// exported variable for input logic
// @todo refactor
bool tintty_cursor_key_mode_application;

const uint16_t ANSI_COLORS[] = {
    TFT_BLACK,
    TFT_RED,
    TFT_GREEN,
    TFT_YELLOW,
    TFT_BLUE,
    TFT_MAGENTA,
    TFT_CYAN,
    TFT_WHITE
};

const uint16_t ANSI_BOLD_COLORS[] = {
    TFT_BOLD_BLACK,
    TFT_BOLD_RED,
    TFT_BOLD_GREEN,
    TFT_BOLD_YELLOW,
    TFT_BOLD_BLUE,
    TFT_BOLD_MAGENTA,
    TFT_BOLD_CYAN,
    TFT_BOLD_WHITE
};

// cursor animation
const int16_t IDLE_CYCLE_MAX = 500;
const int16_t IDLE_CYCLE_ON = (IDLE_CYCLE_MAX/2);

const int16_t TAB_SIZE = 4;

// cursor and character position is in global buffer coordinate space (may exceed screen height)
struct tintty_state {
    // @todo consider storing cursor position as single int offset
    int16_t cursor_col, cursor_row;
    uint16_t bg_ansi_color, fg_ansi_color;
    bool bold;

    // cursor mode
    bool cursor_key_mode_application;

    // saved DEC cursor info (in screen coords)
    int16_t dec_saved_col, dec_saved_row, dec_saved_bg, dec_saved_fg;
    uint8_t dec_saved_g4bank;
    bool dec_saved_bold, dec_saved_no_wrap;

    // @todo deal with integer overflow
    int16_t top_row; // first displayed row in a logical scrollback buffer
    bool no_wrap;
    bool cursor_hidden;

    char out_char;
    int16_t out_char_col, out_char_row;
    uint8_t out_char_g4bank; // current set shift state, G0 to G3
    int16_t out_clear_before, out_clear_after;

    uint8_t g4bank_char_set[4];

    int16_t idle_cycle_count; // @todo track during blocking reads mid-command
} state;

struct tintty_rendered {
    int16_t cursor_col, cursor_row;
    int16_t top_row;
} rendered;

// @todo support negative cursor_row
void _render(tintty_display *display) {
    // expose the cursor key mode state
    tintty_cursor_key_mode_application = state.cursor_key_mode_application;

    // if scrolling, prepare the "recycled" screen area
    if (state.top_row != rendered.top_row) {
        // clear the new piece of screen to be recycled as blank space
        // @todo handle scroll-up
        if (state.top_row > rendered.top_row) {
            // pre-clear the lines at the bottom
            // @todo always use black instead of current background colour?
            // @todo deal with overflow from multiplication by CHAR_HEIGHT
            /*int16_t old_bottom_y = rendered.top_row * FONT_HEIGHT + display->screen_row_count * FONT_HEIGHT; // bottom of text may not align with screen height
            int16_t new_bottom_y = state.top_row * FONT_HEIGHT + display->screen_height; // extend to bottom edge of new displayed area
            int16_t clear_sbuf_bottom = new_bottom_y % display->screen_height;
            int16_t clear_height = min((int)display->screen_height, new_bottom_y - old_bottom_y);
            int16_t clear_sbuf_top = clear_sbuf_bottom - clear_height;*/

            // if rectangle straddles the screen buffer top edge, render that slice at bottom edge
            /*if (clear_sbuf_top < 0) {
                display->fill_rect(
                    0,
                    clear_sbuf_top + display->screen_height,
                    display->screen_width,
                    -clear_sbuf_top,
                    ANSI_COLORS[state.bg_ansi_color]
                );
            }*/

            // if rectangle is not entirely above top edge, render the normal slice
            /*if (clear_sbuf_bottom > 0) {
                display->fill_rect(
                    0,
                    max(0, (int)clear_sbuf_top),
                    display->screen_width,
                    clear_sbuf_bottom - max(0, (int)clear_sbuf_top),
                    ANSI_COLORS[state.bg_ansi_color]
                );
            }*/
        }

        // update displayed scroll
        display->set_vscroll((state.top_row) % display->screen_row_count); // @todo deal with overflow from multiplication

        // save rendered state
        rendered.top_row = state.top_row;
    }

    // render character if needed
    if (state.out_char != 0) {
        const uint16_t fg_tft_color = state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color];
        const uint16_t bg_tft_color = ANSI_COLORS[state.bg_ansi_color];
        const uint8_t char_set = state.g4bank_char_set[state.out_char_g4bank & 0x03]; // ensure 0-3 value

        display->print_character(state.out_char_col, state.out_char_row, fg_tft_color, bg_tft_color, state.out_char);

        // line-before
        // @todo detect when straddling edge of buffer
        if (state.out_clear_before > 0) {
            const int16_t line_before_chars = min(state.out_char_col, state.out_clear_before);
            const int16_t lines_before = (state.out_clear_before - line_before_chars) / display->screen_col_count;

            display->fill_rect(
                (state.out_char_col - line_before_chars),
                (state.out_char_row) % display->screen_row_count, // @todo deal with overflow from multiplication
                line_before_chars,
                1,
                ANSI_COLORS[state.bg_ansi_color]
            );

            for (int16_t i = 0; i < lines_before; i += 1) {
                display->fill_rect(
                    0,
                    (state.out_char_row - 1 - i) % display->screen_row_count, // @todo deal with overflow from multiplication
                    display->screen_col_count,
                    1,
                    ANSI_COLORS[state.bg_ansi_color]
                );
            }
        }

        // line-after
        // @todo detect when straddling edge of buffer
        if (state.out_clear_after > 0) {
            const int16_t line_after_chars = min(display->screen_col_count - 1 - state.out_char_col, (int) state.out_clear_after);
            const int16_t lines_after = (state.out_clear_after - line_after_chars) / display->screen_col_count;

            display->fill_rect(
                state.out_char_col + 1,
                (state.out_char_row) % display->screen_row_count, // @todo deal with overflow from multiplication
                line_after_chars,
                1,
                ANSI_COLORS[state.bg_ansi_color]
            );

            for (int16_t i = 0; i < lines_after; i += 1) {
                display->fill_rect(
                    0,
                    (state.out_char_row + 1 + i) % display->screen_row_count, // @todo deal with overflow from multiplication
                    display->screen_col_count,
                    1,
                    ANSI_COLORS[state.bg_ansi_color]
                );
            }
        }
 
        // clear for next render
        state.out_char = 0;
        state.out_clear_before = 0;
        state.out_clear_after = 0;

        // the char draw may overpaint the cursor, in which case
        // mark it for repaint
        if (
            rendered.cursor_col == state.out_char_col &&
            rendered.cursor_row == state.out_char_row
        ) {
            display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
            rendered.cursor_col = -1;
        }
    }

    // reflect new cursor bar render state
    const bool cursor_bar_shown = (
        !state.cursor_hidden &&
        state.idle_cycle_count < IDLE_CYCLE_ON
    );

    // clear existing rendered cursor bar if needed
    // @todo detect if it is already cleared during scroll
    if (rendered.cursor_col >= 0) {
        if (
            !cursor_bar_shown ||
            rendered.cursor_col != state.cursor_col ||
            rendered.cursor_row != state.cursor_row
        ) {
          display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
            
          // record the fact that cursor bar is not on screen
          rendered.cursor_col = -1;
        }
    }

    // render new cursor bar if not already shown
    // (sometimes right after clearing existing bar)
    if (rendered.cursor_col < 0) {
        if (cursor_bar_shown) {
          display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
          display->print_cursor(state.cursor_col, state.cursor_row, state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]);
 
            // save new rendered state
            rendered.cursor_col = state.cursor_col;
            rendered.cursor_row = state.cursor_row;
        }
    }
}

void _ensure_cursor_vscroll(tintty_display *display) {
    // move displayed window down to cover cursor
    // @todo support scrolling up as well
    if (state.cursor_row - state.top_row >= display->screen_row_count) {
        state.top_row = state.cursor_row - display->screen_row_count + 1;
    }
}

void _send_sequence(
    void (*send_char)(char ch),
    char* str
) {
    // send zero-terminated sequence character by character
    while (*str) {
        send_char(*str);
        str += 1;
    }
}

char _read_decimal(
    char (*peek_char)(),
    char (*read_char)()
) {
    uint16_t accumulator = 0;

    while (isdigit(peek_char())) {
        const char digit_character = read_char();
        const uint16_t digit = digit_character - '0';
        accumulator = accumulator * 10 + digit;
    }

    return accumulator;
}

void _apply_graphic_rendition(
    uint16_t* arg_list,
    uint16_t arg_count
) {
    if (arg_count == 0) {
        // special case for resetting to default style
        state.bg_ansi_color = 0;
        state.fg_ansi_color = 7;
        state.bold = false;

        return;
    }

    // process commands
    // @todo support bold/etc for better colour support
    // @todo 39/49?
    for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) {
        const uint16_t arg_value = arg_list[arg_index];

        if (arg_value == 0) {
            // reset to default style
            state.bg_ansi_color = 0;
            state.fg_ansi_color = 7;
            state.bold = false;
        } else if (arg_value == 1) {
            // bold
            state.bold = true;
        } else if (arg_value >= 30 && arg_value <= 37) {
            // foreground ANSI colour
            state.fg_ansi_color = arg_value - 30;
        } else if (arg_value >= 40 && arg_value <= 47) {
            // background ANSI colour
            state.bg_ansi_color = arg_value - 40;
        }
    }
}

void _apply_mode_setting(
    bool mode_on,
    uint16_t* arg_list,
    uint16_t arg_count
) {
    // process modes
    for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) {
        const uint16_t mode_id = arg_list[arg_index];

        switch (mode_id) {
            case 4:
                // insert/replace mode
                // @todo this should be off for most practical purposes anyway?
                // ... otherwise visually shifting line text is expensive
                break;

            case 20:
                // auto-LF
                // ignoring per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.6
                break;

            case 34:
                // cursor visibility
                state.cursor_hidden = !mode_on;
                break;
        }
    }
}

void _exec_escape_question_command(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char ch)
) {
    // @todo support multiple mode commands
    // per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.1,
    // ANSI and DEC modes cannot mix; that is, '[?25;20;?7l' is not a valid Esc-command
    // (noting this because https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
    // makes it look like the question mark is a prefix)
    const uint16_t mode = _read_decimal(peek_char, read_char);
    const bool mode_on = (read_char() != 'l');

    switch (mode) {
        case 1:
            // cursor key mode (normal/application)
            state.cursor_key_mode_application = mode_on;
            break;

        case 7:
            // auto wrap mode
            state.no_wrap = !mode_on;
            break;

        case 25:
            // cursor visibility
            state.cursor_hidden = !mode_on;
            break;
    }
}

// @todo cursor position report
void _exec_escape_bracket_command_with_args(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char ch),
    tintty_display *display,
    uint16_t* arg_list,
    uint16_t arg_count
) {
    // convenient arg getter
    #define ARG(index, default_value) (arg_count > index ? arg_list[index] : default_value)

    // process next character after Escape-code, bracket and any numeric arguments
    const char command_character = read_char();

    switch (command_character) {
        case '?':
            // question-mark commands
            _exec_escape_question_command(peek_char, read_char, send_char);
            break;

        case 'A':
            // cursor up (no scroll)
            state.cursor_row = max((int)state.top_row, state.cursor_row - ARG(0, 1));
            break;

        case 'B':
            // cursor down (no scroll)
            state.cursor_row = min(state.top_row + display->screen_row_count - 1, state.cursor_row + ARG(0, 1));
            break;

        case 'C':
            // cursor right (no scroll)
            state.cursor_col = min(display->screen_col_count - 1, state.cursor_col + ARG(0, 1));
            break;

        case 'D':
            // cursor left (no scroll)
            state.cursor_col = max(0, state.cursor_col - ARG(0, 1));
            break;

        case 'H':
        case 'f':
            // Direct Cursor Addressing (row;col)
            state.cursor_col = max(0, min(display->screen_col_count - 1, ARG(1, 1) - 1));
            state.cursor_row = state.top_row + max(0, min(display->screen_row_count - 1, ARG(0, 1) - 1));
            break;

        case 'J':
            // clear screen
            state.out_char = ' ';
            state.out_char_col = state.cursor_col;
            state.out_char_row = state.cursor_row;

            {
                const int16_t rel_row = state.cursor_row - state.top_row;

                state.out_clear_before = ARG(0, 0) != 0
                    ? rel_row * display->screen_col_count + state.cursor_col
                    : 0;
                state.out_clear_after = ARG(0, 0) != 1
                    ? (display->screen_row_count - 1 - rel_row) * display->screen_col_count + (display->screen_col_count - 1 - state.cursor_col)
                    : 0;
            }

            break;

        case 'K':
            // clear line
            state.out_char = ' ';
            state.out_char_col = state.cursor_col;
            state.out_char_row = state.cursor_row;

            state.out_clear_before = ARG(0, 0) != 0
                ? state.cursor_col
                : 0;
            state.out_clear_after = ARG(0, 0) != 1
                ? display->screen_col_count - 1 - state.cursor_col
                : 0;

            break;

        case 'm':
            // graphic rendition mode
            _apply_graphic_rendition(arg_list, arg_count);
            break;

        case 'h':
            // set mode
            _apply_mode_setting(true, arg_list, arg_count);
            break;

        case 'l':
            // unset mode
            _apply_mode_setting(false, arg_list, arg_count);
            break;
    }
}

void _exec_escape_bracket_command(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char ch),
    tintty_display *display
) {
    const uint16_t MAX_COMMAND_ARG_COUNT = 10;
    uint16_t arg_list[MAX_COMMAND_ARG_COUNT];
    uint16_t arg_count = 0;

    // start parsing arguments if any
    // (this means that '' is treated as no arguments, but '0;' is treated as two arguments, each being zero)
    // @todo ignore trailing semi-colon instead of treating it as marking an extra zero arg?
    if (isdigit(peek_char())) {
        // keep consuming arguments while we have space
        while (arg_count < MAX_COMMAND_ARG_COUNT) {
            // consume decimal number
            arg_list[arg_count] = _read_decimal(peek_char, read_char);
            arg_count += 1;

            // stop processing if next char is not separator
            if (peek_char() != ';') {
                break;
            }

            // consume separator before starting next argument
            read_char();
        }
    }

    _exec_escape_bracket_command_with_args(
        peek_char,
        read_char,
        send_char,
        display,
        arg_list,
        arg_count
    );
}

// set the characters displayed for given G0-G3 bank
void _exec_character_set(
    uint8_t g4bank_index,
    char (*read_char)()
) {
    switch (read_char()) {
        case 'A':
        case 'B':
            // normal character set (UK/US)
            state.g4bank_char_set[g4bank_index] = 0;
            break;

        case '0':
            // line-drawing
            state.g4bank_char_set[g4bank_index] = 1;
            break;

        default:
            // alternate sets are unsupported
            state.g4bank_char_set[g4bank_index] = 0;
            break;
    }
}

// @todo terminal reset
// @todo parse modes with arguments even if they are no-op
void _exec_escape_code(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char ch),
    tintty_display *display
) {
    // read next character after Escape-code
    // @todo time out?
    char esc_character = read_char();

    // @todo support for (, ), #, c, cursor save/restore
    switch (esc_character) {
        case '[':
            _exec_escape_bracket_command(peek_char, read_char, send_char, display);
            break;

        case 'D':
            // index (move down and possibly scroll)
            state.cursor_row += 1;
            _ensure_cursor_vscroll(display);
            break;

        case 'M':
            // reverse index (move up and possibly scroll)
            state.cursor_row -= 1;
            _ensure_cursor_vscroll(display);
            break;

        case 'E':
            // next line
            state.cursor_row += 1;
            state.cursor_col = 0;
            _ensure_cursor_vscroll(display);
            break;

        case 'Z':
            // Identify Terminal (DEC Private)
            _send_sequence(send_char, "\e[?1;0c"); // DA response: no options
            break;

        case '7':
            // save cursor
            // @todo verify that the screen-relative coordinate approach is valid
            state.dec_saved_col = state.cursor_col;
            state.dec_saved_row = state.cursor_row - state.top_row; // relative to top
            state.dec_saved_bg = state.bg_ansi_color;
            state.dec_saved_fg = state.fg_ansi_color;
            state.dec_saved_g4bank = state.out_char_g4bank;
            state.dec_saved_bold = state.bold;
            state.dec_saved_no_wrap = state.no_wrap;
            break;

        case '8':
            // restore cursor
            state.cursor_col = state.dec_saved_col;
            state.cursor_row = state.dec_saved_row + state.top_row; // relative to top
            state.bg_ansi_color = state.dec_saved_bg;
            state.fg_ansi_color = state.dec_saved_fg;
            state.out_char_g4bank = state.dec_saved_g4bank;
            state.bold = state.dec_saved_bold;
            state.no_wrap = state.dec_saved_no_wrap;
            break;

        case '=':
        case '>':
            // keypad mode setting - ignoring
            break;

        case '(':
            // set G0
            _exec_character_set(0, read_char);
            break;

        case ')':
            // set G1
            _exec_character_set(1, read_char);
            break;

        case '*':
            // set G2
            _exec_character_set(2, read_char);
            break;

        case '+':
            // set G3
            _exec_character_set(3, read_char);
            break;

        default:
            // unrecognized character, silently ignore
            break;
    }
}

void _main(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char str),
    tintty_display *display,
    tintty_keyboard *keyboard
) {
    // start in default idle state
    char initial_character = read_char();

    if (initial_character >= 0x20 && initial_character <= 0x7e) {
        // output displayable character
        state.out_char = initial_character;
        state.out_char_col = state.cursor_col;
        state.out_char_row = state.cursor_row;

        // update caret
        state.cursor_col += 1;

        if (state.cursor_col >= display->screen_col_count) {
            if (state.no_wrap) {
                state.cursor_col = display->screen_col_count - 1;
            } else {
                state.cursor_col = 0;
                state.cursor_row += 1;
                _ensure_cursor_vscroll(display);
            }
        }

        // reset idle state
        state.idle_cycle_count = 0;
    } else {
        // @todo bell, answer-back (0x05), delete
        switch (initial_character) {
            case '\a':
                // bell
                keyboard->bell();
                break;
            case '\n':
                // line-feed
                state.cursor_row += 1;
                _ensure_cursor_vscroll(display);
                break;

            case '\r':
                // carriage-return
                state.cursor_col = 0;
                break;

            case '\b':
                // backspace
                state.cursor_col -= 1;

                if (state.cursor_col < 0) {
                    if (state.no_wrap) {
                        state.cursor_col = 0;
                    } else {
                        state.cursor_col = display->screen_col_count - 1;
                        state.cursor_row -= 1;
                        _ensure_cursor_vscroll(display);
                    }
                }

                break;

            case '\t':
                // tab
                {
                    // @todo blank out the existing characters? not sure if that is expected
                    const int16_t tab_num = state.cursor_col / TAB_SIZE;
                    state.cursor_col = min(display->screen_col_count - 1, (tab_num + 1) * TAB_SIZE);
                }
                break;

            case '\e':
                // Escape-command
                _exec_escape_code(peek_char, read_char, send_char, display);
                break;

            case '\x0f':
                // Shift-In (use G0)
                // see also the fun reason why these are called this way:
                // https://en.wikipedia.org/wiki/Shift_Out_and_Shift_In_characters
                state.out_char_g4bank = 0;
                break;

            case '\x0e':
                // Shift-Out (use G1)
                state.out_char_g4bank = 1;
                break;

            default:
                // nothing, just animate cursor
                delay(1);
                state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX;
        }
    }

    _render(display);
}

void tintty_run(
    char (*peek_char)(),
    char (*read_char)(),
    void (*send_char)(char str),
    tintty_display *display,
    tintty_keyboard *keyboard
) {
    // set up initial state
    state.cursor_col = 0;
    state.cursor_row = 0;
    state.top_row = 0;
    state.no_wrap = 0;
    state.cursor_hidden = 0;
    state.bg_ansi_color = 0;
    state.fg_ansi_color = 7;
    state.bold = false;

    state.cursor_key_mode_application = false;

    state.dec_saved_col = 0;
    state.dec_saved_row = 0;
    state.dec_saved_bg = state.bg_ansi_color;
    state.dec_saved_fg = state.fg_ansi_color;
    state.dec_saved_g4bank = 0;
    state.dec_saved_bold = state.bold;
    state.dec_saved_no_wrap = false;

    state.out_char = 0;
    state.out_char_g4bank = 0;
    state.g4bank_char_set[0] = 0;
    state.g4bank_char_set[1] = 0;
    state.g4bank_char_set[2] = 0;
    state.g4bank_char_set[3] = 0;

    rendered.cursor_col = -1;
    rendered.cursor_row = -1;

    // clear screen
    display->fill_rect(0, 0, display->screen_width, display->screen_height, TFT_BLACK);

    // reset TFT scroll to default
    display->set_vscroll(0);

    // initial render
    _render(display);

    // send CR to indicate that the screen is ready
    // (this works with the agetty --wait-cr option to help wait until Arduino boots)
    send_char('\r');

    // main read cycle
    while (1) {
        _main(peek_char, read_char, send_char, display, keyboard);
    }
}

void tintty_idle(
    tintty_display *display
) {
    delay(1);
  
    // animate cursor
    state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX;

    // re-render
    _render(display);
}
