/*
    * main.cpp
    * MontaukOS Wikipedia GUI client - standalone Window Server process
    * Fetches articles via TLS (BearSSL), renders with Roboto TTF
    * Copyright (c) 2026 Daniel Hammer
*/

#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/mtk.hpp>
#include <gui/standalone.hpp>
#include <gui/svg.hpp>
#include <gui/truetype.hpp>
#include <tls/tls.hpp>

extern "C" {
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
}

using namespace gui;
using namespace gui::colors;

// ============================================================================
// Constants
// ============================================================================

static constexpr int INIT_W       = 820;
static constexpr int INIT_H       = 580;
static int           TOOLBAR_H    = 42;
static constexpr int SCROLLBAR_W  = 14;
static int           FONT_SIZE    = 18;
static int           TITLE_SIZE   = 32;
static int           SECTION_SIZE = 24;
static constexpr int TEXT_PAD     = 16;
static constexpr int RESP_MAX     = 131072;
static constexpr int MAX_LINES    = 2000;

static const char WIKI_HOST[] = "en.wikipedia.org";
static const char DISPLAY_ICON_PATH[] = "0:/icons/dialog-text-and-font.svg";

// ============================================================================
// Display line
// ============================================================================

struct WikiLine {
    char         text[256];
    Color        color;
    int          font_size;
    TrueTypeFont* font;   // which font to render with
    int          indent;
};

// ============================================================================
// App state
// ============================================================================

enum class AppPhase { IDLE, LOADING, DONE, ERR };
enum class ReaderTextSize : uint8_t { SMALL = 0, MEDIUM, LARGE, COUNT };
enum class ReaderFontFace : uint8_t { SANS = 0, SERIF, BOOK, COUNT };

struct ReaderPalette {
    Color page_bg;
    Color control_bg;
    Color toolbar_bg;
    Color border;
    Color text;
    Color heading;
    Color muted;
    Color accent;
    Color accent_hover;
    Color accent_soft;
    Color danger;
    Color danger_hover;
    Color disabled_bg;
    Color disabled_fg;
};

static constexpr int READER_TEXT_SIZE_COUNT = (int)ReaderTextSize::COUNT;
static constexpr int READER_FONT_FACE_COUNT = (int)ReaderFontFace::COUNT;

static AppPhase      g_phase      = AppPhase::IDLE;
static char          g_query[256] = {};
static mtk::TextInputState g_query_input = {};
static char          g_status[256] = {};
static int           g_scroll_y   = 0;
static int           g_line_count = 0;
static int           g_line_h     = 24;   // updated after font load
static int           g_win_w      = INIT_W;
static int           g_win_h      = INIT_H;
static int           g_mouse_x    = -1;
static int           g_mouse_y    = -1;
static bool          g_search_focused = true;
static bool          g_scroll_dragging = false;
static int           g_scroll_drag_offset = 0;
static bool          g_display_controls_open = false;
static ReaderTextSize g_reader_text_size = ReaderTextSize::MEDIUM;
static ReaderFontFace g_reader_font_face = ReaderFontFace::SANS;
static char          g_title[512] = {};
static int           g_extract_len = 0;
static int           g_base_font_size = 18;
static int           g_base_title_size = 32;
static int           g_base_section_size = 24;

// Large buffers — heap allocated in _start
static WikiLine*     g_lines      = nullptr;
static char*         g_resp_buf   = nullptr;
static char*         g_extract_buf = nullptr;

// Fonts
static TrueTypeFont* g_font       = nullptr;  // Roboto Medium
static TrueTypeFont* g_font_serif = nullptr;  // NotoSerif SemiBold (headings)
static TrueTypeFont* g_body_fonts[READER_FONT_FACE_COUNT] = {};
static TrueTypeFont* g_heading_fonts[READER_FONT_FACE_COUNT] = {};
static SvgIcon       g_display_icon = {};
static SvgIcon       g_display_icon_active = {};

// TLS state (lazy-init on first search)
static bool          g_tls_ready  = false;
static uint32_t      g_server_ip  = 0;
static tls::TrustAnchors g_tas = {nullptr, 0, 0};

static const char* g_text_size_labels[READER_TEXT_SIZE_COUNT] = {
    "A-", "A", "A+"
};

static const char* g_font_face_labels[READER_FONT_FACE_COUNT] = {
    "Sans", "Serif", "Book"
};

// ============================================================================
// UI scale
// ============================================================================

struct ReaderToolbarLayout {
    Rect search_field;
    Rect display_button;
    Rect search_button;
    Rect text_size_buttons[READER_TEXT_SIZE_COUNT];
    Rect font_buttons[READER_FONT_FACE_COUNT];
    int sep_x;
};

static void apply_reader_preferences();
static void update_toolbar_height();
static void reload_display_icons();

static void apply_scale(int scale) {
    switch (scale) {
    case 0:
        g_base_font_size = 14;
        g_base_title_size = 26;
        g_base_section_size = 20;
        break;
    case 2:
        g_base_font_size = 22;
        g_base_title_size = 40;
        g_base_section_size = 30;
        break;
    default:
        g_base_font_size = 18;
        g_base_title_size = 32;
        g_base_section_size = 24;
        break;
    }
    apply_reader_preferences();
    update_toolbar_height();
    reload_display_icons();
}

// ============================================================================
// Theme and layout
// ============================================================================

