// Visual keyboard + mouse demo: one square per jlKeyE, lit when its
// key is held, and a small pointer drawn at the live mouse position.
// Holding the left mouse button while the pointer is over a cell lights
// it as if the corresponding key were down. Press ESC to quit.
//
// The strip under the grid is a typed-text line fed by jlInputGetChar:
// whatever you type -- letters, digits, shifted punctuation -- appears
// there, Backspace deletes, Return clears. It doubles as the
// acceptance test for the typed-character queue ("192.168.1.10:6510"
// must be typeable on every port). The bottom two scanlines
// additionally encode verification data as raw pixel nibbles behind
// sentinels: row 198 carries every received character (A5A5 sentinel,
// 2 pixels = 1 byte) and row 199 the live mouse state (5A5A sentinel,
// present/button flags plus position), so emulator harnesses can
// verify the exact bytes from a framebuffer dump.
//
// The render loop only redraws cells whose target lit state changed
// since last frame -- so on idle frames, the only work is the cursor
// erase + redraw + a tiny rect-present pair. The cursor erase is
// implemented by redrawing the cell that contained the *previous*
// cursor position; in the gap regions between cells the cursor will
// briefly leave a trail until that cell is touched again, which is an
// acceptable demo-quality compromise.

#include <stdio.h>

#include <joey/joey.h>
#include <joey/debug.h>

#define GRID_COLS        10
#define GRID_ROWS        6
#define CELL_W           28
#define CELL_H           26
#define GAP              4
#define MARGIN_X         2
#define MARGIN_Y         2

#define CURSOR_W         4
#define CURSOR_H         4

#define COLOR_BACKGROUND 0
#define COLOR_UNLIT      1
#define COLOR_LIT        2
#define COLOR_CURSOR     3

#define CELL_NONE        ((int16_t)-1)

// Typed-text line under the grid (the grid ends at y = 178 with the
// 26-pixel cells above).
#define FONT_W           5
#define FONT_H           7
#define TEXT_STRIP_Y     180
#define TEXT_X           4
#define TEXT_Y           182
#define TEXT_INPUT_X     68
#define MAX_LINE         40

// Verification scanline: sentinel nibbles, then a received-character
// count byte, then every received character as (high nibble, low
// nibble) pixel pairs. An emulator harness reads this row back from
// the framebuffer to prove each typed byte arrived.
#define VERIFY_Y          198
#define VERIFY_SENTINEL   0xA5A5
#define VERIFY_COUNT_X    4
#define VERIFY_CHARS_X    8
#define MAX_RECEIVED      150

// Every field on both verification rows is stamped high nibble first,
// one nibble per pixel: a byte is two pixels wide, a 16-bit value four.
#define VERIFY_BYTE_NIBBLES 2
#define VERIFY_WORD_NIBBLES 4

// Mouse verification scanline (the row below the typed-character one):
// its own 5A5A sentinel, then present/left/right/middle as one nibble
// each, then jlMouseX and jlMouseY as four nibbles each.
// Re-stamped only when the reported state changes.
#define VERIFY_MOUSE_Y       199
#define VERIFY_MOUSE_SENTINEL 0x5A5A
#define VERIFY_MOUSE_FLAGS_X 4
#define VERIFY_MOUSE_XPOS_X  8
#define VERIFY_MOUSE_YPOS_X  12

// The flags field packed as one nibble per predicate, in the order the
// row stamps them. This packing is the ONLY definition of that layout:
// it is both what gets drawn and what the change check compares.
#define MOUSE_FLAG_PRESENT 0x1000
#define MOUSE_FLAG_LEFT    0x0100
#define MOUSE_FLAG_RIGHT   0x0010
#define MOUSE_FLAG_MIDDLE  0x0001

static void buildPalette(jlSurfaceT *screen);
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow);
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
static void drawMouseVerifyRow(jlSurfaceT *screen);
static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *text, uint8_t color);
static void drawVerifyRow(jlSurfaceT *screen);
static int  glyphIdx(char c);
static void initialPaint(jlSurfaceT *screen);
static void logTypedHistory(void);
static uint16_t mouseFlags(void);
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void processTypedChars(jlSurfaceT *screen);
static void redrawTextStrip(jlSurfaceT *screen);
static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles);
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateMouseVerifyRow(jlSurfaceT *screen);

// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly
// resembles a real keyboard (top number row, then QWERTY rows, then a
// cluster of modifiers / arrows / function keys).
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
    { KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 },
    { KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P },
    { KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE },
    { KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_LSHIFT, KEY_RSHIFT, KEY_TAB },
    { KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_LCTRL, KEY_LALT, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_NONE },
    { KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 }
};

