"""EEPROM model — exi domain.

Emulates the MX98730EC's 93C46 serial EEPROM.

93C46 protocol (Microwire, bit-bang)
-------------------------------------
CS=1 activates the device.
Data clocked on rising SK edge, 9-bit header then data:
  Bit 0: start (always 1)
  Bit 1: opcode MSB }  READ = 10
  Bit 2: opcode LSB }
  Bits 3–8: 6-bit address (MSB first)

After the 9th rising SK the DO line presents the MSB of the 16-bit word.
Each subsequent rising SK advances one bit (MSB→LSB).

Shift register `shift_in` convention
--------------------------------------
`Cat(di_s, shift_in[:-1])` places di_s at bit 0 and shifts existing bits up.
After N edges:
  shift_in[N-1] = first bit received (start)
  shift_in[0]   = last bit received so far

At bit_ctr==8 (after 8 edges, receiving 9th on di_s):
  shift_in[7] = start          (bit 0)
  shift_in[6] = opcode MSB     (bit 1)
  shift_in[5] = opcode LSB     (bit 2)
  shift_in[4:0] = addr[5:1]    (bits 3–7, MSB first→LSB first in register)
  di_s          = addr[0]      (bit 8)

  opcode  = Cat(shift_in[5], shift_in[6])   → 0b10 = READ
  address = Cat(di_s, shift_in[0:5])        → addr[0..5]

EEPROM content (64 × 16-bit words)
-------------------------------------
Words 0–2 hold the source MAC address (Nintendo OUI 00:09:BF:AA:BB:CC).
The GC BBA driver reads words 0–3 then copies to PAR0–5.
"""

from amaranth import *
from amaranth.lib.cdc import FFSynchronizer

__all__ = ["EEPROMModel"]

_EEPROM_WORDS = [
    0x0009,   # word 0: PAR0=0x00, PAR1=0x09
    0xBFAA,   # word 1: PAR2=0xBF, PAR3=0xAA
    0xBBCC,   # word 2: PAR4=0xBB, PAR5=0xCC
    0x0000,   # word 3: checksum placeholder
]
_EEPROM_WORDS += [0x0000] * (64 - len(_EEPROM_WORDS))

_OP_READ = 0b10   # opcode for READ


