// Copyright (c) 2023-2026 Chris (boreddevnl)
// This software is released under the GNU General Public License v3.0. See LICENSE file for details.
// This header needs to maintain in any file it is present in, as per the GPL license terms.
// BOREDOS_APP_DESC: BoredOS Terminal shell.
// BOREDOS_APP_ICONS: /Library/images/icons/colloid/xterm.png;/Library/images/icons/colloid/utilities-terminal_su.png
#include <stdlib.h>
#include <syscall.h>
#include "libc/libui.h"
#include "libc/input.h"
#include "utf-8.h"
#include <stdint.h>

#define DEFAULT_COLS 116
#define DEFAULT_ROWS 41
#define SCROLLBACK_LINES 800
#define SCROLLBACK_COLS 256
static int g_char_w = 8;
static int g_line_h = 10;
#define TAB_BAR_H 20
#define CONTENT_PAD_BOTTOM 20
#define TAB_CLOSE_W 12
#define TAB_CLOSE_PAD 6
#define MAX_TABS 4
#define TTY_READ_CHUNK 512
#define LINE_MAX 256

typedef struct {
    uint32_t c;
    uint32_t color;
} CharCell;

typedef struct {
    int tty_id;
    int bsh_pid;
    CharCell *cells;
    CharCell *scrollback;
    int *scroll_cols;
    int scroll_head;
    int scroll_count;
    int scroll_offset;
    int cursor_row;
    int cursor_col;
    uint32_t fg_color;
    uint32_t bg_color;
    uint32_t input_color;

    int ansi_state;
    int ansi_params[8];
    int ansi_param_count;
    int saved_row;
    int saved_col;

    bool colors_enabled;

    // UTF-8 decoding state
    uint32_t utf8_codepoint;
    int utf8_expected;
    int utf8_received;

    int unacknowledged_chars;

    // for color
    char current_input[LINE_MAX];
    int input_len;
    int input_start_col;
    int input_start_row;
} TerminalSession;

static ui_window_t g_win;
static TerminalSession g_tabs[MAX_TABS];
static int g_tab_count = 0;
static int g_active_tab = 0;
static int g_cols = DEFAULT_COLS;
static int g_rows = DEFAULT_ROWS;
static int g_win_w = DEFAULT_COLS * 8;
static int g_win_h = TAB_BAR_H + (DEFAULT_ROWS * 10);

static void update_font_metrics(void) {
    int h = (int)ui_get_font_height();
    if (h > 0) g_line_h = h;
    else g_line_h = 15;

    // For non-monospace fonts, we find the widest character to use as our grid cell width
    int max_w = 0;
    for (int i = 32; i < 127; i++) {
        char buf[2] = { (char)i, 0 };
        int w = (int)ui_get_string_width(buf);
        if (w > max_w) max_w = w;
    }
    
    if (max_w > 0) g_char_w = max_w;
    else g_char_w = 8;
}

static void str_copy(char *dst, const char *src, int max_len) {
    int i = 0;
    if (max_len <= 0) return;
    while (i < max_len - 1 && src && src[i]) {
        dst[i] = src[i];
        i++;
    }
    dst[i] = 0;
}

static void str_append(char *dst, const char *src, int max_len) {
    if (!dst || !src || max_len <= 0) return;
    int dlen = (int)strlen(dst);
    int i = 0;
    while (dlen + i < max_len - 1 && src[i]) {
        dst[dlen + i] = src[i];
        i++;
    }
    dst[dlen + i] = 0;
}

static void trim_end(char *s) {
    if (!s) return;
    int len = (int)strlen(s);
    while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || s[len - 1] == '\t')) {
        s[len - 1] = 0;
        len--;
    }
}

static void scrollback_init(TerminalSession *s) {
    s->scroll_head = 0;
    s->scroll_count = 0;
    s->scroll_offset = 0;
    if (s->scrollback) {
        for (int i = 0; i < SCROLLBACK_LINES * SCROLLBACK_COLS; i++) {
            s->scrollback[i].c = ' ';
            s->scrollback[i].color = s->fg_color;
        }
    }
    if (s->scroll_cols) {
        for (int i = 0; i < SCROLLBACK_LINES; i++) s->scroll_cols[i] = 0;
    }
}

static void scrollback_push_row(TerminalSession *s, const CharCell *row, int cols) {
    if (!s->scrollback || !s->scroll_cols) return;
    int idx = s->scroll_head;
    CharCell *dest = s->scrollback + (idx * SCROLLBACK_COLS);
    int copy_cols = cols;
    if (copy_cols > SCROLLBACK_COLS) copy_cols = SCROLLBACK_COLS;

    for (int i = 0; i < SCROLLBACK_COLS; i++) {
        dest[i].c = ' ';
        dest[i].color = s->fg_color;
    }
    for (int i = 0; i < copy_cols; i++) {
        dest[i] = row[i];
    }

    s->scroll_cols[idx] = copy_cols;
    s->scroll_head = (idx + 1) % SCROLLBACK_LINES;
    if (s->scroll_count < SCROLLBACK_LINES) {
        s->scroll_count++;
    } else if (s->scroll_offset > 0) {
        s->scroll_offset--;
    }

    if (s->scroll_offset > 0) {
        s->scroll_offset++;
    }
}

static int scrollback_max_offset(TerminalSession *s) {
    int total = s->scroll_count + g_rows;
    if (total <= g_rows) return 0;
    return total - g_rows;
}