static ReaderPalette reader_palette() {
    return {
        WHITE,
        WHITE,
        Color::from_rgb(0xF6, 0xF7, 0xF9),
        Color::from_rgb(0xD5, 0xDB, 0xE3),
        Color::from_rgb(0x20, 0x23, 0x29),
        Color::from_rgb(0x11, 0x14, 0x18),
        Color::from_rgb(0x7A, 0x84, 0x92),
        Color::from_rgb(0x33, 0x66, 0xCC),
        Color::from_rgb(0x24, 0x52, 0xAF),
        Color::from_rgb(0xE5, 0xEE, 0xFF),
        Color::from_rgb(0xC8, 0x32, 0x32),
        Color::from_rgb(0xA9, 0x24, 0x24),
        Color::from_rgb(0xE7, 0xEB, 0xF0),
        Color::from_rgb(0x97, 0x9E, 0xA8),
    };
}

static TrueTypeFont* current_body_font() {
    return g_font ? g_font : g_body_fonts[(int)ReaderFontFace::SANS];
}

static TrueTypeFont* current_heading_font() {
    return g_font_serif ? g_font_serif : current_body_font();
}

static void apply_reader_preferences() {
    static const int body_adjust[READER_TEXT_SIZE_COUNT] = {-2, 0, 4};
    static const int title_adjust[READER_TEXT_SIZE_COUNT] = {-4, 0, 6};
    static const int section_adjust[READER_TEXT_SIZE_COUNT] = {-3, 0, 5};

    int font_index = (int)g_reader_font_face;
    g_font = g_body_fonts[font_index] ? g_body_fonts[font_index]
                                      : g_body_fonts[(int)ReaderFontFace::SANS];
    g_font_serif = g_heading_fonts[font_index] ? g_heading_fonts[font_index]
                                               : g_heading_fonts[(int)ReaderFontFace::SERIF];

    FONT_SIZE = g_base_font_size + body_adjust[(int)g_reader_text_size];
    TITLE_SIZE = g_base_title_size + title_adjust[(int)g_reader_text_size];
    SECTION_SIZE = g_base_section_size + section_adjust[(int)g_reader_text_size];
    if (current_body_font())
        g_line_h = current_body_font()->get_line_height(FONT_SIZE) + 4;
}

static mtk::Theme wiki_theme() {
    ReaderPalette palette = reader_palette();
    mtk::Theme theme = mtk::make_theme(palette.accent);
    theme.window_bg = palette.page_bg;
    theme.surface = palette.control_bg;
    theme.surface_alt = palette.toolbar_bg;
    theme.surface_hover = mtk::mix(palette.toolbar_bg, palette.accent, 26);
    theme.border = palette.border;
    theme.text = palette.text;
    theme.text_muted = palette.muted;
    theme.text_subtle = palette.muted;
    theme.accent = palette.accent;
    theme.accent_hover = palette.accent_hover;
    theme.accent_soft = palette.accent_soft;
    theme.selection = palette.accent;
    theme.danger = palette.danger;
    theme.danger_hover = palette.danger_hover;
    theme.disabled_bg = palette.disabled_bg;
    theme.disabled_fg = palette.disabled_fg;
    return theme;
}

static int search_control_h() {
    return gui_clamp(g_base_font_size + 12, 24, 32);
}

static int reader_button_h() {
    return gui_clamp(search_control_h() - 4, 20, 28);
}

static int display_toggle_icon_size() {
    return gui_clamp(search_control_h() - 12, 14, 20);
}

static int display_toggle_button_w() {
    return gui_max(search_control_h(), display_toggle_icon_size() + 12);
}

static int search_row_y() {
    return 8;
}

static int reader_row_y() {
    return search_row_y() + search_control_h() + 8;
}

static void update_toolbar_height() {
    int h = search_row_y() + search_control_h() + 8;
    if (g_display_controls_open)
        h += reader_button_h() + 8;
    TOOLBAR_H = h;
}

static void reload_display_icons() {
    if (g_display_icon.pixels) svg_free(g_display_icon);
    if (g_display_icon_active.pixels) svg_free(g_display_icon_active);

    int icon_size = display_toggle_icon_size();
    ReaderPalette palette = reader_palette();
    g_display_icon = svg_load(DISPLAY_ICON_PATH, icon_size, icon_size, palette.text);
    g_display_icon_active = svg_load(DISPLAY_ICON_PATH, icon_size, icon_size, WHITE);
}

static int segmented_button_w(const char* label, int min_w) {
    return gui_max(text_width(label) + 18, min_w);
}

static int button_group_width(const char* const* labels, int count, int min_w, int gap) {
    int w = 0;
    for (int i = 0; i < count; i++) {
        if (i > 0) w += gap;
        w += segmented_button_w(labels[i], min_w);
    }
    return w;
}

static void layout_button_group(Rect* out, int count,
                                const char* const* labels,
                                int min_w, int gap,
                                int* x, int y, int h) {
    for (int i = 0; i < count; i++) {
        int w = segmented_button_w(labels[i], min_w);
        out[i] = {*x, y, w, h};
        *x += w + gap;
    }
    *x -= gap;
}

