"""StatusPanel — 5-LED / 3-button bring-up panel (sync domain).

A development/diagnostics front panel for the iCEbreaker LED+button PMOD.  It
turns the device's internal liveness signals into something you can watch on a
real GameCube during bring-up, and gives three buttons for manual control.

LEDs (logical, active-high; set `led_active_low=True` if the board sinks current)
    led[0]  heartbeat   — ~1–2 Hz blink: clock alive, bitstream loaded
    led[1]  exi_active  — stretched `cs_active`: the GC is talking on EXI
    led[2]  rx_act      — stretched `rx_pulse`: a packet arrived from the net
    led[3]  tx_act      — stretched `tx_pulse`: a packet went out
    led[4]  ready       — `ready` level (e.g. ethernet init complete)

Buttons (raw pin level; `btn_active_low=True` for the usual pull-up wiring)
    btn[0]  eth_rst  — while held, drive `eth_rst_n` low (reset the ethernet chip)
    btn[1]  reinit   — on press, emit a one-cycle `reinit` pulse (force re-init)
    btn[2]  freeze   — toggle: latch the rx/tx activity LEDs so a single one-shot
                       blink sticks until you unfreeze (catch a lone packet)

Single-cycle events (`rx_pulse`/`tx_pulse`) are stretched to ~`stretch_cycles`
so the eye can see them; `cs_active` is a level that is re-triggered while high.
Buttons are debounced (`debounce_cycles` stable samples) — same idea as
`rebbarb/debouncer.py`, inlined here to keep this module self-contained.
"""

from amaranth import *

__all__ = ["StatusPanel"]


class StatusPanel(Elaboratable):
    def __init__(self, hb_bit=23, stretch_cycles=1_440_000,
                 debounce_cycles=240_000, led_active_low=False,
                 btn_active_low=True):
        # hb_bit: heartbeat = bit `hb_bit` of a free-running counter
        #   (24 MHz / 2**23 ≈ 1.4 Hz).  stretch_cycles ≈ 60 ms at 24 MHz.
        self._hb_bit  = hb_bit
        self._stretch = stretch_cycles
        self._deb     = debounce_cycles
        self._led_inv = led_active_low
        self._btn_inv = btn_active_low

        # Status inputs (sync domain)
        self.cs_active = Signal()   # level: EXI transaction in progress
        self.rx_pulse  = Signal()   # 1-cycle: frame received
        self.tx_pulse  = Signal()   # 1-cycle: frame sent
        self.ready     = Signal()   # level: ethernet ready

        # Raw button inputs (from pins)
        self.btn = Signal(3)

        # Outputs
        self.led       = Signal(5)
        self.eth_rst_n = Signal(init=1)   # btn0 held → 0
        self.reinit    = Signal()         # btn1 press → 1-cycle pulse

    def elaborate(self, platform):
        m = Module()

        # ── Heartbeat ────────────────────────────────────────────────────
        hb = Signal(self._hb_bit + 1)
        m.d.sync += hb.eq(hb + 1)
        heartbeat = hb[self._hb_bit]

        # ── Button conditioning (normalise polarity → debounce) ──────────
        braw = Signal(3)
        m.d.comb += braw.eq(self.btn ^ C(0b111 if self._btn_inv else 0, 3))

        bdeb = Signal(3)
        for i in range(3):
            cnt = Signal(range(self._deb + 1), name=f"deb_cnt{i}")
            with m.If(braw[i] == bdeb[i]):
                m.d.sync += cnt.eq(0)              # stable: hold
            with m.Else():
                m.d.sync += cnt.eq(cnt + 1)        # changing: count stable samples
                with m.If(cnt == self._deb - 1):
                    m.d.sync += [bdeb[i].eq(braw[i]), cnt.eq(0)]

        # btn0: hold → ethernet reset asserted (active-low output)
        m.d.comb += self.eth_rst_n.eq(~bdeb[0])

        # btn1: rising edge → reinit pulse
        b1_prev = Signal()
        m.d.sync += b1_prev.eq(bdeb[1])
        m.d.comb += self.reinit.eq(bdeb[1] & ~b1_prev)

        # btn2: rising edge toggles freeze
        b2_prev = Signal()
        freeze  = Signal()
        m.d.sync += b2_prev.eq(bdeb[2])
        with m.If(bdeb[2] & ~b2_prev):
            m.d.sync += freeze.eq(~freeze)

        # ── Activity stretchers (rx/tx), sticky while frozen ─────────────
        def stretch(pulse, name):
            cnt    = Signal(range(self._stretch + 1), name=f"{name}_cnt")
            sticky = Signal(name=f"{name}_sticky")
            with m.If(pulse):
                m.d.sync += cnt.eq(self._stretch)
                with m.If(freeze):
                    m.d.sync += sticky.eq(1)       # latch a one-shot when frozen
            with m.Elif(cnt != 0):
                m.d.sync += cnt.eq(cnt - 1)
            with m.If(~freeze):
                m.d.sync += sticky.eq(0)           # clear sticky when unfrozen
            return (cnt != 0) | sticky

        rx_led = stretch(self.rx_pulse, "rx")
        tx_led = stretch(self.tx_pulse, "tx")

        # ── cs_active: level → stretched so brief transactions are visible ─
        cs_cnt = Signal(range(self._stretch + 1))
        with m.If(self.cs_active):
            m.d.sync += cs_cnt.eq(self._stretch)
        with m.Elif(cs_cnt != 0):
            m.d.sync += cs_cnt.eq(cs_cnt - 1)
        cs_led = cs_cnt != 0

        leds = Cat(heartbeat, cs_led, rx_led, tx_led, self.ready)
        m.d.comb += self.led.eq(leds ^ C(0b11111 if self._led_inv else 0, 5))

        return m


