#include "tui.h"

#include <time.h>
#include <ncurses.h>
#include <readline/readline.h>

#include "caret_notater.h"

extern bool do_execute;

/* I fucking hate readline.
 * Apparently the only way to set an initial value is using a hook.
 * What makes this extra painful is that readline cannot be explicitly
 *  initialized nor is it documented clearly that shit will segfault
 *  otherwise.
 * If I ever find out what is a sensible alternative im ditching it forever.
 */
const char * initial_text;

const char * get_input_line(void) { return rl_line_buffer; }

/* "Cursor" position; the entry selected by the user
 */
size_t selection_relative = 0;
size_t selection_offset   = 0;
size_t entry_lines;

bool is_input_changed = true;
bool do_redisplay     = true;

/* Counter to signal which line to print to,
 *  and saved counterpart which is used to
 *  determine how many lines were printed.
 */
static size_t entry_line_index      = 0;
static size_t last_entry_line_index = 0;

static char version_string[] =
#  include "version.inc"
;

// Ncurses fun
static WINDOW * main_window;
static WINDOW * entry_window;
static WINDOW * input_window;
static WINDOW * version_window;

// Readline requirements
static int input_available = false;
static int input;

//
static inline void update_input(void);
static void full_redraw(void);

static bool do_fullredraw = true;

int init_tui(void) {
    // Ncurses
    initscr();
    nonl();
    cbreak();
    noecho();
    curs_set(0);
    keypad(stdscr, TRUE);

    entry_lines = LINES-3;

	main_window    = newwin(LINES-1, COLS,       0, 0);
    input_window   = newwin(      1, COLS, LINES-1, 0);
    entry_window   = subwin(main_window, entry_lines,                 COLS-2, 1, 1);
    version_window = subwin(main_window,           1, strlen(version_string), 0, 5);
    refresh();

    // Readline
    rl_bind_key('\t', rl_insert);
    rl_catch_signals        = 0;
    rl_catch_sigwinch       = 0;
    rl_change_environment   = 0;
    rl_prep_term_function   = NULL;
    rl_deprep_term_function = NULL;

    int getc_function([[maybe_unused]] FILE* ignore) { input_available = false; return (int)(input); }
    int return_input_available(void) { return input_available; }
    int initializer(void) { rl_insert_text(initial_text); return 0; }
    rl_getc_function        = getc_function;
    rl_input_available_hook = return_input_available;
    rl_startup_hook         = initializer;
    /* Due to this bug: https://mail.gnu.org/archive/html/bug-readline/2013-09/msg00021.html ;
     *  we cannot null this function.
     * Im seriously questioning why readline is still the """default""" library in the wild
     *  and whether i should participate.
     */
    void redisplay_nop(void) { return; }
    rl_redisplay_function   = redisplay_nop;
    /* We must specify an input handler or readline chimps out,
     *  but we dont want the line to be actually submittable,
     *  (search is continous and that would delete what the user
     *   has typedso far)
     *  so we also override enter to do nothing.
     */
    void no_op_handler([[maybe_unused]] char *line) { return; }
    int  no_op_bind([[maybe_unused]] int i, [[maybe_unused]] int h) { return 0; }
	rl_callback_handler_install("", no_op_handler);
    rl_bind_key('\n', no_op_bind);

    return 0;
}

int deinit_tui(void) {
	endwin();
    return 0;
}

void tui_append_back(const entry_t entry) {
    const int TIME_SIZE = 19 + 1;
    char time_buffer[TIME_SIZE];
    strftime(time_buffer, TIME_SIZE, "%Y-%m-%d %H:%M:%S", localtime((time_t*)&entry.timestamp));
    char caret_notation_buffer[(strlen(entry.command)*2)+1];
    const size_t current_line_y = (entry_lines-1)-entry_line_index;

    if (entry_line_index == selection_relative) {
        wattron(entry_window, A_REVERSE);
    }
    mvwprintw(entry_window, current_line_y, 0,
                    "%s  %.*s",
                        time_buffer,
                        getmaxx(entry_window) - (TIME_SIZE-1) - /*space-padding*/2,
                        string_to_caret_notation(entry.command,
                                                    strlen(entry.command),
                                                    caret_notation_buffer
                                                )
                );
    if (entry_line_index == selection_relative) {
        wattroff(entry_window, A_REVERSE);
    }

    /* Only delete to the end of the line if the cursor did not overflow
     *  to the line below from a long entry.
     */
    if ((size_t)getcury(entry_window) == current_line_y) {
        wclrtoeol(entry_window);
    }

    ++entry_line_index;
}

