// Serial add-on demo: an echo terminal + loopback self-test with no text
// assets. It opens the DEFAULT serial port at 9600 8N1 and each frame:
//   - drains received bytes (jlSerialPoll + jlSerialRead), echoes each one
//     straight back out the port, and shows the most recent byte as eight
//     lit/unlit bit cells (MSB left) plus a green RX blink;
//   - sends the ASCII for any letter/digit key pressed, or a banner string on
//     SPACE, with a yellow TX blink.
//
// With a loopback plug (TX tied to RX) it is a self-test: press SPACE and the
// banner streams back in, driving the RX blink and the bit cells. Without one,
// TX still blinks and typing still transmits -- wire a real device to the port
// to see its replies. ESC quits.
//
// Serial is an OPT-IN add-on: it is reached through <joey/serial.h>, which the
// umbrella <joey/joey.h> deliberately does NOT pull in.

#include <stdio.h>

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

#define CELL_W           28
#define CELL_H           40
#define CELL_GAP         4
#define CELLS_X          8
#define BITS_ORIGIN_X    12
#define BITS_ORIGIN_Y    30

#define LAMP_W           52
#define LAMP_H           24
#define LAMP_GAP         8
#define LAMP_Y           110
#define LAMP_TX_X        12
#define LAMP_RX_X        (LAMP_TX_X + LAMP_W + LAMP_GAP)
#define LAMP_LINK_X      (LAMP_RX_X + LAMP_W + LAMP_GAP)

#define BLINK_FRAMES     6

#define COLOR_BACKGROUND 0
#define COLOR_BIT_OFF    1
#define COLOR_BIT_ON     2
#define COLOR_TX_ON      3
#define COLOR_RX_ON      4
#define COLOR_LAMP_IDLE  5
#define COLOR_LINK_OPEN  6
#define COLOR_LINK_SHUT  7

#define SERIAL_BAUD      9600u
#define SERIAL_DATABITS  8u
#define SERIAL_STOPBITS  1u

#define RX_CHUNK         32

static const char gBanner[] = "Hello from JoeyLib serial!\r\n";

// Keys that transmit, paired with the ASCII byte each one sends.
typedef struct KeySendT {
    jlKeyE  key;
    char    ch;
} KeySendT;

static const KeySendT gKeySend[] = {
    { KEY_A, 'A' }, { KEY_B, 'B' }, { KEY_C, 'C' }, { KEY_D, 'D' },
    { KEY_E, 'E' }, { KEY_F, 'F' }, { KEY_G, 'G' }, { KEY_H, 'H' },
    { KEY_I, 'I' }, { KEY_J, 'J' }, { KEY_K, 'K' }, { KEY_L, 'L' },
    { KEY_M, 'M' }, { KEY_N, 'N' }, { KEY_O, 'O' }, { KEY_P, 'P' },
    { KEY_Q, 'Q' }, { KEY_R, 'R' }, { KEY_S, 'S' }, { KEY_T, 'T' },
    { KEY_U, 'U' }, { KEY_V, 'V' }, { KEY_W, 'W' }, { KEY_X, 'X' },
    { KEY_Y, 'Y' }, { KEY_Z, 'Z' },
    { KEY_0, '0' }, { KEY_1, '1' }, { KEY_2, '2' }, { KEY_3, '3' },
    { KEY_4, '4' }, { KEY_5, '5' }, { KEY_6, '6' }, { KEY_7, '7' },
    { KEY_8, '8' }, { KEY_9, '9' },
    { KEY_RETURN, '\r' }
};

#define KEY_SEND_COUNT   ((int16_t)(sizeof(gKeySend) / sizeof(gKeySend[0])))

static int16_t gLastByte = -1;    // most recent received byte, -1 = none yet
static int16_t gTxTimer  = 0;     // frames the TX lamp stays lit
static int16_t gRxTimer  = 0;     // frames the RX lamp stays lit

static void buildPalette(jlSurfaceT *screen);
static void drawBits(jlSurfaceT *screen, int16_t value);
static void drawLamp(jlSurfaceT *screen, int16_t x, bool active, uint8_t activeColor);
static void drawLinkLamp(jlSurfaceT *screen, bool open);
static int16_t pollKeySend(void);
static void sendBytes(const char *bytes, uint16_t len);


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_BIT_OFF]    = 0x0114;  // dim blue
    colors[COLOR_BIT_ON]     = 0x00FF;  // bright cyan
    colors[COLOR_TX_ON]      = 0x0FF0;  // yellow
    colors[COLOR_RX_ON]      = 0x00F0;  // green
    colors[COLOR_LAMP_IDLE]  = 0x0333;  // gray
    colors[COLOR_LINK_OPEN]  = 0x00C0;  // dim green
    colors[COLOR_LINK_SHUT]  = 0x0C00;  // dim red

    jlPaletteSet(screen, 0, colors);
}


