// 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 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>

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

#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)

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 initialPaint(jlSurfaceT *screen);
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);

// 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;


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

    for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
        colors[i] = 0x0000;
    }
    colors[COLOR_BACKGROUND] = 0x0000;  // black
    colors[COLOR_UNLIT]      = 0x0333;  // dark gray
    colors[COLOR_LIT]        = 0x00F0;  // bright green
    colors[COLOR_CURSOR]     = 0x0FFF;  // white

    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);
}


static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
    jlFillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
}


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;
        }
    }
    jlStagePresent();
}


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;
        }
    }
}


// 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.
            jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
        }
    }

    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;
}


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);
    }

    jlShutdown();
    return 0;
}