static ReaderToolbarLayout toolbar_layout() {
    ReaderToolbarLayout lo {};
    int pad = 8;
    int search_h = search_control_h();
    int reader_h = reader_button_h();
    int display_w = display_toggle_button_w();

    lo.search_button = {g_win_w - pad - 108, search_row_y(), 108, search_h};
    lo.display_button = {lo.search_button.x - 8 - display_w, search_row_y(), display_w, search_h};
    int field_w = lo.display_button.x - pad - 8;
    if (field_w < 80) field_w = 80;
    lo.search_field = {pad, search_row_y(), field_w, search_h};

    if (!g_display_controls_open)
        return lo;

    int gap = 6;
    int group_gap = 20;
    int size_w = button_group_width(g_text_size_labels, READER_TEXT_SIZE_COUNT, 34, gap);
    int font_w = button_group_width(g_font_face_labels, READER_FONT_FACE_COUNT, 50, gap);
    int total_w = size_w + font_w + group_gap;
    int x = gui_max((g_win_w - total_w) / 2, pad);
    int y = reader_row_y();

    layout_button_group(lo.text_size_buttons, READER_TEXT_SIZE_COUNT, g_text_size_labels, 34, gap, &x, y, reader_h);
    x += group_gap;
    lo.sep_x = x - group_gap / 2;

    layout_button_group(lo.font_buttons, READER_FONT_FACE_COUNT, g_font_face_labels, 50, gap, &x, y, reader_h);
    return lo;
}

static Rect content_rect() {
    int y = TOOLBAR_H + 1;
    return {0, y, g_win_w, gui_max(g_win_h - y, 0)};
}

static Rect article_column_rect() {
    Rect content = content_rect();
    int available_w = gui_max(content.w - TEXT_PAD * 2 - SCROLLBAR_W - 12, 80);
    int column_w = gui_min(available_w, 700);
    int x = TEXT_PAD + gui_max((available_w - column_w) / 2, 0);
    return {x, content.y, column_w, content.h};
}

static Rect search_button_rect() {
    return toolbar_layout().search_button;
}

static Rect search_field_rect() {
    return toolbar_layout().search_field;
}

static void draw_display_toggle_button(Canvas& canvas,
                                       const Rect& bounds,
                                       const mtk::WidgetState& state,
                                       const mtk::Theme& theme) {
    mtk::ButtonColors colors = mtk::resolve_button_colors(mtk::BUTTON_SECONDARY, state, theme);
    mtk::draw_rounded_frame(canvas, bounds, theme.radius_md, colors.bg, colors.border);

    const SvgIcon& icon = (state.active && g_display_icon_active.pixels)
                        ? g_display_icon_active
                        : g_display_icon;
    if (icon.pixels) {
        int ix = bounds.x + (bounds.w - icon.width) / 2;
        int iy = bounds.y + (bounds.h - icon.height) / 2;
        canvas.icon(ix, iy, icon);
        return;
    }

    const char* fallback = "Aa";
    int tw = text_width(fallback);
    int ty = bounds.y + (bounds.h - system_font_height()) / 2;
    canvas.text(bounds.x + (bounds.w - tw) / 2, ty, fallback, colors.fg);
}

static int visible_line_count() {
    Rect content = content_rect();
    if (g_line_h <= 0) return 0;
    return content.h / g_line_h;
}

static int max_scroll_lines() {
    int max_scroll = g_line_count - visible_line_count();
    return max_scroll > 0 ? max_scroll : 0;
}

static void clamp_scroll() {
    g_scroll_y = gui_clamp(g_scroll_y, 0, max_scroll_lines());
}

static bool mouse_in_rect(const Rect& rect) {
    return rect.contains(g_mouse_x, g_mouse_y);
}

static Rect scrollbar_track_rect() {
    Rect content = content_rect();
    int visible = visible_line_count();
    if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0};

    return {
        g_win_w - SCROLLBAR_W - 4,
        content.y + 8,
        SCROLLBAR_W,
        gui_max(content.h - 16, 0)
    };
}

static Rect scrollbar_thumb_rect() {
    Rect track = scrollbar_track_rect();
    if (track.empty()) return {0, 0, 0, 0};

    int visible = visible_line_count();
    if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0};

    int max_sc = max_scroll_lines();
    int thumb_h = (visible * track.h) / g_line_count;
    thumb_h = gui_clamp(thumb_h, 24, track.h);
    int travel = gui_max(track.h - thumb_h, 0);
    int thumb_y = track.y + (max_sc > 0 ? (g_scroll_y * travel) / max_sc : 0);
    return {track.x + 2, thumb_y, track.w - 4, thumb_h};
}

static void set_scroll_from_thumb_top(int thumb_top) {
    Rect track = scrollbar_track_rect();
    Rect thumb = scrollbar_thumb_rect();
    if (track.empty() || thumb.empty()) return;

    int max_sc = max_scroll_lines();
    int travel = gui_max(track.h - thumb.h, 0);
    if (max_sc <= 0 || travel <= 0) {
        g_scroll_y = 0;
        return;
    }

    int rel = gui_clamp(thumb_top - track.y, 0, travel);
    g_scroll_y = (rel * max_sc + travel / 2) / travel;
    clamp_scroll();
}

// ============================================================================
// HTTPS fetch wrapper
// ============================================================================

static int wiki_fetch(const char* path, char* respBuf, int respMax) {
    static char request[2560];
    int reqLen = snprintf(request, sizeof(request),
        "GET %s HTTP/1.0\r\n"
        "Host: %s\r\n"
        "User-Agent: MontaukOS/1.0 wikipedia\r\n"
        "Accept: application/json\r\n"
        "Connection: close\r\n"
        "\r\n",
        path, WIKI_HOST);
    return tls::https_fetch(WIKI_HOST, g_server_ip, 443,
                            request, reqLen, g_tas,
                            respBuf, respMax);
}

// ============================================================================
// Text encoding helpers
// ============================================================================