static bool    gCellLit[GRID_ROWS][GRID_COLS];
static int16_t gLastCursorX    = -100;
static int16_t gLastCursorY    = -100;
static int16_t gLastCursorCol  = CELL_NONE;
static int16_t gLastCursorRow  = CELL_NONE;

// Last state stamped into the mouse verification row; updateMouseVerifyRow
// only redraws when this tuple changes, so idle frames stay draw-free.
static int16_t  gLastMouseRowX     = -1;
static int16_t  gLastMouseRowY     = -1;
static uint16_t gLastMouseRowFlags = 0xFFFF;

// Typed-text line state plus the full received-character history for
// the verification row.
static char    gLine[MAX_LINE + 1];
static int16_t gLineLen       = 0;
static uint8_t gReceived[MAX_RECEIVED];
static int16_t gReceivedCount = 0;

// Tiny 5x7 font (high 5 bits of each row byte), borrowed from the
// adventure example and extended with a few typed-punctuation glyphs.
// Coverage: space, 0-9, A-Z (lowercase renders uppercase), and
// . , : ! ? - ' " / ; = + _ -- everything else renders as the hollow
// box at the end of the table.
//
// Row indices used by glyphIdx. The table below must keep this order;
// the trailing row comments carry the same numbers.
#define GLYPH_SPACE       0
#define GLYPH_DIGIT_BASE  1
#define GLYPH_ALPHA_BASE  11
#define GLYPH_PERIOD      37
#define GLYPH_COMMA       38
#define GLYPH_COLON       39
#define GLYPH_BANG        40
#define GLYPH_QUESTION    41
#define GLYPH_MINUS       42
#define GLYPH_APOSTROPHE  43
#define GLYPH_QUOTE       44
#define GLYPH_SLASH       45
#define GLYPH_SEMICOLON   46
#define GLYPH_EQUALS      47
#define GLYPH_PLUS        48
#define GLYPH_UNDERSCORE  49
#define GLYPH_UNKNOWN     50