static
void full_redraw(void) {
    box(main_window, 0, 0);

    waddstr(version_window, version_string);

    update_input();

    wnoutrefresh(version_window);
    doupdate();
}

void tui_rearm() {
    entry_line_index = 0;
}

static inline
void update_input() {
    const char * const prompt = "$ ";

    wmove(input_window, 0, 0);
    waddstr(input_window, prompt);

    waddnstr(input_window, rl_line_buffer, rl_point);
    wattron(input_window, A_REVERSE);
    if (rl_point == rl_end) {
        waddch(input_window, ' ');
        wattroff(input_window, A_REVERSE);
    } else {
        waddch(input_window, rl_line_buffer[rl_point]);
        wattroff(input_window, A_REVERSE);
        waddnstr(input_window, rl_line_buffer + rl_point + 1, rl_end - rl_point);
    }

    wclrtoeol(input_window);

    wnoutrefresh(input_window);
}

void tui_refresh(void) {
    // XXX: this is dirty
    if (selection_relative > last_entry_line_index-1) {
        selection_relative = last_entry_line_index-2;
        do_redisplay = true;
        return;
    }

    if (do_fullredraw) {
        do_fullredraw = false;
        full_redraw();
        return;
    }

    last_entry_line_index = entry_line_index;
    while (entry_line_index < entry_lines) {
        wmove(entry_window, (entry_lines-1)-entry_line_index++, 0);
        wclrtoeol(entry_window);
    }
    entry_line_index = 0;

    update_input();

    wnoutrefresh(entry_window);
    wnoutrefresh(main_window);
    doupdate();
}


void tui_take_input(void) {
    extern bool do_run;
    const size_t paging_size = entry_lines / 2;

	input = wgetch(stdscr);
    switch (input) {
        case KEY_UP:
        case CTRL('p'):
        case CTRL('k'): {
            if (selection_relative == last_entry_line_index-1
            &&  entry_lines != last_entry_line_index) {
                break;
            }
            if (selection_relative != entry_lines-1) {
                ++selection_relative;
                do_redisplay = true;
            } else {
                ++selection_offset;
                is_input_changed = true;
            }
        } break;
        case KEY_DOWN:
        case CTRL('n'):
        case CTRL('j'): {
            if (selection_relative != 0) {
                --selection_relative;
                do_redisplay = true;
            } else {
                if (selection_offset != 0) {
                    --selection_offset;
                    is_input_changed = true;
                }
            }
        } break;
        case KEY_PPAGE:
        case CTRL('u'): {
            selection_offset += paging_size;
            is_input_changed = true;
        } break;
        case KEY_NPAGE:
        case CTRL('d'): {
            if (selection_offset == 0) {
                selection_relative = 0;
                do_redisplay = true;
            } else
            if (selection_offset > paging_size) {
                selection_offset -= paging_size;
                is_input_changed = true;
            } else {
                selection_offset = 0;
                is_input_changed = true;
            }
        } break;
        case CTRL('q'): {
            do_execute = false;
            do_run = false;
        } break;
        case KEY_LEFT: {
            if (rl_point != 0) {
                --rl_point;
                is_input_changed = true;
            }
        } break;
        case KEY_RIGHT: {
            if (rl_point != rl_end) {
                ++rl_point;
                is_input_changed = true;
            }
        } break;
        case '\r': {
            do_run = false;
        } break;
        case ERR: break;
        default: {
            input_available = true;
            rl_callback_read_char();
            is_input_changed = true;
        } break;
    }

    if (is_input_changed) {
        do_redisplay = true;
    }
}