static void session_adjust_scroll(TerminalSession *s, int delta) {
    if (!s) return;
    int max_offset = scrollback_max_offset(s);
    s->scroll_offset += delta;
    if (s->scroll_offset < 0) s->scroll_offset = 0;
    if (s->scroll_offset > max_offset) s->scroll_offset = max_offset;
}

static CharCell *scrollback_get_line(TerminalSession *s, int line_index, int *out_cols) {
    if (!s || line_index < 0 || line_index >= s->scroll_count) return NULL;
    int start = s->scroll_head - s->scroll_count;
    if (start < 0) start += SCROLLBACK_LINES;
    int idx = (start + line_index) % SCROLLBACK_LINES;
    if (out_cols) *out_cols = s->scroll_cols[idx];
    return s->scrollback + (idx * SCROLLBACK_COLS);
}

static uint32_t ansi_color_16(int idx) {
    static uint32_t base[16] = {
        0xFF000000, 0xFFAA0000, 0xFF00AA00, 0xFFAA5500,
        0xFF0000AA, 0xFFAA00AA, 0xFF00AAAA, 0xFFAAAAAA,
        0xFF555555, 0xFFFF5555, 0xFF55FF55, 0xFFFFFF55,
        0xFF5555FF, 0xFFFF55FF, 0xFF55FFFF, 0xFFFFFFFF
    };
    if (idx < 0) idx = 0;
    if (idx > 15) idx = 15;
    return base[idx];
}

static void session_reset_colors(TerminalSession *s) {
    s->fg_color = 0xFFFFFFFF;
    s->bg_color = 0xFF1E1E1E;
}

static void session_clear(TerminalSession *s) {
    for (int i = 0; i < g_cols * g_rows; i++) {
        s->cells[i].c = ' ';
        s->cells[i].color = s->fg_color;
    }
    s->cursor_row = 0;
    s->cursor_col = 0;
    s->scroll_offset = 0;
}

static void session_scroll(TerminalSession *s) {
    int row_bytes = g_cols * (int)sizeof(CharCell);
    if (g_rows > 0) {
        scrollback_push_row(s, s->cells, g_cols);
    }
    memmove(s->cells, s->cells + g_cols, row_bytes * (g_rows - 1));
    for (int i = 0; i < g_cols; i++) {
        int idx = (g_rows - 1) * g_cols + i;
        s->cells[idx].c = ' ';
        s->cells[idx].color = s->fg_color;
    }
    s->cursor_row = g_rows - 1;
    s->cursor_col = 0;
}

static void session_put_char(TerminalSession *s, uint32_t c) {
    if (c == '\n') {
        s->cursor_row++;
        s->cursor_col = 0;
        if (s->cursor_row >= g_rows) session_scroll(s);
        return;
    }
    if (c == '\r') {
        s->cursor_col = 0;
        return;
    }
    if (c == '\b') {
        if (s->cursor_col > 0) s->cursor_col--;
        if (s->unacknowledged_chars < 0) s->unacknowledged_chars++;
        int idx = s->cursor_row * g_cols + s->cursor_col;
        if (idx >= 0 && idx < g_cols * g_rows) {
            s->cells[idx].c = ' ';
            s->cells[idx].color = s->fg_color;
        }
        return;
    }
    if (c == '\t') {
        int next = ((s->cursor_col / 4) + 1) * 4;
        while (s->cursor_col < next) {
            session_put_char(s, ' ');
        }
        return;
    }

    if (c < 32) return;

    int idx = s->cursor_row * g_cols + s->cursor_col;
    if (idx >= 0 && idx < g_cols * g_rows) {
        s->cells[idx].c = c;
        s->cells[idx].color = s->fg_color;
    }
    s->cursor_col++;
    if (s->unacknowledged_chars > 0) s->unacknowledged_chars--;
    if (s->cursor_col >= g_cols) {
        s->cursor_col = 0;
        s->cursor_row++;
        if (s->cursor_row >= g_rows) session_scroll(s);
    }
}

static void ansi_handle_sgr(TerminalSession *s) {
    if (s->ansi_param_count == 0) {
        session_reset_colors(s);
        return;
    }
    for (int i = 0; i < s->ansi_param_count; i++) {
        int p = s->ansi_params[i];
        if (p == 0) session_reset_colors(s);
        else if (p >= 30 && p <= 37) s->fg_color = ansi_color_16(p - 30);
        else if (p >= 90 && p <= 97) s->fg_color = ansi_color_16(8 + (p - 90));
        else if (p >= 40 && p <= 47) s->bg_color = ansi_color_16(p - 40);
        else if (p >= 100 && p <= 107) s->bg_color = ansi_color_16(8 + (p - 100));
        else if (p == 38 || p == 48) {
            if (i + 4 < s->ansi_param_count && s->ansi_params[i + 1] == 2) {
                int r = s->ansi_params[i + 2] & 0xFF;
                int g = s->ansi_params[i + 3] & 0xFF;
                int b = s->ansi_params[i + 4] & 0xFF;
                uint32_t color = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
                if (p == 38) s->fg_color = color;
                else s->bg_color = color;
                i += 4;
            }
        }
    }
}