static int encode_single_byte_codepoint(unsigned codepoint) {
    if (codepoint < 0x80) return (int)codepoint;
    if (codepoint >= 0xA0 && codepoint <= 0xFF) return (int)codepoint;

    switch (codepoint) {
    case 0x20AC: return 0x80;
    case 0x201A: return 0x82;
    case 0x0192: return 0x83;
    case 0x201E: return 0x84;
    case 0x2026: return 0x85;
    case 0x2020: return 0x86;
    case 0x2021: return 0x87;
    case 0x02C6: return 0x88;
    case 0x2030: return 0x89;
    case 0x0160: return 0x8A;
    case 0x2039: return 0x8B;
    case 0x0152: return 0x8C;
    case 0x017D: return 0x8E;
    case 0x2018: return 0x91;
    case 0x2019: return 0x92;
    case 0x201C: return 0x93;
    case 0x201D: return 0x94;
    case 0x2022: return 0x95;
    case 0x2013: return 0x96;
    case 0x2014: return 0x97;
    case 0x02DC: return 0x98;
    case 0x2122: return 0x99;
    case 0x0161: return 0x9A;
    case 0x203A: return 0x9B;
    case 0x0153: return 0x9C;
    case 0x017E: return 0x9E;
    case 0x0178: return 0x9F;
    default: return -1;
    }
}

static void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint) {
    if (!out || !j || *j >= maxOut - 1) return;

    int encoded = encode_single_byte_codepoint(codepoint);
    if (encoded >= 0) {
        out[(*j)++] = (char)encoded;
        return;
    }

    switch (codepoint) {
    case 0x00A0:
        out[(*j)++] = ' ';
        return;
    case 0x2010:
    case 0x2011:
    case 0x2212:
        out[(*j)++] = '-';
        return;
    default:
        out[(*j)++] = '?';
        return;
    }
}

static unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed) {
    if (!buf || len <= 0) {
        if (consumed) *consumed = 0;
        return '?';
    }

    unsigned char c0 = (unsigned char)buf[0];
    if (c0 < 0x80) {
        if (consumed) *consumed = 1;
        return c0;
    }

    auto continuation = [](unsigned char c) -> bool {
        return (c & 0xC0) == 0x80;
    };

    if ((c0 & 0xE0) == 0xC0 && len >= 2) {
        unsigned char c1 = (unsigned char)buf[1];
        if (continuation(c1)) {
            if (consumed) *consumed = 2;
            return ((unsigned)(c0 & 0x1F) << 6) | (unsigned)(c1 & 0x3F);
        }
    } else if ((c0 & 0xF0) == 0xE0 && len >= 3) {
        unsigned char c1 = (unsigned char)buf[1];
        unsigned char c2 = (unsigned char)buf[2];
        if (continuation(c1) && continuation(c2)) {
            if (consumed) *consumed = 3;
            return ((unsigned)(c0 & 0x0F) << 12) |
                   ((unsigned)(c1 & 0x3F) << 6) |
                   (unsigned)(c2 & 0x3F);
        }
    } else if ((c0 & 0xF8) == 0xF0 && len >= 4) {
        unsigned char c1 = (unsigned char)buf[1];
        unsigned char c2 = (unsigned char)buf[2];
        unsigned char c3 = (unsigned char)buf[3];
        if (continuation(c1) && continuation(c2) && continuation(c3)) {
            if (consumed) *consumed = 4;
            return ((unsigned)(c0 & 0x07) << 18) |
                   ((unsigned)(c1 & 0x3F) << 12) |
                   ((unsigned)(c2 & 0x3F) << 6) |
                   (unsigned)(c3 & 0x3F);
        }
    }

    if (consumed) *consumed = 1;
    return '?';
}

// ============================================================================
// HTTP parsing
// ============================================================================

static int find_header_end(const char* buf, int len) {
    for (int i = 0; i + 3 < len; i++)
        if (buf[i]=='\r' && buf[i+1]=='\n' && buf[i+2]=='\r' && buf[i+3]=='\n')
            return i + 4;
    return -1;
}

static int parse_status_code(const char* buf, int len) {
    int i = 0;
    while (i < len && buf[i] != ' ') i++;
    if (i >= len || i + 3 >= len) return -1;
    i++;
    if (buf[i] < '0' || buf[i] > '9') return -1;
    return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0');
}

// ============================================================================
// URL encoding
// ============================================================================

static int url_encode_title(const char* in, char* out, int maxLen) {
    const char hex[] = "0123456789ABCDEF";
    int j = 0;
    for (int i = 0; in[i] && j < maxLen - 4; i++) {
        char c = in[i];
        if (c == ' ') {
            out[j++] = '_';
        } else if ((c>='A'&&c<='Z')||(c>='a'&&c<='z')||(c>='0'&&c<='9')||
                   c=='-'||c=='_'||c=='.'||c=='~'||c=='('||c==')'||c==',') {
            out[j++] = c;
        } else {
            out[j++] = '%';
            out[j++] = hex[(unsigned char)c >> 4];
            out[j++] = hex[(unsigned char)c & 0x0F];
        }
    }
    out[j] = '\0';
    return j;
}

// ============================================================================
// JSON string extraction
// ============================================================================

