#include <stdint.h>

#define USART0_STAT  (*(volatile uint32_t*)0x4001381C)
#define USART0_RDATA (*(volatile uint32_t*)0x40013824)
#define USART0_TDATA (*(volatile uint32_t*)0x40013828)
#define USART0_CTL2  (*(volatile uint32_t*)0x40013808)
#define RBNE_BIT     (1u << 5)
#define TBE_BIT      (1u << 7)
#define DENR_BIT     (1u << 6)
#define DENT_BIT     (1u << 7)
#define TC_BIT       (1u << 6)

#define GPIOB_BOP    (*(volatile uint32_t*)0x48000418)
#define GPIOB_BC     (*(volatile uint32_t*)0x48000428)
#define FAN_PIN      0x200u                              /* PB9 */
#define FAN_STATE    (*(volatile uint8_t*)0x20000115)
#define FAN_ON_LEVEL 20

#define SETPOINT_BUF  ((volatile uint8_t*)0x2000006e)
#define LIVE_BUF      ((volatile uint8_t*)0x20000068)
#define SCHEDULE_MODE (*(volatile uint8_t*)0x20000110)
#define TEMPERATURE   (*(volatile uint16_t*)0x2000010c)  /* degC x10, NTC on ADC ch0 */
#define MANUAL_MODE   1
#define SCHEDULE_RUNS 0          /* 0/2/3 all let the fixture's scheduler run */

#define CMD_READBACK  0xB0
#define CMD_RELEASE   0xB2
#define CMD_SET_TMO   0xB3       /* followed by 2 bytes, little-endian, in ticks */

/* Failsafe. If nothing commands a channel for this long, hand control back
   to the fixture's own sunrise/sunset schedule. Without it a dead ESP32
   leaves the lights frozen at whatever was last commanded, which over a
   reef tank is worse than falling back to a dumb schedule.
   The stock main loop calls us from behind a 1000 ms gate (the `elapsed`
   helper at 0x08003d94), so one tick is one second and a uint16 covers
   just over 18 hours. The tick counter is in the readback frame so the
   rate can be confirmed on real hardware, and 0xB3 retunes this at
   runtime so it needs no reflash. */
#define DEFAULT_TMO 3600u        /* ticks ~= seconds: 1 hour */

/* Burst drain. The USART holds ONE byte and we are called at 1 Hz, so a
   naive "read one byte per call" forces the controller to space every
   single byte more than a second apart -- a six-channel write took 13s.
   Instead, once a byte is waiting, keep consuming for a short window so a
   whole back-to-back burst lands in one poll. At 115200 a byte takes 87us;
   the idle window must comfortably exceed that, and the byte cap stops
   line noise from holding the fixture's main loop. Costs nothing when
   idle: we only enter the loop if a byte is already waiting. */
#define BURST_IDLE_SPIN 8000u    /* ~0.6ms at 72MHz, >> one 87us byte time */
#define BURST_MAX_BYTES 40u

/* Our state lives above the fixture's stack top (initial SP = 0x20001310);
   nothing in the stock firmware references SRAM above that. Its startup code
   zeroes only its OWN .bss, so ours comes up as garbage after a cold boot --
   hence the cookie. */
#define INIT_COOKIE 0x5A7F3C01u

static volatile uint32_t cookie;
static volatile uint32_t rb_count;
static volatile uint8_t  awaiting_value;
static volatile uint8_t  pending_channel;
static volatile uint32_t ticks;
static volatile uint32_t last_cmd_tick;
static volatile uint16_t tmo;

static void tx_byte(uint8_t b)
{
    uint32_t spin = 20000;       /* bounded: never hang the fixture's main loop */
    while (!(USART0_STAT & TBE_BIT) && --spin) { }
    if (spin) USART0_TDATA = b;
}

