#include "console.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <ctype.h>
#include <assert.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdint.h>

#define ESC(x) "\x1B" x
#define CSI(x) ESC("[" x)
#define SGR(x) CSI(x "m")

static bool initialized = false;
static bool no_csi = false;
typedef struct listent_s listent_t;

struct listent_s {
    listent_t *prev;
    listent_t *next;
    char content[];
};

typedef struct {
    listent_t *first;
    listent_t *last;
} fifo_t;

static fifo_t lines;
static fifo_t history;

static void remove_entry(fifo_t *fifo, listent_t *entry)
{
    if (fifo->last == entry) {
        fifo->last = entry->prev;
    }
    if (fifo->first == entry) {
        fifo->first = entry->next;
    }
    if (entry->next) {
        entry->next->prev = entry->prev;
    }
    if (entry->prev) {
        entry->prev->next = entry->next;
    }
    free(entry);
}

static void add_entry(fifo_t *fifo, const char *content)
{
    size_t length = strlen(content);
    listent_t *entry = malloc(sizeof(*entry) + length + 1);
    entry->next = NULL;
    entry->prev = fifo->last;
    memcpy(entry->content, content, length);
    entry->content[length] = 0;
    if (fifo->last) {
        fifo->last->next = entry;
    }
    fifo->last = entry;
    if (!fifo->first) {
        fifo->first = entry;
    }
}

static listent_t *reverse_find(listent_t *entry, const char *string, bool exact)
{
    while (entry) {
        if (exact && strcmp(entry->content, string) == 0) {
            return entry;
        }
        if (!exact && strstr(entry->content, string)) {
            return entry;
        }
        entry = entry->prev;
    }
    return NULL;
}

static bool is_term(void)
{
    if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return false;
#ifdef _WIN32
    unsigned long input_mode, output_mode;
    bool has_con_output;
    
    HANDLE stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
    
    GetConsoleMode(stdin_handle, &input_mode);
    has_con_output = GetConsoleMode(stdout_handle, &output_mode);
    if (!has_con_output) {
        return false; // stdout has been redirected to a file or pipe
    }
    
    SetConsoleMode(stdin_handle, ENABLE_VIRTUAL_TERMINAL_INPUT);
    SetConsoleMode(stdout_handle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
    
    CONSOLE_SCREEN_BUFFER_INFO before = {0,};
    GetConsoleScreenBufferInfo(stdout_handle, &before);
    
    printf(SGR("0"));
    
    CONSOLE_SCREEN_BUFFER_INFO after = {0,};
    GetConsoleScreenBufferInfo(stdout_handle, &after);
    
    SetConsoleMode(stdin_handle, input_mode);
    SetConsoleMode(stdout_handle, output_mode);

    if (before.dwCursorPosition.X != after.dwCursorPosition.X ||
        before.dwCursorPosition.Y != after.dwCursorPosition.Y) {
        printf("\r      \r");
        no_csi = true;
    }
    
    return true;
#else
    return getenv("TERM");
#endif
}

static unsigned width, height;

static char raw_getc(void)
{
#ifdef _WIN32
    char c;
    unsigned long ret;
    ReadFile(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &ret, NULL);
#else
    ssize_t ret;
    char c;
    
    do {
        ret = read(STDIN_FILENO, &c, 1);
    } while (ret == -1 && errno == EINTR);
#endif
    return ret == 1? c : EOF;
}

#ifdef _WIN32
#pragma clang diagnostic ignored "-Wmacro-redefined"
#include <Windows.h>

static void update_size(void)
{
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
    width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}

static unsigned long input_mode, output_mode;

static void cleanup(void)
{
    if (!no_csi) {
        printf(CSI("!p")); // reset
    }
    fflush(stdout);
    SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode);
    SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode);
}

static bool initialize(void)
{
    if (!is_term()) return false;
    update_size();
    if (width == 0 || height == 0) {
        return false;
    }
    
    static bool once = false;
    if (!once) {
        atexit(cleanup);
        GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode);
        GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode);
        once = true;
    }
    if (no_csi) {
        initialized = true;
        return true;
    }
    SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT);
    SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);

    fprintf(stdout, CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height);
    
    fflush(stdout);
    initialized = true;
    return true;
}
#else
#include <sys/ioctl.h>