static int extract_json_string(const char* buf, int len, const char* key,
                                char* out, int maxOut) {
    int klen = (int)strlen(key);
    for (int i = 0; i < len - klen - 3; i++) {
        if (buf[i] != '"') continue;
        if (memcmp(buf + i + 1, key, klen) != 0) continue;
        if (buf[i + 1 + klen] != '"') continue;
        if (buf[i + 2 + klen] != ':') continue;

        int p = i + 3 + klen;
        while (p < len && (buf[p]==' ' || buf[p]=='\t')) p++;
        if (p >= len || buf[p] != '"') continue;
        p++;

        int j = 0;
        while (p < len && j < maxOut - 4) {
            if (buf[p] == '"') break;
            if (buf[p] == '\\' && p + 1 < len) {
                p++;
                switch (buf[p]) {
                case '"':  out[j++] = '"';  break;
                case '\\': out[j++] = '\\'; break;
                case 'n':  out[j++] = '\n'; break;
                case 'r':  break;
                case 't':  out[j++] = '\t'; break;
                case '/':  out[j++] = '/';  break;
                case 'u': {
                    if (p + 4 < len) {
                        unsigned val = 0;
                        for (int k = 1; k <= 4; k++) {
                            char h = buf[p + k]; val <<= 4;
                            if (h>='0'&&h<='9') val |= h-'0';
                            else if (h>='a'&&h<='f') val |= h-'a'+10;
                            else if (h>='A'&&h<='F') val |= h-'A'+10;
                        }
                        p += 4;
                        append_codepoint(out, &j, maxOut, val);
                    }
                    break;
                }
                default: out[j++] = buf[p]; break;
                }
            } else {
                unsigned char uc = (unsigned char)buf[p];
                if (uc < 0x80) {
                    out[j++] = buf[p];
                } else {
                    int consumed = 0;
                    unsigned codepoint = decode_utf8_codepoint(buf + p, len - p, &consumed);
                    append_codepoint(out, &j, maxOut, codepoint);
                    p += consumed > 0 ? consumed - 1 : 0;
                }
            }
            p++;
        }
        out[j] = '\0';
        return j;
    }
    out[0] = '\0';
    return 0;
}

// ============================================================================
// Display line building
// ============================================================================

static void add_line(const char* text, int len, Color color, int size,
                     TrueTypeFont* font, int indent = 0) {
    if (g_line_count >= MAX_LINES) return;
    WikiLine* l = &g_lines[g_line_count++];
    int copy = len < 255 ? len : 255;
    memcpy(l->text, text, copy);
    l->text[copy] = '\0';
    l->color     = color;
    l->font_size = size;
    l->font      = font;
    l->indent    = indent;
}

static void add_spacer(int size) {
    if (g_line_count >= MAX_LINES) return;
    WikiLine* l = &g_lines[g_line_count++];
    l->text[0]   = '\0';
    l->color     = reader_palette().text;
    l->font_size = size;
    l->font      = current_body_font();
    l->indent    = 0;
}

// Word-wrap a text segment into display lines using pixel-width measurement.
static void wrap_text(TrueTypeFont* font, int size, const char* text, int textLen,
                      int max_px, Color color, int indent = 0) {
    if (!font || !text || textLen <= 0) return;
    static constexpr int MAX_LINE = 255;
    char  cur[MAX_LINE + 1];
    int   cur_len = 0;
    const char* p   = text;
    const char* end = text + textLen;

    while (p < end) {
        while (p < end && *p == ' ') p++;
        if (p >= end) break;

        const char* word_start = p;
        while (p < end && *p != ' ') p++;
        int word_len = (int)(p - word_start);
        if (word_len <= 0) continue;

        // Build candidate: current line + space + word
        char test[MAX_LINE + 1];
        int test_len = cur_len;
        memcpy(test, cur, cur_len);
        if (cur_len > 0 && test_len < MAX_LINE) test[test_len++] = ' ';
        int avail = MAX_LINE - test_len;
        int copy = word_len < avail ? word_len : avail;
        if (copy > 0) {
            memcpy(test + test_len, word_start, copy);
            test_len += copy;
        }
        test[test_len] = '\0';

        int test_w = font->measure_text(test, size);
        if (test_w <= max_px || cur_len == 0) {
            // Fits — append to current line
            memcpy(cur, test, test_len + 1);
            cur_len = test_len;
        } else {
            // Emit current line and start fresh
            if (cur_len > 0) add_line(cur, cur_len, color, size, font, indent);
            int wl = word_len < MAX_LINE ? word_len : MAX_LINE;
            memcpy(cur, word_start, wl);
            cur_len = wl;
            cur[cur_len] = '\0';
        }
    }
    if (cur_len > 0) add_line(cur, cur_len, color, size, font, indent);
}

static void build_display_lines(const char* title, const char* extract, int extractLen,
                                bool preserve_scroll = false) {
    ReaderPalette palette = reader_palette();
    TrueTypeFont* body_font = current_body_font();
    TrueTypeFont* heading_font = current_heading_font();
    int old_scroll = g_scroll_y;

    if (!body_font) return;

    g_line_count = 0;
    g_scroll_y   = 0;

    Rect column = article_column_rect();
    int max_px = column.w;

    // Title — large, serif, black
    if (title && title[0]) {
        TrueTypeFont* tf = heading_font ? heading_font : body_font;
        wrap_text(tf, TITLE_SIZE, title, (int)strlen(title), max_px, palette.heading);
        add_spacer(gui_max(FONT_SIZE, 12));
    }

    // Process extract line-by-line
    const char* p   = extract;
    const char* end = extract + extractLen;
    bool pending_paragraph_break = false;

    while (p < end && g_line_count < MAX_LINES) {
        const char* line_start = p;
        while (p < end && *p != '\n') p++;
        int line_len = (int)(p - line_start);
        if (p < end) p++;  // consume '\n'

        if (line_len == 0) {
            pending_paragraph_break = true;
            continue;
        }

        // Section header: == Title ==
        if (line_len >= 4 && line_start[0] == '=' && line_start[1] == '=') {
            int si = 0, ei = line_len;
            while (si < line_len && line_start[si] == '=') si++;
            while (si < line_len && line_start[si] == ' ') si++;
            while (ei > si && line_start[ei-1] == '=') ei--;
            while (ei > si && line_start[ei-1] == ' ') ei--;
            if (ei > si) {
                if (g_line_count > 0)
                    add_spacer(gui_max(FONT_SIZE - 2, 12));
                TrueTypeFont* tf = heading_font ? heading_font : body_font;
                wrap_text(tf, SECTION_SIZE, line_start + si, ei - si, max_px, palette.heading);
                add_spacer(gui_max(FONT_SIZE - 6, 8));
                pending_paragraph_break = false;
            }
            continue;
        }

        if (pending_paragraph_break && g_line_count > 0) {
            add_spacer(gui_max(FONT_SIZE - 6, 8));
            pending_paragraph_break = false;
        }

        // Regular body text
        wrap_text(body_font, FONT_SIZE, line_start, line_len, max_px, palette.text);
    }

    if (preserve_scroll) {
        g_scroll_y = gui_clamp(old_scroll, 0, max_scroll_lines());
    }
}