# ── Testbench ─────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import sys
    from amaranth.sim import Simulator, Period

    # Tiny parameters so the timed behaviours are observable in a short sim.
    dut = StatusPanel(hb_bit=3, stretch_cycles=8, debounce_cycles=3)
    errors = []

    async def settle(ctx, n=1):
        await ctx.tick("sync").repeat(n)

    async def testbench(ctx):
        ctx.set(dut.btn, 0b111)        # active-low idle (no press)
        await settle(ctx, 4)

        # T1: heartbeat toggles (bit 3 of the counter flips every 8 cycles)
        h0 = ctx.get(dut.led) & 1
        await settle(ctx, 8)
        h1 = ctx.get(dut.led) & 1
        if h0 == h1:
            errors.append("T1 heartbeat did not toggle over 8 cycles")
        print(f"T1 heartbeat toggled: {h0} -> {h1}")

        # T2: rx pulse lights led[2] and it stretches, then clears
        ctx.set(dut.rx_pulse, 1)
        await settle(ctx, 1)
        ctx.set(dut.rx_pulse, 0)
        await settle(ctx, 1)
        on = (ctx.get(dut.led) >> 2) & 1
        if not on:
            errors.append("T2 rx LED not lit after pulse")
        await settle(ctx, 12)          # > stretch_cycles
        off = (ctx.get(dut.led) >> 2) & 1
        if off:
            errors.append("T2 rx LED did not clear after stretch")
        print(f"T2 rx LED: on={on} then off={not off}")

        # T3: ready level drives led[4]
        ctx.set(dut.ready, 1)
        await settle(ctx, 1)
        if not ((ctx.get(dut.led) >> 4) & 1):
            errors.append("T3 ready LED not lit")
        ctx.set(dut.ready, 0)
        print("T3 ready LED follows level")

        # T4: btn0 held (active-low → drive 0) asserts eth_rst_n low after debounce
        ctx.set(dut.btn, 0b110)        # btn0 pressed
        await settle(ctx, 6)           # > debounce
        if ctx.get(dut.eth_rst_n) != 0:
            errors.append("T4 eth_rst_n not asserted while btn0 held")
        ctx.set(dut.btn, 0b111)        # release
        await settle(ctx, 6)
        if ctx.get(dut.eth_rst_n) != 1:
            errors.append("T4 eth_rst_n not released")
        print("T4 btn0 → eth_rst_n hold/release ok")

        # T5: btn1 press emits exactly one reinit pulse
        pulses = 0
        ctx.set(dut.btn, 0b101)        # btn1 pressed
        for _ in range(10):
            await settle(ctx, 1)
            pulses += (ctx.get(dut.reinit) & 1)
        ctx.set(dut.btn, 0b111)
        await settle(ctx, 6)
        if pulses != 1:
            errors.append(f"T5 reinit pulses: got {pulses}, want 1")
        print(f"T5 btn1 → reinit pulses={pulses}")

        # T6: freeze (btn2) makes a single rx pulse stick
        ctx.set(dut.btn, 0b011)        # btn2 press → toggle freeze on
        await settle(ctx, 6)
        ctx.set(dut.btn, 0b111)
        await settle(ctx, 2)
        ctx.set(dut.rx_pulse, 1)       # one-shot while frozen
        await settle(ctx, 1)
        ctx.set(dut.rx_pulse, 0)
        await settle(ctx, 20)          # well past stretch
        stuck = (ctx.get(dut.led) >> 2) & 1
        if not stuck:
            errors.append("T6 frozen rx LED did not stick")
        ctx.set(dut.btn, 0b011)        # toggle freeze off
        await settle(ctx, 6)
        ctx.set(dut.btn, 0b111)
        await settle(ctx, 2)
        cleared = ((ctx.get(dut.led) >> 2) & 1) == 0
        if not cleared:
            errors.append("T6 rx LED did not clear after unfreeze")
        print(f"T6 freeze: stuck={stuck} cleared_after_unfreeze={cleared}")

    sim = Simulator(dut)
    sim.add_clock(Period(MHz=24), domain="sync")
    sim.add_testbench(testbench)
    sim.run()

    if errors:
        print("\nFAILURES:")
        for e in errors:
            print(" ", e)
        sys.exit(1)
    else:
        print("\nAll tests passed.")