static void update_size(void)
{
    struct winsize winsize;
    ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize);
    width = winsize.ws_col;
    height = winsize.ws_row;
}

static void terminal_resized(int ignored)
{
    update_size();
}

#include <termios.h>
static struct termios terminal;


static void cleanup(void)
{
    if (!no_csi) {
        printf(CSI("!p")); // reset
    }
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal);
    fflush(stdout);
}

static bool initialize(void)
{
    if (!is_term()) return false;
    update_size();
    if (width == 0 || height == 0) {
        return false;
    }
    
    static bool once = false;
    if (!once) {
        atexit(cleanup);
        signal(SIGWINCH, terminal_resized);
        tcgetattr(STDIN_FILENO, &terminal);
#ifdef _WIN32
        _setmode(STDIN_FILENO, _O_TEXT);
#endif
        once = true;
    }
    struct termios raw_terminal;
    raw_terminal = terminal;
    raw_terminal.c_lflag = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_terminal);
    
    printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height);
    
    fflush(stdout);
    initialized = true;
    return true;
}
#endif

static struct {
    char *content;
    size_t allocation_size;
    size_t length;
    size_t position;
    size_t scroll;
    bool reverse_search;
    listent_t *search_line;
} line;

#define CTL(x) ((x) - 'A' + 1)

static const char *prompt = "";
static size_t prompt_length = 0;
static bool repeat_empty = false;

static bool redraw_prompt(bool force)
{
    if (no_csi) return true;
    
    if (line.reverse_search) {
        if (!force) return false;
        if (line.length == 0) {
            printf("\r" CSI("K") "%s" SGR("2") "Reverse Search..." SGR("0") CSI("%zuG"), prompt, prompt_length + 1);
            return true;
        }
        if (!line.search_line) {
            printf("\r" CSI("K") "%s" SGR("1") "%s" SGR("0"), prompt, line.content);
            return true;
        }
        const char *loc = strstr(line.search_line->content, line.content);
        printf("\r" CSI("K") "%s" "%.*s" SGR("1") "%s" SGR("0") "%s" CSI("%uG"),
               prompt,
               (int)(loc - line.search_line->content),
               line.search_line->content,
               line.content,
               loc + line.length,
               (unsigned)(loc - line.search_line->content + line.length + prompt_length + 1));
        return true;
    }
    
    size_t max = width - 1 - prompt_length;
    
    if (line.scroll && line.length <= max) {
        line.scroll = 0;
        force = true;
    }
    
    if (line.scroll > line.length - max) {
        line.scroll = line.length - max;
        force = true;
    }
    
    if (line.position < line.scroll + 1 && line.position) {
        line.scroll = line.position - 1;
        force = true;
    }
    
    if (line.position == 0 && line.scroll) {
        line.scroll = 0;
        force = true;
    }
    
    if (line.position > line.scroll + max) {
        line.scroll = line.position - max;
        force = true;
    }
    
    if (!force && line.length <= max) {
        return false;
    }
    
    if (line.length <= max) {
        printf("\r" CSI("K") "%s%s" CSI("%uG"), prompt, line.content, (unsigned)(line.position + prompt_length + 1));
        return true;
    }
    
    size_t left = max;
    const char *string = line.content + line.scroll;
    printf("\r" CSI("K") "%s", prompt);
    if (line.scroll) {
        printf(SGR("2") "%c" SGR("0"), *string);
        string++;
        left--;
    }
    if (line.scroll + max == line.length) {
        printf("%s", string);
    }
    else {
        printf("%.*s", (int)(left - 1), string);
        string += left;
        left = 1;
        printf(SGR("2") "%c" SGR("0"), *string);
    }
    printf(CSI("%uG"), (unsigned)(line.position - line.scroll + prompt_length + 1));
    
    return true;
}

static void set_position(size_t position)
{
    if (position > line.length) {
        printf("\a");
        return;
    }
    line.position = position;
    if (!redraw_prompt(false)) {
        printf(CSI("%uG"), (unsigned)(position + prompt_length + 1));
    }
}

static void set_line(const char *content)
{
    line.length = strlen(content);
    if (line.length + 1 > line.allocation_size) {
        line.content = realloc(line.content, line.length + 1);
        line.allocation_size = line.length + 1;
    }
    else if (line.allocation_size > 256 && line.length < 128) {
        line.content = realloc(line.content, line.length + 1);
        line.allocation_size = line.length + 1;
    }
    line.position = line.length;
    strcpy(line.content, content);
    redraw_prompt(true);
}