static void rebuild_article(bool preserve_scroll = true) {
    if (g_phase == AppPhase::DONE && g_extract_len > 0)
        build_display_lines(g_title, g_extract_buf, g_extract_len, preserve_scroll);
}

static void apply_reader_setting_change() {
    g_scroll_dragging = false;
    apply_reader_preferences();
    update_toolbar_height();
    rebuild_article(true);
    clamp_scroll();
}

static bool handle_reader_option_click(int mx, int my) {
    ReaderToolbarLayout lo = toolbar_layout();
    if (!g_display_controls_open) return false;

    for (int i = 0; i < READER_TEXT_SIZE_COUNT; i++) {
        if (!lo.text_size_buttons[i].contains(mx, my)) continue;
        ReaderTextSize next = (ReaderTextSize)i;
        if (g_reader_text_size != next) {
            g_reader_text_size = next;
            apply_reader_setting_change();
        }
        return true;
    }

    for (int i = 0; i < READER_FONT_FACE_COUNT; i++) {
        if (!lo.font_buttons[i].contains(mx, my)) continue;
        ReaderFontFace next = (ReaderFontFace)i;
        if (g_reader_font_face != next) {
            g_reader_font_face = next;
            apply_reader_setting_change();
        }
        return true;
    }

    return false;
}

// ============================================================================
// Network search (blocking)
// ============================================================================

static void do_search(const char* query) {
    // Lazy TLS/DNS init
    if (!g_tls_ready) {
        g_server_ip = montauk::resolve(WIKI_HOST);
        if (g_server_ip == 0) {
            snprintf(g_status, sizeof(g_status),
                     "Error: could not resolve en.wikipedia.org");
            g_phase = AppPhase::ERR; return;
        }
        g_tas = tls::load_trust_anchors();
        if (g_tas.count == 0) {
            snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded");
            g_phase = AppPhase::ERR; return;
        }
        g_tls_ready = true;
    }

    static char encoded[1024];
    url_encode_title(query, encoded, sizeof(encoded));

    static char path[2048];
    snprintf(path, sizeof(path),
        "/w/api.php?action=query&format=json&formatversion=2"
        "&prop=extracts&explaintext=1&titles=%s", encoded);

    int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX);
    if (respLen <= 0) {
        snprintf(g_status, sizeof(g_status), "Error: no response from Wikipedia");
        g_phase = AppPhase::ERR; return;
    }
    g_resp_buf[respLen] = '\0';

    int headerEnd = find_header_end(g_resp_buf, respLen);
    if (headerEnd < 0) {
        snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
        g_phase = AppPhase::ERR; return;
    }

    int status      = parse_status_code(g_resp_buf, headerEnd);
    const char* body = g_resp_buf + headerEnd;
    int bodyLen     = respLen - headerEnd;

    if (status == 404) {
        snprintf(g_status, sizeof(g_status), "Article not found: %s", query);
        g_phase = AppPhase::ERR; return;
    }

    extract_json_string(body, bodyLen, "title", g_title, sizeof(g_title));
    g_extract_len = extract_json_string(body, bodyLen, "extract",
                                        g_extract_buf, RESP_MAX - 1);
    if (g_extract_len == 0) {
        snprintf(g_status, sizeof(g_status), "No content found for: %s", query);
        g_phase = AppPhase::ERR; return;
    }

    build_display_lines(g_title, g_extract_buf, g_extract_len);
    g_phase = AppPhase::DONE;
}

// ============================================================================
// Rendering
// ============================================================================

static void render_message(Canvas& canvas, const Rect& content, const char* text, Color color) {
    if (!text || !text[0]) return;
    Rect column = article_column_rect();
    canvas.text(column.x, content.y + 16, text, color);
}