// Eight cells, MSB at the left. value < 0 draws all cells "off".
static void drawBits(jlSurfaceT *screen, int16_t value) {
    int16_t  i;

    for (i = 0; i < CELLS_X; i++) {
        int16_t  x   = (int16_t)(BITS_ORIGIN_X + i * (CELL_W + CELL_GAP));
        bool     set = (value >= 0) && ((value & (0x80 >> i)) != 0);

        jlFillRect(screen, x, BITS_ORIGIN_Y, CELL_W, CELL_H, set ? COLOR_BIT_ON : COLOR_BIT_OFF);
    }
}


static void drawLamp(jlSurfaceT *screen, int16_t x, bool active, uint8_t activeColor) {
    jlFillRect(screen, x, LAMP_Y, LAMP_W, LAMP_H, active ? activeColor : COLOR_LAMP_IDLE);
}


static void drawLinkLamp(jlSurfaceT *screen, bool open) {
    jlFillRect(screen, LAMP_LINK_X, LAMP_Y, LAMP_W, LAMP_H, open ? COLOR_LINK_OPEN : COLOR_LINK_SHUT);
}


// Return the ASCII byte for the first transmit key pressed this frame, or -1.
static int16_t pollKeySend(void) {
    int16_t  i;

    for (i = 0; i < KEY_SEND_COUNT; i++) {
        if (jlKeyPressed(gKeySend[i].key)) {
            return (int16_t)(uint8_t)gKeySend[i].ch;
        }
    }
    return -1;
}


static void sendBytes(const char *bytes, uint16_t len) {
    uint16_t  sent;

    sent = 0u;
    // jlSerialWrite may report a short count under flow control; retry the
    // remainder for a few frames' worth of spins so the banner is not clipped.
    while (sent < len) {
        uint16_t  n = jlSerialWrite((const uint8_t *)bytes + sent, (uint16_t)(len - sent));

        if (n == 0u) {
            break;
        }
        sent = (uint16_t)(sent + n);
    }
}


int main(void) {
    jlConfigT        config;
    jlSurfaceT      *screen;
    jlSerialConfigT  serialCfg;
    bool             linkOpen;

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

    serialCfg.baud     = SERIAL_BAUD;
    serialCfg.dataBits = SERIAL_DATABITS;
    serialCfg.stopBits = SERIAL_STOPBITS;
    serialCfg.parity   = JL_SERIAL_PARITY_NONE;
    serialCfg.flow     = JL_SERIAL_FLOW_NONE;
    serialCfg.unit     = 0u;
    linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg);
    if (linkOpen) {
        // Greet on connect -- also a TX heartbeat that a loopback peer can see.
        sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u));
    }

    buildPalette(screen);
    jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
    jlSurfaceClear(screen, COLOR_BACKGROUND);
    drawBits(screen, gLastByte);
    drawLamp(screen, LAMP_TX_X, false, COLOR_TX_ON);
    drawLamp(screen, LAMP_RX_X, false, COLOR_RX_ON);
    drawLinkLamp(screen, linkOpen);
    jlStagePresent();
    jlInputPoll();

    for (;;) {
        uint8_t   rx[RX_CHUNK];
        uint16_t  got;
        int16_t   keyByte;
        bool      txFired;
        bool      dirty;

        jlInputPoll();
        if (jlKeyPressed(KEY_ESCAPE)) {
            break;
        }
        jlSerialPoll();

        txFired = false;
        dirty   = false;

        // Transmit: a banner on SPACE, otherwise the pressed key's ASCII.
        if (jlKeyPressed(KEY_SPACE)) {
            sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u));
            txFired = true;
        } else {
            keyByte = pollKeySend();
            if (keyByte >= 0) {
                char  c = (char)keyByte;

                sendBytes(&c, 1u);
                txFired = true;
            }
        }

        // Receive: show the last byte, echo the whole chunk back out.
        got = jlSerialRead(rx, (uint16_t)RX_CHUNK);
        if (got > 0u) {
            gLastByte = (int16_t)rx[got - 1u];
            drawBits(screen, gLastByte);
            dirty   = true;
            gRxTimer = BLINK_FRAMES;
            sendBytes((const char *)rx, got);   // echo
        }

        if (txFired && gTxTimer == 0) {
            drawLamp(screen, LAMP_TX_X, true, COLOR_TX_ON);
            dirty = true;
        }
        if (txFired) {
            gTxTimer = BLINK_FRAMES;
        }

        // Age the blink lamps, redrawing only on the lit->idle transition.
        if (gTxTimer > 0) {
            gTxTimer--;
            if (gTxTimer == 0) {
                drawLamp(screen, LAMP_TX_X, false, COLOR_TX_ON);
                dirty = true;
            }
        }
        if (gRxTimer > 0) {
            if (got > 0u) {
                drawLamp(screen, LAMP_RX_X, true, COLOR_RX_ON);
                dirty = true;
            }
            gRxTimer--;
            if (gRxTimer == 0) {
                drawLamp(screen, LAMP_RX_X, false, COLOR_RX_ON);
                dirty = true;
            }
        }

        if (dirty) {
            jlStagePresent();
        }
    }

    jlSerialClose();
    jlShutdown();
    return 0;
}