static void ansi_finalize(TerminalSession *s, uint32_t cmd) {
    if (cmd == 'm') {
        ansi_handle_sgr(s);
    } else if (cmd == 'J') {
        int mode = s->ansi_param_count > 0 ? s->ansi_params[0] : 0;
        if (mode == 2 || mode == 0) session_clear(s);
    } else if (cmd == 'K') {
        int row = s->cursor_row;
        for (int col = s->cursor_col; col < g_cols; col++) {
            int idx = row * g_cols + col;
            s->cells[idx].c = ' ';
            s->cells[idx].color = s->fg_color;
        }
    } else if (cmd == 'H' || cmd == 'f') {
        int row = (s->ansi_param_count > 0) ? s->ansi_params[0] : 1;
        int col = (s->ansi_param_count > 1) ? s->ansi_params[1] : 1;
        if (row < 1) row = 1;
        if (col < 1) col = 1;
        s->cursor_row = row - 1;
        s->cursor_col = col - 1;
    } else if (cmd == 'A') {
        int n = (s->ansi_param_count > 0) ? s->ansi_params[0] : 1;
        s->cursor_row -= n;
        if (s->cursor_row < 0) s->cursor_row = 0;
    } else if (cmd == 'B') {
        int n = (s->ansi_param_count > 0) ? s->ansi_params[0] : 1;
        s->cursor_row += n;
        if (s->cursor_row >= g_rows) s->cursor_row = g_rows - 1;
    } else if (cmd == 'C') {
        int n = (s->ansi_param_count > 0) ? s->ansi_params[0] : 1;
        s->cursor_col += n;
        if (s->cursor_col >= g_cols) s->cursor_col = g_cols - 1;
    } else if (cmd == 'D') {
        int n = (s->ansi_param_count > 0) ? s->ansi_params[0] : 1;
        s->cursor_col -= n;
        if (s->cursor_col < 0) s->cursor_col = 0;
    } else if (cmd == 's') {
        s->saved_row = s->cursor_row;
        s->saved_col = s->cursor_col;
    } else if (cmd == 'u') {
        s->cursor_row = s->saved_row;
        s->cursor_col = s->saved_col;
    }

    s->ansi_state = 0;
    s->ansi_param_count = 0;
}

static void session_process_char(TerminalSession *s, char c) {
    uint8_t b = (uint8_t)c;
    if (s->utf8_expected == 0) {
        if (b < 128) {
            s->utf8_codepoint = b;
            s->utf8_expected = 0;
        } else if ((b & 0xE0) == 0xC0) {
            s->utf8_codepoint = b & 0x1F;
            s->utf8_expected = 1;
            s->utf8_received = 1;
            return;
        } else if ((b & 0xF0) == 0xE0) {
            s->utf8_codepoint = b & 0x0F;
            s->utf8_expected = 2;
            s->utf8_received = 1;
            return;
        } else if ((b & 0xF8) == 0xF0) {
            s->utf8_codepoint = b & 0x07;
            s->utf8_expected = 3;
            s->utf8_received = 1;
            return;
        } else {
            return;
        }
    } else {
        if ((b & 0xC0) == 0x80) {
            s->utf8_codepoint = (s->utf8_codepoint << 6) | (b & 0x3F);
            s->utf8_received++;
            if (s->utf8_received <= s->utf8_expected) {
                return;
            }
        } else {
            s->utf8_expected = 0;
            return;
        }
    }

    uint32_t cp = s->utf8_codepoint;
    s->utf8_expected = 0;

    if (s->ansi_state == 0) {
        if (cp == 27) {
            s->ansi_state = 1;
            s->ansi_param_count = 0;
            s->ansi_params[0] = 0;
            return;
        }
        session_put_char(s, cp);
        return;
    }

    if (s->ansi_state == 1) {
        if (cp == '[') {
            s->ansi_state = 2;
            s->ansi_param_count = 0;
            s->ansi_params[0] = 0;
            return;
        }
        s->ansi_state = 0;
        session_put_char(s, cp);
        return;
    }

    if (s->ansi_state == 2) {
        if (cp >= '0' && cp <= '9') {
            int idx = s->ansi_param_count;
            s->ansi_params[idx] = s->ansi_params[idx] * 10 + (cp - '0');
            return;
        }
        if (cp == ';') {
            if (s->ansi_param_count < 7) {
                s->ansi_param_count++;
                s->ansi_params[s->ansi_param_count] = 0;
            }
            return;
        }
        s->ansi_param_count++;
        ansi_finalize(s, cp);
        return;
    }
}

static void session_process_output(TerminalSession *s, const char *buf, int len) {
    for (int i = 0; i < len; i++) {
        session_process_char(s, buf[i]);
    }
}

static int get_tab_width(void) {
    if (g_tab_count <= 0) return g_win_w;
    int tab_w = g_win_w / g_tab_count;
    if (tab_w < 60) tab_w = 60;
    return tab_w;
}

static int read_proc_field(int pid, const char *field, char *out, int max_len) {
    if (!out || max_len <= 0) return -1;
    char path[64];
    path[0] = 0;
    str_append(path, "/proc/", sizeof(path));
    char pid_buf[16];
    itoa(pid, pid_buf);
    str_append(path, pid_buf, sizeof(path));
    str_append(path, "/", sizeof(path));
    str_append(path, field, sizeof(path));

    int fd = sys_open(path, "r");
    if (fd < 0) return -1;
    int bytes = sys_read(fd, out, max_len - 1);
    sys_close(fd);
    if (bytes <= 0) return -1;
    out[bytes] = 0;
    trim_end(out);
    return 0;
}