static void render(Canvas& canvas) {
    mtk::Theme theme = wiki_theme();
    ReaderToolbarLayout lo = toolbar_layout();
    Rect toolbar = {0, 0, g_win_w, TOOLBAR_H};
    Rect content = content_rect();
    Rect column = article_column_rect();
    bool searching = (g_phase == AppPhase::LOADING);

    // Background
    canvas.fill(theme.window_bg);

    // ---- Toolbar ----
    canvas.fill_rect(toolbar.x, toolbar.y, toolbar.w, toolbar.h, theme.surface_alt);
    mtk::draw_text_field(canvas, lo.search_field, g_query,
                         g_query_input.cursor,
                         g_search_focused, false, theme,
                         g_query_input.selection_anchor);
    draw_display_toggle_button(canvas, lo.display_button,
                               mtk::widget_state(g_display_controls_open,
                                                 mouse_in_rect(lo.display_button), true),
                               theme);
    mtk::draw_button(canvas, lo.search_button, searching ? "Searching..." : "Search",
                     mtk::BUTTON_PRIMARY,
                     mtk::widget_state(false, mouse_in_rect(lo.search_button), !searching),
                     theme);

    if (g_display_controls_open) {
        for (int i = 0; i < READER_TEXT_SIZE_COUNT; i++) {
            mtk::draw_button(canvas, lo.text_size_buttons[i], g_text_size_labels[i], mtk::BUTTON_SECONDARY,
                             mtk::widget_state((int)g_reader_text_size == i,
                                               mouse_in_rect(lo.text_size_buttons[i]), true),
                             theme);
        }
        for (int i = 0; i < READER_FONT_FACE_COUNT; i++) {
            mtk::draw_button(canvas, lo.font_buttons[i], g_font_face_labels[i], mtk::BUTTON_SECONDARY,
                             mtk::widget_state((int)g_reader_font_face == i,
                                               mouse_in_rect(lo.font_buttons[i]), true),
                             theme);
        }

        int sep_y = reader_row_y() + 3;
        int sep_h = gui_max(reader_button_h() - 6, 1);
        canvas.vline(lo.sep_x, sep_y, sep_h, theme.border);
    }
    mtk::draw_separator(canvas, 0, TOOLBAR_H, g_win_w, theme);

    // ---- Content area ----
    if (g_phase == AppPhase::IDLE) {
        render_message(canvas, content,
                       "Type a topic and press Enter or click Search.",
                       theme.text_muted);
    } else if (g_phase == AppPhase::LOADING) {
        render_message(canvas, content, "Searching Wikipedia...", theme.text_muted);
    } else if (g_phase == AppPhase::ERR) {
        render_message(canvas, content, g_status, theme.danger);
    } else if (g_phase == AppPhase::DONE && g_line_count > 0) {
        int y = content.y + 8;

        for (int i = g_scroll_y; i < g_line_count && y < g_win_h; i++) {
            WikiLine& l  = g_lines[i];
            TrueTypeFont* line_font = l.font ? l.font : current_body_font();
            int lh = line_font ? (line_font->get_line_height(l.font_size) + 4) : g_line_h;
            if (y + lh > g_win_h) break;
            if (l.text[0] != '\0' && line_font) {
                line_font->draw_to_buffer(canvas.pixels, canvas.w, canvas.h,
                                          column.x + l.indent, y, l.text, l.color, l.font_size);
            }
            y += lh;
        }

        // Scrollbar
        Rect track = scrollbar_track_rect();
        if (!track.empty()) {
            Rect thumb = scrollbar_thumb_rect();
            Color thumb_color = (g_scroll_dragging || mouse_in_rect(thumb))
                              ? theme.accent_hover
                              : theme.selection;

            canvas.fill_rounded_rect(track.x, track.y, track.w, track.h,
                                     theme.radius_sm, theme.surface_alt);
            canvas.rect(track.x, track.y, track.w, track.h, theme.border);
            canvas.fill_rounded_rect(thumb.x, thumb.y, thumb.w, thumb.h,
                                     theme.radius_sm, thumb_color);
        }
    }

    mtk::draw_text_input_context_menu(
        canvas, g_query_input, theme,
        mtk::text_input_has_selection(g_query_input,
                                      g_query, (int)sizeof(g_query)));
}

// ============================================================================
// Entry point
// ============================================================================