static void handle_byte(uint8_t b)
{
    uint8_t i;

    /* 0xB0: status. Frame is [0xB0][len][payload...][xor of payload].
       The length byte lets the far end tolerate this frame growing. */
    if (b == CMD_READBACK) {
        uint8_t ck = 0, v;
        rb_count++;             /* observable proof this branch ran */
        /* A readback proves the controller is alive and talking, so it pets
           the failsafe. Petting only on channel writes would misfire: the
           controller sends those only when a value actually changes, and a
           midday plateau can go hours without one. */
        last_cmd_tick = ticks;
        uint16_t t = TEMPERATURE;
        tx_byte(CMD_READBACK);
        tx_byte(21);                                  /* payload + checksum */
        #define P(x) do { v = (uint8_t)(x); ck ^= v; tx_byte(v); } while (0)
        P(SCHEDULE_MODE);
        P(FAN_STATE);
        P(t & 0xFF); P(t >> 8);                       /* degC x10, little-endian */
        P(ticks & 0xFF); P(ticks >> 8); P(ticks >> 16); P(ticks >> 24);
        for (i = 0; i < 6; i++) P(SETPOINT_BUF[i]);
        for (i = 0; i < 6; i++) P(LIVE_BUF[i]);
        #undef P
        tx_byte(ck);
        /* Trailer. The fixture interleaves its own log output on this TX
           line, and whatever lands immediately after our last byte can
           displace it -- observed as 'I' (0x49, from "IR RAW[...]") sitting
           where the checksum should be. These two bytes absorb that, and
           double as a harmless line terminator in the log stream. The
           receiver reads 2+len bytes and ignores anything after. */
        tx_byte(0x0D);
        tx_byte(0x0A);
        return;
    }

    /* 0xB2: hand control back to the fixture's own schedule, now. */
    if (b == CMD_RELEASE) {
        SCHEDULE_MODE = SCHEDULE_RUNS;
        awaiting_value = 0;
        return;
    }

    /* 0xB3 nn nn: set the failsafe timeout in ticks (~seconds), little-endian */
    if (b == CMD_SET_TMO) { awaiting_value = 2; return; }
    if (awaiting_value == 2) { tmo = b;        awaiting_value = 3; return; }
    if (awaiting_value == 3) { tmo |= b << 8;  awaiting_value = 0; return; }

    if (b >= 0xA0 && b <= 0xA5) {
        pending_channel = b - 0xA0;
        awaiting_value = 1;
    } else if (awaiting_value == 1) {
        SETPOINT_BUF[pending_channel] = b;
        awaiting_value = 0;
        SCHEDULE_MODE = MANUAL_MODE;
        last_cmd_tick = ticks;                        /* pet the failsafe */
    }
}

void uart_cmd_poll_impl(void)
{
    uint8_t i, on = 0;

#if RELEASE_DISPLAY_PINS
    *(volatile uint32_t*)0x48000000 &= ~0xFCC0u;   /* PA3/5/6/7 -> input */
#endif

    if (cookie != INIT_COOKIE) {
        cookie = INIT_COOKIE;
        awaiting_value = 0;
        pending_channel = 0;
        ticks = 0;
        last_cmd_tick = 0;
        rb_count = 0;
        tmo = DEFAULT_TMO;
    }
    ticks++;

    /* fan control -- the original function's behaviour, restored */
    for (i = 0; i < 6; i++)
        if (SETPOINT_BUF[i] >= FAN_ON_LEVEL) { on = 1; break; }
    if (on) GPIOB_BOP = FAN_PIN; else GPIOB_BC = FAN_PIN;
    FAN_STATE = on;

    /* failsafe handback */
    if (SCHEDULE_MODE == MANUAL_MODE && (ticks - last_cmd_tick) >= tmo)
        SCHEDULE_MODE = SCHEDULE_RUNS;

    USART0_CTL2 &= ~DENR_BIT;
    if (!(USART0_STAT & RBNE_BIT)) return;      /* nothing waiting: no spin */

    {
        uint32_t idle = 0;
        uint8_t count = 0;
        for (;;) {
            if (USART0_STAT & RBNE_BIT) {
                handle_byte((uint8_t)USART0_RDATA);
                if (++count >= BURST_MAX_BYTES) break;
                idle = 0;
            } else if (++idle >= BURST_IDLE_SPIN) {
                break;
            }
        }
    }
}