static void insert(const char *string)
{
    size_t insertion_length = strlen(string);
    size_t new_length = insertion_length + line.length;
    bool need_realloc = false;
    while (line.allocation_size < new_length + 1) {
        line.allocation_size *= 2;
        need_realloc = true;
    }
    if (need_realloc) {
        line.content = realloc(line.content, line.allocation_size);
    }
    memmove(line.content + line.position + insertion_length,
            line.content + line.position,
            line.length - line.position);
    memcpy(line.content + line.position, string, insertion_length);
    line.position += insertion_length;
    line.content[new_length] = 0;
    line.length = new_length;
    if (!redraw_prompt(line.position != line.length)) {
        printf("%s", string);
    }
}

static void delete(size_t size, bool forward)
{
    if (line.length < size) {
        printf("\a");
        return;
    }
    if (forward) {
        if (line.position > line.length - size) {
            printf("\a");
            return;
        }
        else {
            line.position += size;
        }
    }
    else if (line.position < size) {
        printf("\a");
        return;
    }
    memmove(line.content + line.position - size,
            line.content + line.position,
            line.length - line.position);
    line.length -= size;
    line.content[line.length] = 0;
    line.position -= size;
    
    if (!redraw_prompt(line.position != line.length)) {
        printf(CSI("%uG") CSI("K"),
               (unsigned)(line.position + prompt_length + 1));
    }
}

static void move_word(bool forward)
{
    signed offset = forward? 1 : -1;
    size_t end = forward? line.length : 0;
    signed check_offset = forward? 0 : -1;
    if (line.position == end) {
        printf("\a");
        return;
    }
    line.position += offset;
    while (line.position != end && isalnum(line.content[line.position + check_offset])) {
        line.position += offset;
    }
    if (!redraw_prompt(false)) {
        printf(CSI("%uG"), (unsigned)(line.position + prompt_length + 1));
    }
}

static void delete_word(bool forward)
{
    size_t original_pos = line.position;
    signed offset = forward? 1 : -1;
    size_t end = forward? line.length : 0;
    signed check_offset = forward? 0 : -1;
    if (line.position == end) {
        printf("\a");
        return;
    }
    line.position += offset;
    while (line.position != end && isalnum(line.content[line.position + check_offset])) {
        line.position += offset;
    }
    if (forward) {
        delete(line.position - original_pos, false);
    }
    else {
        delete(original_pos - line.position, true);
    }
}

#define MOD_ALT(x) (0x100 | x)
#define MOD_SHIFT(x) (0x200 | x)
#define MOD_CTRL(x) (0x400 | x)
#define MOD_SPECIAL(x) (0x800 | x)

static unsigned get_extended_key(void)
{
    unsigned modifiers = 0;
    char c = 0;
restart:
    c = raw_getc();
    if (c == 0x1B) {
        modifiers = MOD_SHIFT(MOD_ALT(0));
        goto restart;
    }
    else if (c != '[' && c != 'O') {
        return MOD_ALT(c);
    }
    unsigned ret = 0;
    while (true) {
        c = raw_getc();
        if (c >= '0' && c <= '9') {
            ret = ret * 10 + c - '0';
        }
        else if (c == ';') {
            if (ret == 1) {
                modifiers |= MOD_ALT(0);
            }
            else if (ret == 2) {
                modifiers |= MOD_SHIFT(0);
            }
            else if (ret == 5) {
                modifiers |= MOD_CTRL(0);
            }
            ret = 0;
        }
        else if (c == '~') {
            return MOD_SPECIAL(ret) | modifiers;
        }
        else {
            if (ret == 1) {
                modifiers |= MOD_ALT(0);
            }
            else if (ret == 2) {
                modifiers |= MOD_SHIFT(0);
            }
            else if (ret == 5) {
                modifiers |= MOD_CTRL(0);
            }
            return c | modifiers;
        }
    }
}

#define SWAP(x, y) do {typeof(*(x)) _tmp = *(x); *(x) = *(y);*(y) = _tmp;} while (0)
static pthread_mutex_t terminal_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t lines_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t lines_cond = PTHREAD_COND_INITIALIZER;
static void (*line_ready_callback)(void) = NULL;