extern "C" void _start() {
    if (!fonts::init())
        montauk::exit(1);

    // Allocate large buffers from heap
    g_lines       = (WikiLine*)montauk::malloc(MAX_LINES * sizeof(WikiLine));
    g_resp_buf    = (char*)malloc(RESP_MAX + 1);
    g_extract_buf = (char*)malloc(RESP_MAX + 1);
    if (!g_lines || !g_resp_buf || !g_extract_buf) montauk::exit(1);

    // Load fonts
    auto load_font = [](const char* path) -> TrueTypeFont* {
        TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
        if (!f) return nullptr;
        montauk::memset(f, 0, sizeof(TrueTypeFont));
        if (!f->init(path)) { montauk::mfree(f); return nullptr; }
        return f;
    };

    g_body_fonts[(int)ReaderFontFace::SANS] = load_font("0:/fonts/Roboto-Regular.ttf");
    if (!g_body_fonts[(int)ReaderFontFace::SANS])
        g_body_fonts[(int)ReaderFontFace::SANS] = load_font("0:/fonts/Roboto-Medium.ttf");
    g_heading_fonts[(int)ReaderFontFace::SANS] = load_font("0:/fonts/Roboto-Bold.ttf");

    g_body_fonts[(int)ReaderFontFace::SERIF] = load_font("0:/fonts/NotoSerif-Regular.ttf");
    g_heading_fonts[(int)ReaderFontFace::SERIF] = load_font("0:/fonts/NotoSerif-SemiBold.ttf");

    g_body_fonts[(int)ReaderFontFace::BOOK] = load_font("0:/fonts/C059-Roman.ttf");
    g_heading_fonts[(int)ReaderFontFace::BOOK] = load_font("0:/fonts/C059-Bold.ttf");

    if (!g_body_fonts[(int)ReaderFontFace::SANS])
        montauk::exit(1);

    for (int i = 0; i < READER_FONT_FACE_COUNT; i++) {
        if (!g_heading_fonts[i]) g_heading_fonts[i] = g_body_fonts[i];
    }

    apply_scale(montauk::win_getscale());

    WsWindow win;
    if (!win.create("Wikipedia", INIT_W, INIT_H))
        montauk::exit(1);

    mtk::StandaloneHost host(&win);
    Canvas canvas = host.canvas();
    render(canvas);
    host.present();

    bool search_pending = false;

    while (true) {
        Montauk::WinEvent ev;
        int r = win.poll(&ev);

        if (r < 0) break;  // window closed / error

        if (r == 0) {
            // No event — idle at ~60 fps
            montauk::sleep_ms(16);
            canvas = host.canvas();
            render(canvas);
            host.present();
            continue;
        }

        // ---- Handle event ----
        if (ev.type == 3) break;  // close

        if (ev.type == 4) {
            apply_scale(win.scale_factor);
            g_scroll_dragging = false;
            rebuild_article(true);
        }

        if (ev.type == 2) {
            if (win.width > 0 && win.height > 0) {
                g_win_w = win.width;
                g_win_h = win.height;
                g_scroll_dragging = false;
                rebuild_article(true);
                clamp_scroll();
            }

        } else if (ev.type == 0 && ev.key.pressed) {
            uint8_t ascii = ev.key.ascii;
            uint8_t scan  = ev.key.scancode;

            if (g_search_focused) {
                if (ascii == '\n' || ascii == '\r') {
                    search_pending = true;
                } else {
                    mtk::text_input_key(g_query_input, g_query,
                                        (int)sizeof(g_query), ev.key);
                }
            } else if (g_phase == AppPhase::DONE) {
                // Navigation keys
                int visible = visible_line_count();
                int max_sc  = max_scroll_lines();
                if      (scan == 0x48) { if (g_scroll_y > 0) g_scroll_y--; }
                else if (scan == 0x50) { if (g_scroll_y < max_sc) g_scroll_y++; }
                else if (scan == 0x49) { g_scroll_y -= visible; if (g_scroll_y < 0) g_scroll_y = 0; }
                else if (scan == 0x51) { g_scroll_y += visible; if (g_scroll_y > max_sc) g_scroll_y = max_sc; }
                else if (scan == 0x47) { g_scroll_y = 0; }
                else if (scan == 0x4F) { g_scroll_y = max_sc; }
            }

        } else if (ev.type == 1) {
            // Mouse
            int mx = ev.mouse.x, my = ev.mouse.y;
            g_mouse_x = mx;
            g_mouse_y = my;
            bool left_down = (ev.mouse.buttons & 1) != 0;
            bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
            bool just_right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
            bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);

            ReaderToolbarLayout lo = toolbar_layout();
            Rect scrollbar_track = scrollbar_track_rect();
            Rect scrollbar_thumb = scrollbar_thumb_rect();

            if (g_query_input.context.open || g_query_input.dragging) {
                int result = mtk::text_input_handle_mouse(g_query_input, lo.search_field,
                                                          g_query, (int)sizeof(g_query),
                                                          mx, my, ev.mouse.buttons,
                                                          ev.mouse.prev_buttons,
                                                          g_win_w, g_win_h,
                                                          g_search_focused);
                if (result & mtk::TEXT_INPUT_CONSUMED) {
                    just_clicked = false;
                    just_right_clicked = false;
                }
            }

            if (just_released) {
                g_scroll_dragging = false;
            }

            if (g_scroll_dragging && left_down) {
                set_scroll_from_thumb_top(my - g_scroll_drag_offset);
            }

            if (just_clicked || just_right_clicked) {
                if (lo.search_field.contains(mx, my)) {
                    g_search_focused = true;
                    mtk::text_input_handle_mouse(g_query_input, lo.search_field,
                                                 g_query, (int)sizeof(g_query),
                                                 mx, my, ev.mouse.buttons, ev.mouse.prev_buttons,
                                                 g_win_w, g_win_h, true);
                } else if (scrollbar_thumb.contains(mx, my)) {
                    g_search_focused = false;
                    g_scroll_dragging = true;
                    g_scroll_drag_offset = my - scrollbar_thumb.y;
                } else if (scrollbar_track.contains(mx, my)) {
                    g_search_focused = false;
                    g_scroll_dragging = true;
                    g_scroll_drag_offset = scrollbar_thumb.h / 2;
                    set_scroll_from_thumb_top(my - g_scroll_drag_offset);
                } else if (lo.display_button.contains(mx, my)) {
                    g_search_focused = false;
                    g_display_controls_open = !g_display_controls_open;
                    update_toolbar_height();
                    rebuild_article(true);
                    clamp_scroll();
                } else if (handle_reader_option_click(mx, my)) {
                    g_search_focused = false;
                    // Reader controls handled.
                } else {
                    g_search_focused = false;
                }
            }

            // Search button hit-test
            if (just_clicked && lo.search_button.contains(mx, my) &&
                g_phase != AppPhase::LOADING) {
                search_pending = true;
            }

            // Scroll wheel
            if (ev.mouse.scroll != 0 && g_phase == AppPhase::DONE) {
                g_scroll_y += ev.mouse.scroll * 3;
                clamp_scroll();
            }
        }

        // Trigger search if requested and query is non-empty
        if (search_pending && g_query[0] != '\0') {
            search_pending = false;
            g_phase = AppPhase::LOADING;
            canvas = host.canvas();
            render(canvas);
            host.present();
            do_search(g_query);  // blocking
            clamp_scroll();
        }

        canvas = host.canvas();
        render(canvas);
        host.present();
    }

    if (g_display_icon.pixels) svg_free(g_display_icon);
    if (g_display_icon_active.pixels) svg_free(g_display_icon_active);
    win.destroy();
    montauk::exit(0);
}
