"""Synthesis script for BBATop → iCEbreaker (iCE40UP5K SG48).

Run from workspace root:
    python -m exi_bba.synth              # synthesize only
    python -m exi_bba.synth --flash      # synthesize and flash

This file re-declares IceBreakerPlatform inline so that importing
rebbarb/rebbarb.py (which has a module-level platform.build() call) is avoided.
"""

import os
import subprocess
import sys

from amaranth import *
from amaranth.build import *
from amaranth.vendor import LatticeICE40Platform

from exi_bba.bba_top import BBATop


# ── Platform definition ───────────────────────────────────────────────────
# Pin assignments use the iCEbreaker PMOD connectors as placeholders.
# Replace with actual SP1-interposer pin numbers once PCB is finalised.
#
# PMOD1A (J2): pins 4 2 47 45 / 3 48 46 44  (top/bottom)
# PMOD1B (J3): pins 43 38 34 31 / 42 36 32 28
# PMOD2  (J4): pins 27 25 21 19 / 26 23 20 18
#
# EXI   : CLK=4  MOSI=2  MISO=47  CS_N=45  INT_N=3   (PMOD1A)
# W5100 : indirect parallel bus — 15 pins across PMOD1B + PMOD2.
#   ADDR[1:0]=43 38   DATA[7:0]=34 31 42 36 32 28 27 25
#   CS_N=21  RD_N=19  WR_N=26  INT_N=23  RST_N=20      (pin 18 free)
# Board: tie the W5100's upper address lines A[14:2] to 0 (only A[1:0] wired);
# DATA[7:0] is bidirectional (SB_IO tristate, single shared output-enable).