static void truncate_label(const char *src, char *out, int max_chars) {
    if (!out || max_chars <= 0) return;
    int len = (int)strlen(src);
    if (len <= max_chars) {
        str_copy(out, src, max_chars + 1);
        return;
    }
    if (max_chars <= 3) {
        for (int i = 0; i < max_chars; i++) out[i] = src[i];
        out[max_chars] = 0;
        return;
    }
    for (int i = 0; i < max_chars - 3; i++) out[i] = src[i];
    out[max_chars - 3] = '.';
    out[max_chars - 2] = '.';
    out[max_chars - 1] = '.';
    out[max_chars] = 0;
}

static void get_tab_title(TerminalSession *s, char *out, int max_len) {
    if (!s || !out) return;
    int fg = sys_tty_get_fg(s->tty_id);
    if (fg > 0) {
        if (read_proc_field(fg, "name", out, max_len) == 0) return;
    }
    if (s->bsh_pid > 0) {
        if (read_proc_field(s->bsh_pid, "cwd", out, max_len) == 0) return;
    }
    str_copy(out, "Bsh", max_len);
}

static int read_config_value(const char *key, char *out, int max_len) {
    if (!key || !out || max_len <= 0) return -1;

    int fd = sys_open("/Library/bsh/bshrc", "r");
    if (fd < 0) return -1;

    char buf[4096];
    int bytes = sys_read(fd, buf, sizeof(buf) - 1);
    sys_close(fd);

    if (bytes <= 0) return -1;
    buf[bytes] = 0;

    char *line = buf;

    while (*line) {
        char *end = line;
        while (*end && *end != '\n' && *end != '\r') end++;

        char saved = *end;
        *end = 0;

        trim_end(line);

        if (line[0] != '#' && line[0] != 0) {
            char *sep = line;

            while (*sep && *sep != '=') sep++;

            if (*sep == '=') {
                *sep = 0;

                char *k = line;

                // skip leading spaces
                while (*k == ' ' || *k == '\t') k++;

                // skip "export "
                if (strncmp(k, "export ", 7) == 0) {
                    k += 7;
                }

                // skip spaces again after export
                while (*k == ' ' || *k == '\t') k++;

                // trim end of key
                char *kend = k + strlen(k) - 1;
                while (kend > k && (*kend == ' ' || *kend == '\t')) {
                    *kend = 0;
                    kend--;
                }

                if (strcmp(k, key) == 0) {
                    char *val = sep + 1;

                    // skip leading spaces
                    while (*val == ' ' || *val == '\t') val++;

                    // remove "
                    if (*val == '"') {
                        val++;
                        char *vend = strchr(val, '"');
                        if (vend) *vend = 0;
                    }

                    str_copy(out, val, max_len);
                    return 0;
                }
            }
        }

        line = end + (saved ? 1 : 0);
        if (saved == '\r' && *line == '\n') line++;
    }

    return -1;
}

static void resolve_bsh_path(char *out, int max_len) {
    char path_line[256];
    if (read_config_value("PATH", path_line, sizeof(path_line)) != 0) {
        str_copy(path_line, "/bin", sizeof(path_line));
    }

    int i = 0;
    int start = 0;
    while (1) {
        if (path_line[i] == ':' || path_line[i] == 0) {
            int len = i - start;
            if (len > 0) {
                char base[128];
                if (len >= (int)sizeof(base)) len = (int)sizeof(base) - 1;
                for (int j = 0; j < len; j++) base[j] = path_line[start + j];
                base[len] = 0;

                char candidate[160];
                candidate[0] = 0;
                str_append(candidate, base, sizeof(candidate));
                if (candidate[0] && candidate[strlen(candidate) - 1] != '/') str_append(candidate, "/", sizeof(candidate));
                str_append(candidate, "bsh.elf", sizeof(candidate));
                if (sys_exists(candidate)) {
                    str_copy(out, candidate, max_len);
                    return;
                }

                candidate[0] = 0;
                str_append(candidate, base, sizeof(candidate));
                if (candidate[0] && candidate[strlen(candidate) - 1] != '/') str_append(candidate, "/", sizeof(candidate));
                str_append(candidate, "bsh", sizeof(candidate));
                if (sys_exists(candidate)) {
                    str_copy(out, candidate, max_len);
                    return;
                }
            }
            start = i + 1;
        }
        if (path_line[i] == 0) break;
        i++;
    }

    str_copy(out, "/bin/bsh.elf", max_len);
}

static bool has_space(const char *s) {
    if (!s) return false;
    while (*s) {
        if (*s == ' ') return true;
        s++;
    }
    return false;
}