static char reverse_search_mainloop(void)
{
    while (true) {
        char c = raw_getc();
        pthread_mutex_lock(&terminal_lock);
        
        switch (c) {
            case CTL('C'):
                line.search_line = NULL;
                set_line("");
                pthread_mutex_unlock(&terminal_lock);
                return CTL('A');
            case CTL('R'):
                line.search_line = reverse_find(line.search_line? line.search_line->prev : history.last, line.content, false);
                if (!line.search_line) {
                    printf("\a");
                }
                redraw_prompt(true);
                break;
            case CTL('W'):
                delete_word(false);
                redraw_prompt(true);
                break;
            case CTL('H'):
            case 0x7F: // Backspace
                delete(1, false);
                redraw_prompt(true);
                break;
            default:
                if (c >= ' ') {
                    char string[2] = {c, 0};
                    insert(string);
                    line.search_line = reverse_find(line.search_line?: history.last, line.content, false);
                    if (!line.search_line) {
                        printf("\a");
                    }
                    redraw_prompt(true);
                }
                else {
                    pthread_mutex_unlock(&terminal_lock);
                    return c;
                }
                break;
        }
        pthread_mutex_unlock(&terminal_lock);
        fflush(stdout);
    }
    
}
static void no_csi_mainloop(void)
{
    while (true) {
        char *expression = NULL;
        size_t size = 0;
        
        errno = 0;
        if (getline(&expression, &size, stdin) <= 0) {
            if (!errno) {
                continue;
            }
            return;
        }
        
        pthread_mutex_lock(&lines_lock);
        if (!expression) {
            add_entry(&lines, "");
        }
        else {
            size_t length = strlen(expression);
            if (expression[length - 1] == '\n') {
                expression[length - 1] = 0;
            }
            add_entry(&lines, expression);
            free(expression);
        }
        pthread_cond_signal(&lines_cond);
        pthread_mutex_unlock(&lines_lock);
        if (line_ready_callback) {
            line_ready_callback();
        }
    }
}