static const uint8_t kFontGlyph[][FONT_H] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, //  0 space
    { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, //  1 '0'
    { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, //  2 '1'
    { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, //  3 '2'
    { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, //  4 '3'
    { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, //  5 '4'
    { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, //  6 '5'
    { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, //  7 '6'
    { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, //  8 '7'
    { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, //  9 '8'
    { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, // 10 '9'
    { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 11 'A'
    { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, // 12 'B'
    { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, // 13 'C'
    { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, // 14 'D'
    { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, // 15 'E'
    { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, // 16 'F'
    { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, // 17 'G'
    { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 18 'H'
    { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 19 'I'
    { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, // 20 'J'
    { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, // 21 'K'
    { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, // 22 'L'
    { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, // 23 'M'
    { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, // 24 'N'
    { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 25 'O'
    { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, // 26 'P'
    { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, // 27 'Q'
    { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, // 28 'R'
    { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, // 29 'S'
    { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, // 30 'T'
    { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 31 'U'
    { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, // 32 'V'
    { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, // 33 'W'
    { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, // 34 'X'
    { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, // 35 'Y'
    { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, // 36 'Z'
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, // 37 '.'
    { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, // 38 ','
    { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, // 39 ':'
    { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, // 40 '!'
    { 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, // 41 '?'
    { 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, // 42 '-'
    { 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 43 '\''
    { 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 44 '"'
    { 0x00, 0x08, 0x10, 0x20, 0x40, 0x80, 0x00 }, // 45 '/'
    { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x40 }, // 46 ';'
    { 0x00, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00 }, // 47 '='
    { 0x00, 0x20, 0x20, 0xF8, 0x20, 0x20, 0x00 }, // 48 '+'
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8 }, // 49 '_'
    { 0xF8, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8 }, // 50 unknown box
};


static void buildPalette(jlSurfaceT *screen) {
    uint16_t colors[SURFACE_COLORS_PER_PALETTE];
    uint16_t i;

    // Every entry gets a DISTINCT color (gray ramp for the unused
    // ones, red for 15 since 0x0FFF is taken by the cursor) so a
    // screenshot-based harness can decode the verification row's
    // nibble pixels back to values on ports without direct
    // framebuffer reads.
    for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
        colors[i] = (uint16_t)((i << 8) | (i << 4) | i);
    }
    colors[COLOR_BACKGROUND] = 0x0000;  // black
    colors[COLOR_UNLIT]      = 0x0333;  // dark gray
    colors[COLOR_LIT]        = 0x00F0;  // bright green
    colors[COLOR_CURSOR]     = 0x0FFF;  // white
    colors[15]               = 0x0F00;  // red (0x0FFF is the cursor's)

    jlPaletteSet(screen, 0, colors);
}


static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow) {
    int16_t col;
    int16_t row;
    int16_t cx;
    int16_t cy;

    *outCol = CELL_NONE;
    *outRow = CELL_NONE;
    for (row = 0; row < GRID_ROWS; row++) {
        for (col = 0; col < GRID_COLS; col++) {
            cx = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
            cy = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
            if (px >= cx && px < (cx + CELL_W) && py >= cy && py < (cy + CELL_H)) {
                *outCol = col;
                *outRow = row;
                return;
            }
        }
    }
}


static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) {
    jlKeyE key;

    key = gKeyGrid[row][col];
    if (key == KEY_NONE) {
        return false;
    }
    if (jlKeyDown(key)) {
        return true;
    }
    if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) {
        return true;
    }
    return false;
}


static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit) {
    int16_t x;
    int16_t y;
    uint8_t color;

    x     = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
    y     = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
    color = lit ? COLOR_LIT : COLOR_UNLIT;
    jlFillRect(screen, x, y, CELL_W, CELL_H, color);
}


// The block is clipped against the verification scanlines rather than
// drawn over them: emulator harnesses sample those rows between
// frames, so even a stamp-then-repair within one loop iteration is a
// visible corruption window to them.
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
    int16_t h;

    h = CURSOR_H;
    if ((int16_t)(y + h) > VERIFY_Y) {
        h = (int16_t)(VERIFY_Y - y);
    }
    if (h <= 0) {
        return;
    }
    jlFillRect(screen, x, y, CURSOR_W, h, COLOR_CURSOR);
}


// Stamp the mouse verification scanline: sentinel, present/button
// flags, then the pointer position as nibble pixels. An emulator
// harness reads this row back from the framebuffer to prove mouse
// motion and button state crossed the HAL (see verify-x68000-mouse.sh).
static void drawMouseVerifyRow(jlSurfaceT *screen) {
    stampNibbles(screen, 0,                    VERIFY_MOUSE_Y, VERIFY_MOUSE_SENTINEL, VERIFY_WORD_NIBBLES);
    stampNibbles(screen, VERIFY_MOUSE_FLAGS_X, VERIFY_MOUSE_Y, mouseFlags(),          VERIFY_WORD_NIBBLES);
    stampNibbles(screen, VERIFY_MOUSE_XPOS_X,  VERIFY_MOUSE_Y, (uint16_t)jlMouseX(),  VERIFY_WORD_NIBBLES);
    stampNibbles(screen, VERIFY_MOUSE_YPOS_X,  VERIFY_MOUSE_Y, (uint16_t)jlMouseY(),  VERIFY_WORD_NIBBLES);
}


static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *text, uint8_t color) {
    int16_t cx;
    int16_t i;
    int16_t row;
    int16_t col;
    int     idx;
    uint8_t bits;

    cx = x;
    for (i = 0; text[i] != '\0'; i++) {
        idx = glyphIdx(text[i]);
        for (row = 0; row < FONT_H; row++) {
            bits = kFontGlyph[idx][row];
            for (col = 0; col < FONT_W; col++) {
                if (bits & (0x80 >> col)) {
                    jlDrawPixel(screen, (int16_t)(cx + col), (int16_t)(y + row), color);
                }
            }
        }
        cx = (int16_t)(cx + FONT_W + 1);
        if (cx > SURFACE_WIDTH - FONT_W) {
            return;
        }
    }
}


// Re-stamp the verification scanline: sentinel, received count, then
// every received character as two nibble pixels. Only the count and
// the newest characters actually change, but the row is cheap enough
// to redraw whole.
static void drawVerifyRow(jlSurfaceT *screen) {
    int16_t i;

    stampNibbles(screen, 0,               VERIFY_Y, VERIFY_SENTINEL,            VERIFY_WORD_NIBBLES);
    stampNibbles(screen, VERIFY_COUNT_X,  VERIFY_Y, (uint16_t)gReceivedCount,   VERIFY_BYTE_NIBBLES);
    for (i = 0; i < gReceivedCount; i++) {
        stampNibbles(screen, (int16_t)(VERIFY_CHARS_X + VERIFY_BYTE_NIBBLES * i), VERIFY_Y, gReceived[i], VERIFY_BYTE_NIBBLES);
    }
    // The strip repaint that called us blanked the mouse row too, so
    // restore it in the same pass.
    drawMouseVerifyRow(screen);
}


static int glyphIdx(char c) {
    if (c == ' ')             { return GLYPH_SPACE; }
    if (c >= '0' && c <= '9') { return GLYPH_DIGIT_BASE + (c - '0'); }
    if (c >= 'A' && c <= 'Z') { return GLYPH_ALPHA_BASE + (c - 'A'); }
    if (c >= 'a' && c <= 'z') { return GLYPH_ALPHA_BASE + (c - 'a'); }  // uppercase fallback
    if (c == '.')             { return GLYPH_PERIOD; }
    if (c == ',')             { return GLYPH_COMMA; }
    if (c == ':')             { return GLYPH_COLON; }
    if (c == '!')             { return GLYPH_BANG; }
    if (c == '?')             { return GLYPH_QUESTION; }
    if (c == '-')             { return GLYPH_MINUS; }
    if (c == '\'')            { return GLYPH_APOSTROPHE; }
    if (c == '"')             { return GLYPH_QUOTE; }
    if (c == '/')             { return GLYPH_SLASH; }
    if (c == ';')             { return GLYPH_SEMICOLON; }
    if (c == '=')             { return GLYPH_EQUALS; }
    if (c == '+')             { return GLYPH_PLUS; }
    if (c == '_')             { return GLYPH_UNDERSCORE; }
    return GLYPH_UNKNOWN;
}


static void initialPaint(jlSurfaceT *screen) {
    int16_t  col;
    int16_t  row;
    jlKeyE key;

    jlSurfaceClear(screen, COLOR_BACKGROUND);
    for (row = 0; row < GRID_ROWS; row++) {
        for (col = 0; col < GRID_COLS; col++) {
            key = gKeyGrid[row][col];
            if (key == KEY_NONE) {
                continue;
            }
            drawCell(screen, col, row, false);
            gCellLit[row][col] = false;
        }
    }
    redrawTextStrip(screen);
    jlStagePresent();
}


// Emit the received-character history as hex on exit so an emulator
// harness can verify the typed-character path from the boot volume
// when it cannot read the framebuffer (see verify-amiga-input.sh).
static void logTypedHistory(void) {
    static const char kHex[] = "0123456789ABCDEF";
    char    line[2 * MAX_RECEIVED + 8];
    int16_t i;
    int16_t n;

    n = 0;
    for (i = 0; i < gReceivedCount; i++) {
        line[n]     = kHex[(gReceived[i] >> 4) & 0x0F];
        line[n + 1] = kHex[gReceived[i] & 0x0F];
        n           = (int16_t)(n + 2);
    }
    line[n] = '\0';
    jlLogF("typed=%s", line);
    jlLogFlush();
}


// The mouse predicates packed one nibble per flag, in the order
// drawMouseVerifyRow stamps them. Sampled once per use so the drawn
// row and updateMouseVerifyRow's change check can never disagree.
static uint16_t mouseFlags(void) {
    return (uint16_t)((jlMousePresent()                 ? MOUSE_FLAG_PRESENT : 0) |
                      (jlMouseDown(MOUSE_BUTTON_LEFT)   ? MOUSE_FLAG_LEFT    : 0) |
                      (jlMouseDown(MOUSE_BUTTON_RIGHT)  ? MOUSE_FLAG_RIGHT   : 0) |
                      (jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE  : 0));
}


static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
    int16_t  col;
    int16_t  row;
    jlKeyE key;
    bool     lit;

    for (row = 0; row < GRID_ROWS; row++) {
        for (col = 0; col < GRID_COLS; col++) {
            key = gKeyGrid[row][col];
            if (key == KEY_NONE) {
                continue;
            }
            lit = cellTargetLit(col, row, cursorCol, cursorRow);
            if (lit == gCellLit[row][col]) {
                continue;
            }
            // drawCell marks the cell's rect dirty; jlStagePresent
            // flushes that one band.
            drawCell(screen, col, row, lit);
            jlStagePresent();
            gCellLit[row][col] = lit;
        }
    }
}


// Pop everything the user typed since last frame, apply it to the
// text line (Backspace deletes, Return clears, Escape is the quit key
// and types nothing), and refresh the strip when anything changed.
static void processTypedChars(jlSurfaceT *screen) {
    int  ch;
    bool changed;

    changed = false;
    while ((ch = jlInputGetChar()) != -1) {
        if (gReceivedCount < MAX_RECEIVED) {
            gReceived[gReceivedCount] = (uint8_t)ch;
            gReceivedCount            = (int16_t)(gReceivedCount + 1);
        }
        changed = true;
        if (ch == JL_CHAR_BACKSPACE) {
            if (gLineLen > 0) {
                gLineLen = (int16_t)(gLineLen - 1);
            }
        } else if (ch == JL_CHAR_RETURN) {
            gLineLen = 0;
        } else if (ch == JL_CHAR_ESCAPE || ch == JL_CHAR_TAB) {
            // Nothing to render for these.
        } else if (gLineLen < MAX_LINE) {
            gLine[gLineLen] = (char)ch;
            gLineLen        = (int16_t)(gLineLen + 1);
        }
    }
    if (!changed) {
        return;
    }
    redrawTextStrip(screen);
    jlStagePresent();
}


// Repaint everything below the key grid: the prompt, the current text
// line, and the verification scanline. Shared by the typed-character
// refresh and the cursor-erase path (which may stamp background over
// this region).
static void redrawTextStrip(jlSurfaceT *screen) {
    gLine[gLineLen] = '\0';
    jlFillRect(screen, 0, TEXT_STRIP_Y, SURFACE_WIDTH, SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND);
    drawTextLine(screen, TEXT_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT);
    drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR);
    drawVerifyRow(screen);
}


// Stamp a value into consecutive pixels as nibbles, high nibble
// first -- the encoding both verification rows and every emulator
// harness that decodes them share.
static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles) {
    int16_t i;
    int16_t shift;

    for (i = 0; i < nibbles; i++) {
        shift = (int16_t)(4 * (nibbles - 1 - i));
        jlDrawPixel(screen, (int16_t)(x + i), y, (uint8_t)((value >> shift) & 0x0F));
    }
}


// Erase the previous cursor (by redrawing the cell that held it) and
// stamp the new cursor at the current mouse position. Both rects are
// presented; if the cursor stayed inside the same cell only one rect
// pair is touched, so steady-state cost is small.
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
    int16_t mouseX;
    int16_t mouseY;

    mouseX = jlMouseX();
    mouseY = jlMouseY();

    if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
        if (gLastCursorCol != CELL_NONE) {
            drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
        } else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
            // Old cursor was in a gap region. Stamp background over it,
            // then repair the text strip / verify row if the stamp
            // reached into them.
            jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
            if ((int16_t)(gLastCursorY + CURSOR_H) > TEXT_STRIP_Y) {
                redrawTextStrip(screen);
            }
        }
    }

    drawCursor(screen, mouseX, mouseY);
    // All draw calls above marked their rects dirty; one jlStagePresent
    // flushes the union (cursor erase + cursor draw).
    jlStagePresent();

    gLastCursorX   = mouseX;
    gLastCursorY   = mouseY;
    gLastCursorCol = cursorCol;
    gLastCursorRow = cursorRow;
}


// Re-stamp the mouse verification row only when the reported state
// changed since the last stamp, so idle frames stay draw-free.
static void updateMouseVerifyRow(jlSurfaceT *screen) {
    int16_t  x;
    int16_t  y;
    uint16_t flags;

    x     = jlMouseX();
    y     = jlMouseY();
    flags = mouseFlags();
    if (x == gLastMouseRowX && y == gLastMouseRowY && flags == gLastMouseRowFlags) {
        return;
    }
    drawMouseVerifyRow(screen);
    jlStagePresent();
    gLastMouseRowX     = x;
    gLastMouseRowY     = y;
    gLastMouseRowFlags = flags;
}


int main(void) {
    jlConfigT  config;
    jlSurfaceT    *screen;
    int16_t      cursorCol;
    int16_t      cursorRow;

    config.codegenBytes = 8 * 1024;
    config.audioBytes   = 64UL * 1024;

    if (!jlInit(&config)) {
        fprintf(stderr, "jlInit failed: %s\n", jlLastError());
        return 1;
    }

    screen = jlStageGet();
    if (screen == NULL) {
        fprintf(stderr, "jlStageGet returned NULL\n");
        jlShutdown();
        return 1;
    }

    buildPalette(screen);
    jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
    initialPaint(screen);
    jlInputPoll();

    for (;;) {
        jlInputPoll();
        if (jlKeyPressed(KEY_ESCAPE)) {
            break;
        }
        cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
        presentChangedCells(screen, cursorCol, cursorRow);
        updateCursor(screen, cursorCol, cursorRow);
        updateMouseVerifyRow(screen);
        processTypedChars(screen);
    }

    logTypedHistory();
    jlShutdown();
    return 0;
}