static void terminal_resize(int w, int h) {
    int min_w = g_char_w * 40;
    int min_h = TAB_BAR_H + (g_line_h * 10);
    if (w < min_w) w = min_w;
    if (h < min_h) h = min_h;

    g_win_w = w;
    g_win_h = h;

    int new_cols = w / g_char_w;
    int content_h = h - TAB_BAR_H - CONTENT_PAD_BOTTOM;
    if (g_line_h <= 0) g_line_h = 15;
    if (content_h < g_line_h) content_h = g_line_h;
    int new_rows = content_h / g_line_h;
    if (new_cols < 10) new_cols = 10;
    if (new_rows < 5) new_rows = 5;

    if (new_cols == g_cols && new_rows == g_rows) return;

    int old_cols = g_cols;
    int old_rows = g_rows;
    g_cols = new_cols;
    g_rows = new_rows;

    for (int i = 0; i < g_tab_count; i++) {
        TerminalSession *s = &g_tabs[i];
        int old_scroll_count = s->scroll_count;
        int old_scroll_offset = s->scroll_offset;
        int old_total_lines = old_scroll_count + old_rows;
        int old_bottom_line = old_total_lines - 1 - old_scroll_offset;
        int old_top_line = old_bottom_line - (old_rows - 1);
        CharCell *old_cells = s->cells;
        int old_cursor_row = s->cursor_row;
        int old_cursor_col = s->cursor_col;

        s->cells = (CharCell *)malloc(sizeof(CharCell) * g_cols * g_rows);
        for (int j = 0; j < g_cols * g_rows; j++) {
            s->cells[j].c = ' ';
            s->cells[j].color = s->fg_color;
        }

        int row_start = 0;
        if (old_rows > g_rows) {
            if (old_cursor_row >= g_rows) {
                row_start = old_cursor_row - (g_rows - 1);
            } else {
                row_start = 0;
            }
            int max_start = old_rows - g_rows;
            if (row_start > max_start) row_start = max_start;
            if (row_start < 0) row_start = 0;
        }
        int dropped = 0;
        if (row_start > 0) {
            int projected = old_scroll_count + row_start;
            if (projected > SCROLLBACK_LINES) dropped = projected - SCROLLBACK_LINES;
        }
        int desired_top_line = old_top_line - dropped;
        if (desired_top_line < 0) desired_top_line = 0;
        for (int r = 0; r < row_start; r++) {
            scrollback_push_row(s, old_cells + (r * old_cols), old_cols);
        }

        int copy_rows = old_rows;
        if (copy_rows > g_rows) copy_rows = g_rows;
        int copy_cols = old_cols < g_cols ? old_cols : g_cols;
        for (int r = 0; r < copy_rows; r++) {
            CharCell *src = old_cells + ((row_start + r) * old_cols);
            CharCell *dst = s->cells + (r * g_cols);
            for (int c = 0; c < copy_cols; c++) {
                dst[c] = src[c];
            }
        }

        s->cursor_row = old_cursor_row - row_start;
        if (old_rows <= g_rows) s->cursor_row = old_cursor_row;
        if (s->cursor_row < 0) s->cursor_row = 0;
        if (s->cursor_row >= g_rows) s->cursor_row = g_rows - 1;

        s->cursor_col = old_cursor_col;
        if (s->cursor_col < 0) s->cursor_col = 0;
        if (s->cursor_col >= g_cols) s->cursor_col = g_cols - 1;

        if (old_scroll_offset == 0) {
            s->scroll_offset = 0;
        } else {
            int new_total_lines = s->scroll_count + g_rows;
            int desired_bottom = desired_top_line + (g_rows - 1);
            s->scroll_offset = (new_total_lines - 1) - desired_bottom;
        }
        session_adjust_scroll(s, 0);

        if (old_cells) free(old_cells);
    }
}

static void draw_tabs(void) {
    if (g_tab_count <= 0) return;
    ui_draw_rect(g_win, 0, 0, g_win_w, TAB_BAR_H, 0xFF1A1A1A);
    int tab_w = get_tab_width();
    for (int i = 0; i < g_tab_count; i++) {
        int x = i * tab_w;
        uint32_t bg = (i == g_active_tab) ? 0xFF333333 : 0xFF222222;
        ui_draw_rect(g_win, x, 0, tab_w - 2, TAB_BAR_H, bg);
        int close_size = TAB_CLOSE_W;
        int close_x = x + tab_w - TAB_CLOSE_PAD - close_size;
        int close_y = (TAB_BAR_H - close_size) / 2;
        uint32_t close_bg = (i == g_active_tab) ? 0xFF444444 : 0xFF333333;
        ui_draw_rect(g_win, close_x, close_y, close_size, close_size, close_bg);
        ui_draw_string(g_win, close_x + 2, close_y + 1, "x", 0xFFFFFFFF);
        char title[64];
        char label[64];
        get_tab_title(&g_tabs[i], title, sizeof(title));
        int text_w = tab_w - 10 - (TAB_CLOSE_PAD + TAB_CLOSE_W);
        int max_chars = text_w / g_char_w;
        if (max_chars < 4) max_chars = 4;
        truncate_label(title, label, max_chars);
        ui_draw_string(g_win, x + 6, 4, label, 0xFFFFFFFF);
    }
}