static
#ifdef _WIN32
int __stdcall
#else
void *
#endif
mainloop(char *(*completer)(const char *substring, uintptr_t *context))
{
    if (no_csi) {
        no_csi_mainloop();
        return 0;
    }
    
    listent_t *history_line = NULL;
    uintptr_t complete_context = 0;
    size_t completion_length = 0;
    while (true) {
        char c;
        if (line.reverse_search) {
            c = reverse_search_mainloop();
            line.reverse_search = false;
            if (line.search_line) {
                size_t pos = strstr(line.search_line->content, line.content) - line.search_line->content + line.length;
                set_line(line.search_line->content);
                line.search_line = NULL;
                set_position(pos);
            }
            else {
                redraw_prompt(true);
            }
        }
        else {
            c = raw_getc();
        }
        if (c == EOF) {
            return 0;
        }

        pthread_mutex_lock(&terminal_lock);
        
        switch (c) {
            case CTL('A'):
                set_position(0);
                complete_context = completion_length = 0;
                break;
            case CTL('B'):
                set_position(line.position - 1);
                complete_context = completion_length = 0;
                break;
            case CTL('C'):
                if (line.length) {
                    set_line("");
                    history_line = NULL;
                    complete_context = completion_length = 0;
                }
                else {
#ifdef _WIN32
                    raise(SIGINT);
#else
                    kill(getpid(), SIGINT);
#endif
                }
                break;
            case CTL('D'):
                if (line.length) {
                    delete(1, true);
                    complete_context = completion_length = 0;
                }
                else {
                    pthread_mutex_lock(&lines_lock);
                    add_entry(&lines, CON_EOF);
                    pthread_cond_signal(&lines_cond);
                    pthread_mutex_unlock(&lines_lock);
                    if (line_ready_callback) {
                        line_ready_callback();
                    }
                }
                break;
            case CTL('E'):
                set_position(line.length);
                complete_context = completion_length = 0;
                break;
            case CTL('F'):
                set_position(line.position + 1);
                complete_context = completion_length = 0;
                break;
            case CTL('K'):
                printf(CSI("K"));
                if (!redraw_prompt(false)) {
                    line.length = line.position;
                    line.content[line.length] = 0;
                }
                complete_context = completion_length = 0;
                break;
            case CTL('R'):
                complete_context = completion_length = 0;
                line.reverse_search = true;
                set_line("");
                
                break;
            case CTL('T'):
                if (line.length < 2) {
                    printf("\a");
                    break;
                }
                if (line.position && line.position == line.length) {
                    line.position--;
                }
                if (line.position == 0) {
                    printf("\a");
                    break;
                }
                SWAP(line.content + line.position,
                     line.content + line.position - 1);
                line.position++;
                redraw_prompt(true);
                complete_context = completion_length = 0;
                break;
            case CTL('W'):
                delete_word(false);
                complete_context = completion_length = 0;
                break;
            case '\r':
            case '\n':
                pthread_mutex_lock(&lines_lock);
                if (line.length == 0 && repeat_empty && history.last) {
                    add_entry(&lines, history.last->content);
                }
                else {
                    add_entry(&lines, line.content);
                }
                pthread_cond_signal(&lines_cond);
                pthread_mutex_unlock(&lines_lock);
                if (line_ready_callback) {
                    line_ready_callback();
                }
                if (line.length) {
                    listent_t *dup = reverse_find(history.last, line.content, true);
                    if (dup) {
                        remove_entry(&history, dup);
                    }
                    add_entry(&history, line.content);
                    set_line("");
                    history_line = NULL;
                }
                complete_context = completion_length = 0;
                break;
            case CTL('H'):
            case 0x7F: // Backspace
                delete(1, false);
                complete_context = completion_length = 0;
                break;
            case 0x1B:
                switch (get_extended_key()) {
                    case MOD_SPECIAL(1): // Home
                    case MOD_SPECIAL(7):
                    case 'H':
                        set_position(0);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_SPECIAL(8): // End
                    case 'F':
                        set_position(line.length);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_SPECIAL(3): // Delete
                        delete(1, true);
                        complete_context = completion_length = 0;
                        break;
                    case 'A': // Up
                        if (!history_line) {
                            history_line = history.last;
                        }
                        else {
                            history_line = history_line->prev;
                        }
                        if (history_line) {
                            set_line(history_line->content);
                            complete_context = completion_length = 0;
                        }
                        else {
                            history_line = history.first;
                            printf("\a");
                        }
                        
                        break;
                    case 'B': // Down
                        if (!history_line) {
                            printf("\a");
                            break;
                        }
                        history_line = history_line->next;
                        if (history_line) {
                            set_line(history_line->content);
                            complete_context = completion_length = 0;
                        }
                        else {
                            set_line("");
                            complete_context = completion_length = 0;
                        }
                        break;
                    case 'C': // Right
                        set_position(line.position + 1);
                        complete_context = completion_length = 0;
                        break;
                    case 'D': // Left
                        set_position(line.position - 1);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_ALT('b'):
                    case MOD_ALT('D'):
                        move_word(false);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_ALT('f'):
                    case MOD_ALT('C'):
                        move_word(true);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_ALT(0x7F): // ALT+Backspace
                        delete_word(false);
                        complete_context = completion_length = 0;
                        break;
                    case MOD_ALT('('): // ALT+Delete
                        delete_word(true);
                        complete_context = completion_length = 0;
                        break;
                    default:
                        printf("\a");
                        break;
                }
                break;
            case '\t': {
                if (!no_csi) {
                    char temp = line.content[line.position - completion_length];
                    line.content[line.position - completion_length] = 0;
                    char *completion = completer? completer(line.content, &complete_context) : NULL;
                    line.content[line.position - completion_length] = temp;
                    if (completion) {
                        if (completion_length) {
                            delete(completion_length, false);
                        }
                        insert(completion);
                        completion_length = strlen(completion);
                        free(completion);
                    }
                    else {
                        printf("\a");
                    }
                    break;
                }
            }
            default:
                if (c >= ' ') {
                    char string[2] = {c, 0};
                    insert(string);
                    complete_context = completion_length = 0;
                }
                else {
                    printf("\a");
                }
                break;
        }
        fflush(stdout);
        pthread_mutex_unlock(&terminal_lock);
    }
    return 0;
}


void CON_set_line_ready_callback(void (*callback)(void))
{
    line_ready_callback = callback;
}