class EEPROMModel(Elaboratable):
    """93C46 serial EEPROM model in the exi domain (read-only).

    Ports
    -----
    sk / cs / di : bit-bang inputs (raw async; synchronized internally)
    do           : serial data output
    """

    def __init__(self):
        self.sk = Signal()
        self.cs = Signal()
        self.di = Signal()
        self.do = Signal()

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

        words = Array([Signal(16, init=v, name=f"e{i}") for i, v in enumerate(_EEPROM_WORDS)])

        # ── Input synchronization (async → exi, 2 stages) ────────────────
        sk_s = Signal()
        cs_s = Signal()
        di_s = Signal()
        m.submodules.sync_sk = FFSynchronizer(self.sk, sk_s, o_domain="exi")
        m.submodules.sync_cs = FFSynchronizer(self.cs, cs_s, o_domain="exi")
        m.submodules.sync_di = FFSynchronizer(self.di, di_s, o_domain="exi")

        sk_prev = Signal()
        m.d.exi += sk_prev.eq(sk_s)
        rising_sk = Signal()
        m.d.comb += rising_sk.eq(sk_s & ~sk_prev)

        # ── State ─────────────────────────────────────────────────────────
        shift_in  = Signal(9)
        bit_ctr   = Signal(4)    # 0..8 during header receive

        shift_out = Signal(16)   # data word being shifted out MSB-first
        out_ctr   = Signal(4)    # 0..15, counts bits shifted out
        in_read   = Signal()     # 1 while outputting a word

        # DO is combinatorial: MSB of shift_out while in read-out phase
        m.d.comb += self.do.eq(Mux(in_read, shift_out[15], 0))

        with m.If(~cs_s):
            m.d.exi += bit_ctr.eq(0)
            m.d.exi += in_read.eq(0)
            m.d.exi += out_ctr.eq(0)

        with m.Elif(rising_sk):
            with m.If(in_read):
                # Shift out next bit (MSB first: left shift, zero into LSB)
                m.d.exi += shift_out.eq(Cat(0, shift_out[:-1]))
                with m.If(out_ctr == 15):
                    m.d.exi += in_read.eq(0)
                    m.d.exi += out_ctr.eq(0)
                with m.Else():
                    m.d.exi += out_ctr.eq(out_ctr + 1)

            with m.Else():
                # Shift di_s in at bit 0 (existing bits move up)
                m.d.exi += shift_in.eq(Cat(di_s, shift_in[:-1]))
                m.d.exi += bit_ctr.eq(bit_ctr + 1)

                with m.If(bit_ctr == 8):
                    # 9th bit (di_s = addr[0]) arrives.
                    # shift_in[7] = start, [6]=op_MSB, [5]=op_LSB, [4:0]=addr[5:1]
                    op   = Cat(shift_in[5], shift_in[6])    # 0b10 for READ
                    adr  = Cat(di_s, shift_in[0:5])          # addr[0..5]
                    with m.If(op == _OP_READ):
                        m.d.exi += shift_out.eq(words[adr])
                        m.d.exi += in_read.eq(1)
                        m.d.exi += out_ctr.eq(0)

        return m


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

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

    dut = EEPROMModel()
    errors = []

    HALF = 6   # exi-domain ticks per SK half-period (much longer than sync latency)

    async def eeprom_read(ctx, addr):
        """93C46 READ at 6-bit address; returns 16-bit word.

        DO is read BEFORE each rising SK edge, since in_read=1 causes
        shift_out[15] to be valid between edges.  After 16 reads the full
        16-bit word is assembled MSB-first.
        """
        ctx.set(dut.cs, 1)
        ctx.set(dut.sk, 0)
        await ctx.tick("exi").repeat(HALF)

        # Transmit 9 bits: start(1) + opcode READ(10) + addr[5:0] MSB-first
        bits = [1, 1, 0]
        for a in range(5, -1, -1):
            bits.append((addr >> a) & 1)

        for bit in bits:
            ctx.set(dut.di, bit)
            ctx.set(dut.sk, 1)   # rising edge: DUT latches bit
            await ctx.tick("exi").repeat(HALF)
            ctx.set(dut.sk, 0)
            await ctx.tick("exi").repeat(HALF)

        # After 9th falling SK: in_read=1, shift_out=word[addr], do=MSB.
        # Read DO before each rising edge (it is valid in the LOW phase).
        result = 0
        for _ in range(16):
            result = (result << 1) | ctx.get(dut.do)   # sample before rising SK
            ctx.set(dut.sk, 1)
            await ctx.tick("exi").repeat(HALF)
            ctx.set(dut.sk, 0)
            await ctx.tick("exi").repeat(HALF)

        ctx.set(dut.cs, 0)
        await ctx.tick("exi").repeat(HALF)
        return result

    async def testbench(ctx):
        await ctx.tick("exi").repeat(4)
        ctx.set(dut.cs, 0)
        ctx.set(dut.sk, 0)
        ctx.set(dut.di, 0)
        await ctx.tick("exi").repeat(4)

        w0 = await eeprom_read(ctx, 0)
        print(f"T1 word 0 = 0x{w0:04X}  (expected 0x0009)")
        if w0 != 0x0009:
            errors.append(f"T1: word 0 = 0x{w0:04X}, expected 0x0009")

        w1 = await eeprom_read(ctx, 1)
        print(f"T2 word 1 = 0x{w1:04X}  (expected 0xBFAA)")
        if w1 != 0xBFAA:
            errors.append(f"T2: word 1 = 0x{w1:04X}, expected 0xBFAA")

        w2 = await eeprom_read(ctx, 2)
        print(f"T3 word 2 = 0x{w2:04X}  (expected 0xBBCC)")
        if w2 != 0xBBCC:
            errors.append(f"T3: word 2 = 0x{w2:04X}, expected 0xBBCC")

        # T4: word 3 → 0x0000
        w3 = await eeprom_read(ctx, 3)
        print(f"T4 word 3 = 0x{w3:04X}  (expected 0x0000)")
        if w3 != 0x0000:
            errors.append(f"T4: word 3 = 0x{w3:04X}, expected 0x0000")

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

    with sim.write_vcd("EEPROMModel.vcd"):
        sim.run()

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