static void draw_session(TerminalSession *s) {
    int base_y = TAB_BAR_H;
    ui_draw_rect(g_win, 0, base_y, g_win_w, g_win_h - base_y, s->bg_color);

    int max_offset = scrollback_max_offset(s);
    if (s->scroll_offset > max_offset) s->scroll_offset = max_offset;
    int total_lines = s->scroll_count + g_rows;
    int bottom_line = total_lines - 1 - s->scroll_offset;
    int top_line = bottom_line - (g_rows - 1);
    int input_char_len = text_strlen_utf8(s->current_input);
    int visible_len = input_char_len - s->unacknowledged_chars;
    if (visible_len < 0) visible_len = 0;

    for (int row = 0; row < g_rows; row++) {
        int line_index = top_line + row;
        CharCell *line = NULL;
        int line_cols = 0;
        if (line_index >= 0 && line_index < s->scroll_count) {
            line = scrollback_get_line(s, line_index, &line_cols);
        } else if (line_index >= s->scroll_count && line_index < total_lines) {
            int live_row = line_index - s->scroll_count;
            if (live_row >= 0 && live_row < g_rows) {
                line = s->cells + (live_row * g_cols);
                line_cols = g_cols;
            }
        }
        
        int input_start = -1;
        if (s->input_len > 0 && row == s->input_start_row) {
            input_start = s->input_start_col;
        }

        char cmd[64];
        int i = 0;
        while (i < s->input_len && s->current_input[i] != ' ' && s->current_input[i] != '\t' && i < 63) {
            cmd[i] = s->current_input[i];
            i++;
        }
        cmd[i] = 0;
        int cmd_char_len = text_strlen_utf8(cmd);

        int input_end = input_start + cmd_char_len;
        if (input_end > g_cols) input_end = g_cols;

        for (int col = 0; col < g_cols; col++) {
            uint32_t ch = ' ';
            uint32_t color = s->fg_color;

            if (line && col < line_cols) {
                ch = line[col].c;
                if (ch == 0) ch = ' ';
                color = line[col].color;
            }

            if (s->scroll_offset == 0 && row == s->input_start_row && input_start >= 0) {
                if (col >= input_start && col < input_end) {
                    color = s->input_color;
                }
            }

            char out[5];
            int len = text_encode_utf8(ch, out);
            out[len] = 0;
            
            int x = col * g_char_w;
            int y = base_y + row * g_line_h;
            ui_draw_string(g_win, x, y, out, color);
        }
    }

    if (s->scroll_offset == 0) {
        int cx = s->cursor_col * g_char_w;
        int cy = base_y + s->cursor_row * g_line_h;
        ui_draw_rect(g_win, cx, cy + g_line_h - 2, g_char_w, 2, 0xFFFFFFFF);
    }

    ui_mark_dirty(g_win, 0, 0, g_win_w, g_win_h);
}

static void tab_init(TerminalSession *s, int tty_id, int bsh_pid) {
    s->tty_id = tty_id;
    s->bsh_pid = bsh_pid;
    s->cells = (CharCell *)malloc(sizeof(CharCell) * g_cols * g_rows);
    s->scrollback = (CharCell *)malloc(sizeof(CharCell) * SCROLLBACK_LINES * SCROLLBACK_COLS);
    s->scroll_cols = (int *)malloc(sizeof(int) * SCROLLBACK_LINES);
    s->cursor_row = 0;
    s->cursor_col = 0;
    s->ansi_state = 0;
    s->ansi_param_count = 0;
    s->saved_row = 0;
    s->saved_col = 0;

    s->input_len = 0;
    s->input_start_col = 0;
    s->input_start_row = 0;
    s->current_input[0] = 0;
    s->utf8_codepoint = 0;
    s->utf8_expected = 0;
    s->utf8_received = 0;
    s->unacknowledged_chars = 0;
    session_reset_colors(s);
    scrollback_init(s);
    
    char value[64];

    if (read_config_value("TERMINAL_COLOR", value, sizeof(value)) == 0) {
        if (strcmp(value, "1") == 0 || strcmp(value, "true") == 0) {
            s->colors_enabled = true;
        } else {
            s->colors_enabled = false;
        }
    } else {
        s->colors_enabled = false;
    }
        
    session_clear(s);
}

static void tab_free(TerminalSession *s) {
    if (!s) return;
    if (s->cells) {
        free(s->cells);
        s->cells = NULL;
    }
    if (s->scrollback) {
        free(s->scrollback);
        s->scrollback = NULL;
    }
    if (s->scroll_cols) {
        free(s->scroll_cols);
        s->scroll_cols = NULL;
    }
}

static void close_tab(int idx) {
    if (idx < 0 || idx >= g_tab_count) return;
    TerminalSession *s = &g_tabs[idx];

    sys_tty_kill_all(s->tty_id);
    sys_tty_destroy(s->tty_id);

    tab_free(s);

    for (int i = idx; i < g_tab_count - 1; i++) {
        g_tabs[i] = g_tabs[i + 1];
    }
    g_tab_count--;
    if (g_tab_count <= 0) {
        sys_exit(0);
    }
    if (g_active_tab > idx) {
        g_active_tab--;
    } else if (g_active_tab == idx) {
        if (g_active_tab >= g_tab_count) g_active_tab = g_tab_count - 1;
    }
}