char *CON_readline(const char *new_prompt)
{
    pthread_mutex_lock(&terminal_lock);
    const char *old_prompt = prompt;
    prompt = new_prompt;
    prompt_length = strlen(prompt);
    redraw_prompt(true);
    fflush(stdout);
    pthread_mutex_unlock(&terminal_lock);
    
    pthread_mutex_lock(&lines_lock);
    while (!lines.first) {
        pthread_cond_wait(&lines_cond, &lines_lock);
    }
    char *ret = strdup(lines.first->content);
    remove_entry(&lines, lines.first);
    pthread_mutex_unlock(&lines_lock);
    
    pthread_mutex_lock(&terminal_lock);
    prompt = old_prompt;
    prompt_length = strlen(prompt);
    redraw_prompt(true);
    fflush(stdout);
    pthread_mutex_unlock(&terminal_lock);
    return ret;
}

char *CON_readline_async(void)
{
    char *ret = NULL;
    pthread_mutex_lock(&lines_lock);
    if (lines.first) {
        ret = strdup(lines.first->content);
        remove_entry(&lines, lines.first);
    }
    pthread_mutex_unlock(&lines_lock);
    return ret;
}

bool CON_start(char *(*completer)(const char *substring, uintptr_t *context))
{
    if (!initialize()) {
        return false;
    }
    set_line("");
    pthread_t thread;
    return pthread_create(&thread, NULL, (void *)mainloop, completer) == 0;
}

void CON_attributed_print(const char *string, CON_attributes_t *attributes)
{
    if (!initialized || no_csi) {
        fprintf(stdout, "%s", string);
        return;
    }
    static bool pending_newline = false;
    pthread_mutex_lock(&terminal_lock);
    printf(ESC("8"));
    bool needs_reset = false;
    if (attributes) {
        if (attributes->color) {
            if (attributes->color >= 0x10) {
                printf(SGR("%d"), attributes->color - 0x11 + 90);
            }
            else {
                printf(SGR("%d"), attributes->color - 1 + 30);
            }
            needs_reset = true;
        }
        if (attributes->background) {
            if (attributes->background >= 0x10) {
                printf(SGR("%d"), attributes->background - 0x11 + 100);
            }
            else {
                printf(SGR("%d"), attributes->background - 1 + 40);
            }
            needs_reset = true;
        }
        if (attributes->bold) {
            printf(SGR("1"));
            needs_reset = true;
        }
        if (attributes->italic) {
            printf(SGR("3"));
            needs_reset = true;
        }
        if (attributes->underline) {
            printf(SGR("4"));
            needs_reset = true;
        }
    }
    const char *it = string;
    bool need_redraw_prompt = false;
    while (*it) {
        if (pending_newline) {
            need_redraw_prompt = true;
            printf("\n" CSI("K") "\n" CSI("A"));
            pending_newline = false;
            continue;
        }
        if (*it == '\n') {
            printf("%.*s", (int)(it - string), string);
            string = it + 1;
            pending_newline = true;
        }
        it++;
    }
    if (*string) {
        printf("%s", string);
    }
    if (needs_reset) {
        printf(SGR("0"));
    }
    printf(ESC("7") CSI("B"));
    if (need_redraw_prompt) {
        redraw_prompt(true);
    }
    else {
        set_position(line.position);
    }
    fflush(stdout);
    pthread_mutex_unlock(&terminal_lock);
}

void CON_print(const char *string)
{
    CON_attributed_print(string, NULL);
}

void CON_vprintf(const char *fmt, va_list args)
{
    char *string = NULL;
    vasprintf(&string, fmt, args);
    CON_attributed_print(string, NULL);
    free(string);
}

void CON_attributed_vprintf(const char *fmt, CON_attributes_t *attributes, va_list args)
{
    char *string = NULL;
    vasprintf(&string, fmt, args);
    CON_attributed_print(string, attributes);
    free(string);
}

void CON_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    CON_vprintf(fmt, args);
    va_end(args);
}


void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...)
{
    va_list args;
    va_start(args, attributes);
    CON_attributed_vprintf(fmt, attributes, args);
    va_end(args);
}

void CON_set_async_prompt(const char *string)
{
    pthread_mutex_lock(&terminal_lock);
    prompt = string;
    prompt_length = strlen(string);
    redraw_prompt(true);
    fflush(stdout);
    pthread_mutex_unlock(&terminal_lock);
}

void CON_set_repeat_empty(bool repeat)
{
    repeat_empty = repeat;
}

bool CON_no_csi_mode(void)
{
    return no_csi;
}