class IceBreakerPlatform(LatticeICE40Platform):
    device      = "iCE40UP5K"
    package     = "SG48"
    default_clk = "clk12"

    resources = [
        Resource("clk12", 0,
                 Pins("20", dir="i"),
                 Clock(12e6),
                 Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")),

        # EXI interface (GC side, SPI Mode 3) — PMOD1A FPGA pins
        Resource("exi", 0,
                 Subsignal("clk",   Pins("4",  dir="i")),
                 Subsignal("mosi",  Pins("2",  dir="i")),
                 Subsignal("miso",  Pins("47", dir="o")),
                 Subsignal("cs_n",  Pins("45", dir="i")),
                 Subsignal("int_n", Pins("3",  dir="o")),
                 Attrs(IO_STANDARD="SB_LVCMOS")),

        # W5100 indirect parallel bus — PMOD1B + PMOD2 FPGA pins
        Resource("w5100", 0,
                 Subsignal("addr",  Pins("43 38", dir="o")),
                 Subsignal("data",  Pins("34 31 42 36 32 28 27 25", dir="io")),
                 Subsignal("cs_n",  Pins("21", dir="o")),
                 Subsignal("rd_n",  Pins("19", dir="o")),
                 Subsignal("wr_n",  Pins("26", dir="o")),
                 Subsignal("int_n", Pins("23", dir="i")),
                 Subsignal("rst_n", Pins("18", dir="o")),
                 Attrs(IO_STANDARD="SB_LVCMOS")),

        # Bring-up status panel → iCEbreaker ONBOARD parts (dedicated pins, not
        # on any PMOD, so they coexist with EXI + W5100).  LEDR/LEDG are
        # active-low discrete LEDs; BTN_N is the user button.
        # RGB LED (pins 39/40/41) is driven via SB_RGBA_DRV — not declared here
        # as a platform resource (see BBATopSynth.elaborate, status_panel block).
        Resource("ledr", 0, Pins("11", dir="o"), Attrs(IO_STANDARD="SB_LVCMOS")),
        Resource("ledg", 0, Pins("37", dir="o"), Attrs(IO_STANDARD="SB_LVCMOS")),
        Resource("btn",  0, Pins("10", dir="i"), Attrs(IO_STANDARD="SB_LVCMOS")),

        # UART debug console → iCEbreaker FT2232H Channel B (onboard USB-UART).
        # No external hardware needed — the FT2232H is already on the board.
        # On the PC: open the second USB serial port at 115200 8N1.
        #   pin  9 = FPGA TX → FT2232H Channel B RX  (FPGA drives)
        #   pin  6 = FPGA RX ← FT2232H Channel B TX  (FPGA reads)
        Resource("uart", 0,
                 Subsignal("tx", Pins("9",  dir="o")),
                 Subsignal("rx", Pins("6",  dir="i")),
                 Attrs(IO_STANDARD="SB_LVCMOS")),
    ]

    connectors = []

    def toolchain_program(self, products, name):
        iceprog = os.environ.get("ICEPROG", "iceprog")
        with products.extract(f"{name}.bin") as bitstream_filename:
            subprocess.check_call([iceprog, bitstream_filename])


# ── BBATop with platform resource wiring ─────────────────────────────────

class BBATopSynth(BBATop):
    """BBATop with platform pin connections added in elaborate()."""

    def elaborate(self, platform):
        m = super().elaborate(platform)

        if platform is not None:
            exi   = platform.request("exi",   0)
            w5100 = platform.request("w5100", 0)

            m.d.comb += [
                self.exi_clk  .eq(exi.clk.i),
                self.exi_mosi .eq(exi.mosi.i),
                self.exi_cs_n .eq(exi.cs_n.i),
                exi.miso.o    .eq(self.exi_miso),
                exi.int_n.o   .eq(self.int_n),

                # W5100 parallel bus (DATA[7:0] bidirectional via SB_IO)
                w5100.addr.o     .eq(self.w5100_addr),
                w5100.data.o     .eq(self.w5100_data_o),
                w5100.data.oe    .eq(self.w5100_data_oe),
                self.w5100_data_i.eq(w5100.data.i),
                w5100.cs_n.o     .eq(self.w5100_cs_n),
                w5100.rd_n.o     .eq(self.w5100_rd_n),
                w5100.wr_n.o     .eq(self.w5100_wr_n),
                self.w5100_int_n .eq(w5100.int_n.i),
                w5100.rst_n.o    .eq(self.w5100_rst_n),
            ]

            # ── Bring-up status panel → onboard LEDs / button ──────────────
            # All 5 panel LEDs mapped:
            #   LEDG (pin 37)        = led[0] heartbeat
            #   LEDR (pin 11)        = led[1] EXI activity
            #   RGB  (pins 39/40/41) = led[2] rx / led[3] tx / led[4] ready
            # The one onboard button → panel btn[1] (manual re-init).
            if self._status_panel:
                ledr = platform.request("ledr", 0)
                ledg = platform.request("ledg", 0)
                btn  = platform.request("btn",  0)
                led  = self.panel_led

                m.d.comb += [
                    ledg.o.eq(~led[0]),     # heartbeat    (active-low LED)
                    ledr.o.eq(~led[1]),     # EXI activity (active-low LED)
                    # btn[0]/[2] held released (active-low idle = 1)
                    self.panel_btn.eq(Cat(C(1, 1), btn.i, C(1, 1))),
                ]

                # iCEbreaker RGB LED has no series resistors — must use
                # SB_RGBA_DRV (raw pad driver with built-in current source).
                # RGB0=red→rx_act  RGB1=green→tx_act  RGB2=blue→ready
                # Verify colour-to-element mapping against schematic at bring-up.
                m.submodules.rgb_drv = Instance("SB_RGBA_DRV",
                    p_CURRENT_MODE="0b1",
                    p_RGB0_CURRENT="0b000001",
                    p_RGB1_CURRENT="0b000001",
                    p_RGB2_CURRENT="0b000001",
                    i_CURREN=Const(1, 1),
                    i_RGBLEDEN=Const(1, 1),
                    i_RGB0PWM=led[2],
                    i_RGB1PWM=led[3],
                    i_RGB2PWM=led[4],
                    o_RGB0=Signal(name="rgb_r"),
                    o_RGB1=Signal(name="rgb_g"),
                    o_RGB2=Signal(name="rgb_b"),
                )

            # ── UART debug console → FT2232H Channel B ─────────────────────
            if self._uart_console:
                uart = platform.request("uart", 0)
                m.d.comb += [
                    uart.tx.o       .eq(self.uart_tx),
                    self.uart_rx    .eq(uart.rx.i),
                ]

        return m


# ── Entry point ───────────────────────────────────────────────────────────
#
# Seed sweep: nextpnr placement is stochastic.  With ~22% LC utilisation
# routing dominates timing, so different seeds can vary fmax by ±20%.
# Pass --seeds N to try N seeds (default 1, i.e. seed 1 only).
# The build directory is reused across seeds; the final artefact in
# build/top.bin is the result of the last (or best) seed tried.

if __name__ == "__main__":
    do_flash  = "--flash"  in sys.argv
    n_seeds   = next((int(sys.argv[i+1]) for i, a in enumerate(sys.argv)
                      if a == "--seeds"), 1)

    platform = IceBreakerPlatform()
    print(f"Synthesizing BBATop for {platform.device}-{platform.package}  "
          f"(do_program={do_flash}, seeds=1..{n_seeds})")

    best_seed = 1
    best_fmax = 0.0
    for seed in range(1, n_seeds + 1):
        print(f"\n{'='*60}")
        print(f"  Seed {seed}/{n_seeds}")
        print(f"{'='*60}")
        opts = (f"--opt-timing --seed {seed} --timing-allow-fail")
        try:
            platform.build(BBATopSynth(status_panel=True, uart_console=True), do_program=False,
                           verbose=True, nextpnr_opts=opts)
        except Exception as exc:
            # nextpnr exits non-zero even with --timing-allow-fail on some
            # versions; treat as non-fatal timing failure.
            print(f"  [seed {seed}] build exception (timing?): {exc}")

        # Parse fmax from nextpnr log in build/top.tim (if present)
        import glob, re
        tim_files = glob.glob("build/top.tim") + glob.glob("build/*.tim")
        fmax_exi = 0.0
        for tf in tim_files:
            try:
                with open(tf) as f:
                    for line in f:
                        m_ = re.search(
                            r"Max frequency.*exi.*?:\s*([\d.]+)\s*MHz", line)
                        if m_:
                            fmax_exi = float(m_.group(1))
            except OSError:
                pass
        print(f"  [seed {seed}] exi fmax extracted: {fmax_exi:.1f} MHz")
        if fmax_exi > best_fmax:
            best_fmax = fmax_exi
            best_seed = seed

    print(f"\nBest seed: {best_seed}  exi fmax: {best_fmax:.1f} MHz")

    if do_flash:
        print(f"\nFlashing with seed {best_seed}...")
        opts = f"--opt-timing --seed {best_seed} --timing-allow-fail"
        platform.build(BBATopSynth(status_panel=True, uart_console=True), do_program=True,
                       verbose=True, nextpnr_opts=opts)

    print("Done.")