static int create_tab(void) {
    if (g_tab_count >= MAX_TABS) return -1;

    int tty_id = sys_tty_create();
    if (tty_id < 0) return -1;

    char start_dir[256];
    start_dir[0] = 0;
    if (g_tab_count > 0) {
        TerminalSession *active = &g_tabs[g_active_tab];
        if (active->bsh_pid > 0) {
            read_proc_field(active->bsh_pid, "cwd", start_dir, sizeof(start_dir));
        }
    }

    char args[32];
    args[0] = 0;
    str_append(args, "-t ", sizeof(args));
    char id_buf[8];
    itoa(tty_id, id_buf);
    str_append(args, id_buf, sizeof(args));

    if (start_dir[0]) {
        str_append(args, " -d ", sizeof(args));
        if (has_space(start_dir)) {
            str_append(args, "\"", sizeof(args));
            str_append(args, start_dir, sizeof(args));
            str_append(args, "\"", sizeof(args));
        } else {
            str_append(args, start_dir, sizeof(args));
        }
    }

    char bsh_path[128];
    resolve_bsh_path(bsh_path, sizeof(bsh_path));
    int pid = sys_spawn(bsh_path, args, SPAWN_FLAG_TERMINAL | SPAWN_FLAG_TTY_ID, tty_id);
    if (pid < 0) return -1;

    tab_init(&g_tabs[g_tab_count], tty_id, pid);
    g_tab_count++;
    return g_tab_count - 1;
}

static int parse_path(char paths[][128], int max_paths) {
    char path_line[256];

    if (read_config_value("PATH", path_line, sizeof(path_line)) != 0) {
        str_copy(path_line, "/bin", sizeof(path_line));
    }

    if (path_line[0] == '"') {
        memmove(path_line, path_line + 1, strlen(path_line));
        char *end = strchr(path_line, '"');
        if (end) *end = 0;
    }

    int count = 0;
    int start = 0;

    for (int i = 0;; i++) {
        if (path_line[i] == ':' || path_line[i] == ';' || path_line[i] == 0) {
            int len = i - start;

            if (len > 0 && count < max_paths) {
                if (len >= 128) len = 127;

                memcpy(paths[count], &path_line[start], len);
                paths[count][len] = 0;
                count++;
            }

            start = i + 1;
        }

        if (path_line[i] == 0) break;
    }

    return count;
}

static bool command_exists(const char *cmd) {
    if (!cmd || !cmd[0]) return false;

    char paths[16][128];
    int path_count = parse_path(paths, 16);

    char full[256];

    for (int i = 0; i < path_count; i++) {
        // folder/cmd
        full[0] = 0;
        str_append(full, paths[i], sizeof(full));
        if (full[strlen(full) - 1] != '/') str_append(full, "/", sizeof(full));
        str_append(full, cmd, sizeof(full));

        if (sys_exists(full)) return true;

        // folder/cmd.elf
        full[0] = 0;
        str_append(full, paths[i], sizeof(full));
        if (full[strlen(full) - 1] != '/') str_append(full, "/", sizeof(full));
        str_append(full, cmd, sizeof(full));
        str_append(full, ".elf", sizeof(full));

        if (sys_exists(full)) return true;
    }

    return false;
}

static bool command_starts_with(const char *prefix) {
    if (!prefix || !prefix[0]) return false;

    char paths[16][128];
    int path_count = parse_path(paths, 16);

    FAT32_FileInfo entries[128];

    for (int p = 0; p < path_count; p++) {
        int count = sys_list(paths[p], entries, 128);
        if (count <= 0) continue;

        for (int i = 0; i < count; i++) {
            if (entries[i].is_directory) continue;

            char name[256];
            str_copy(name, entries[i].name, sizeof(name));

            int len = strlen(name);

            // remove .elf
            if (len > 4 && strcmp(name + len - 4, ".elf") == 0) {
                name[len - 4] = 0;
            }

            int j = 0;
            while (prefix[j] && name[j] && prefix[j] == name[j]) {
                j++;
            }

            if (prefix[j] == 0) {
                return true;
            }
        }
    }

    return false;
}

static void update_input_color(TerminalSession *s) {
    if (!s->colors_enabled) {
        s->input_color = s->fg_color;
        return;
    }
    
    if (s->input_len == 0) {
        s->input_color = 0xFFFFFFFF;
        return;
    }

    char cmd[64];
    int i = 0;

    while (i < s->input_len &&
        s->current_input[i] != ' ' &&
        s->current_input[i] != '\t' &&
        i < 63) {
        cmd[i] = s->current_input[i];
        i++;
    }
    cmd[i] = 0;

    if (command_exists(cmd)) {
        s->input_color = 0xFF55FF55; // green
    } else if (command_starts_with(cmd)) {
        s->input_color = 0xFFFFFF55; // yellow
    } else {
        s->input_color = 0xFFFF5555; // red
    }
}

static void handle_key(gui_event_t *ev) {
    TerminalSession *s = &g_tabs[g_active_tab];
    int legacy = ev->arg1;
    bool ctrl = ev->arg3 != 0;
    uint32_t codepoint = (uint32_t)ev->arg4;

    if (ctrl && (legacy == 'c' || legacy == 'C')) {
        int fg = sys_tty_get_fg(s->tty_id);
        if (fg > 0) {
            sys_tty_kill_fg(s->tty_id);
            sys_tty_set_fg(s->tty_id, 0);
            return;
        }
        s->input_len = 0;
        char ch = 3;
        sys_tty_write_in(s->tty_id, &ch, 1);
        return;
    }

    // create new tab with ctrl + t
    if (ctrl && (legacy == 't' || legacy == 'T')) {
        int idx = create_tab();
        if (idx >= 0) g_active_tab = idx;
        return;
    }

    // switch to tab right, with ctrl + arrow right
    if (ctrl && legacy == KEY_RIGHT) {
        if (g_tab_count > 0) g_active_tab = (g_active_tab + 1) % g_tab_count;
        return;
    }

    // switch to tab left, with ctrl + arrow left
    if (ctrl && legacy == KEY_LEFT) {
        if (g_tab_count > 0) g_active_tab = (g_active_tab + g_tab_count - 1) % g_tab_count;
        return;
    }

    if (legacy == KEY_LEFT) {
        char seq[] = { 27, '[', 'D' };
        sys_tty_write_in(s->tty_id, seq, 3);
        return;
    } else if (legacy == KEY_RIGHT) {
        char seq[] = { 27, '[', 'C' };
        sys_tty_write_in(s->tty_id, seq, 3);
        return;
    } else if (legacy == KEY_UP) {
        char seq[] = { 27, '[', 'A' };
        sys_tty_write_in(s->tty_id, seq, 3);
        return;
    } else if (legacy == KEY_DOWN) {
        char seq[] = { 27, '[', 'B' };
        sys_tty_write_in(s->tty_id, seq, 3);
        return;
    }

    if (!ctrl) {
        if (legacy == KEY_BACKSPACE) {
            if (s->input_len > 0) {
                const char *prev = text_prev_utf8(s->current_input, s->current_input + s->input_len);
                s->input_len = (int)(prev - s->current_input);
                s->current_input[s->input_len] = 0;
                s->unacknowledged_chars--;
            }
        } else if (codepoint >= 32 && codepoint != 127) {
            char utf8[4];
            int len = text_encode_utf8(codepoint, utf8);
            if (len > 0 && s->input_len + len < LINE_MAX - 1) {
                if (s->input_len == 0) {
                    s->input_start_col = s->cursor_col;
                    s->input_start_row = s->cursor_row;
                }
                for (int i = 0; i < len; i++) {
                    s->current_input[s->input_len + i] = utf8[i];
                }
                s->input_len += len;
                s->current_input[s->input_len] = 0;
                s->unacknowledged_chars++;
            }
        } else if (legacy == KEY_ENTER) {
            s->input_color = 0xFFFFFFFF;
            s->input_len = 0;
            s->current_input[0] = 0;
            s->unacknowledged_chars = 0;
        }

        update_input_color(s);
    }

    if (codepoint >= 32 && codepoint != 127) {
        char utf8[4];
        int len = text_encode_utf8(codepoint, utf8);
        if (len > 0) {
            sys_tty_write_in(s->tty_id, utf8, len);
        }
    } else {
        char c = (char)legacy;
        sys_tty_write_in(s->tty_id, &c, 1);
    }
}

int main(void) {
    g_win = ui_window_create("Terminal", 60, 60, g_win_w, g_win_h);
    ui_window_set_resizable(g_win, true);
    
    char font_path[128];
    if (read_config_value("TERMINAL_FONT", font_path, sizeof(font_path)) == 0) {
        ui_set_font(g_win, font_path);
    } else {
        ui_set_font(g_win, "/Library/Fonts/JetBrainsMono-Regular.ttf");
    }
    update_font_metrics();

    terminal_resize(g_win_w, g_win_h);

    int idx = create_tab();
    if (idx < 0) return 1;
    g_active_tab = idx;

    gui_event_t ev;
    char out_buf[TTY_READ_CHUNK];

    while (1) {
        bool dirty = false;

        for (int i = 0; i < g_tab_count; i++) {
            TerminalSession *s = &g_tabs[i];
            int read = 0;
            while ((read = sys_tty_read_out(s->tty_id, out_buf, sizeof(out_buf))) > 0) {
                session_process_output(s, out_buf, read);
                if (i == g_active_tab) dirty = true;
            }
        }

        while (ui_get_event(g_win, &ev)) {
            if (ev.type == GUI_EVENT_CLOSE) {
                for (int i = 0; i < g_tab_count; i++) {
                    if (g_tabs[i].bsh_pid > 0) sys_kill(g_tabs[i].bsh_pid);
                }
                sys_exit(0);
            } else if (ev.type == GUI_EVENT_KEY) {
                handle_key(&ev);
                dirty = true;
            } else if (ev.type == GUI_EVENT_CLICK) {
                if (ev.arg2 < TAB_BAR_H) {
                    int tab_w = get_tab_width();
                    int tab = ev.arg1 / tab_w;
                    if (tab >= 0 && tab < g_tab_count) {
                        int close_size = TAB_CLOSE_W;
                        int close_x = tab * tab_w + tab_w - TAB_CLOSE_PAD - close_size;
                        int close_y = (TAB_BAR_H - close_size) / 2;
                        if (ev.arg1 >= close_x && ev.arg1 < close_x + close_size &&
                            ev.arg2 >= close_y && ev.arg2 < close_y + close_size) {
                            close_tab(tab);
                        } else {
                            g_active_tab = tab;
                        }
                        dirty = true;
                    }
                }
            } else if (ev.type == GUI_EVENT_MOUSE_WHEEL) {
                int lines = ev.arg1 * 3;
                if (lines != 0) {
                    session_adjust_scroll(&g_tabs[g_active_tab], lines);
                    dirty = true;
                }
            } else if (ev.type == GUI_EVENT_RESIZE) {
                terminal_resize(ev.arg1, ev.arg2);
                dirty = true;
            } else if (ev.type == GUI_EVENT_PAINT) {
                dirty = true;
            }
        }

        if (dirty) {
            draw_tabs();
            draw_session(&g_tabs[g_active_tab]);
        } else {
            sys_yield();
        }
    }

    return 0;
}
