from amaranth import *
from amaranth.sim import Simulator


class SyncFF(Elaboratable):
    """Width-N multi-flop synchronizer from `src_domain` to `dst_domain`.

    Use when the source is a level signal that may be stable for multiple destination
    cycles. Not suitable for single-cycle pulses (use TogglePulseSync instead).
    """

    def __init__(self, width=1, src_domain="src", dst_domain="dst"):
        self.width = width
        self.src_domain = src_domain
        self.dst_domain = dst_domain
        self.src = Signal(self.width)
        self.dst = Signal(self.width)

    def elaborate(self, platform):
        m = Module()
        reg_src = Signal(self.width)
        ff0 = Signal(self.width)
        ff1 = Signal(self.width)

        m.d[self.src_domain] += reg_src.eq(self.src)
        m.d[self.dst_domain] += ff0.eq(reg_src)
        m.d[self.dst_domain] += ff1.eq(ff0)
        m.d.comb += self.dst.eq(ff1)

        return m


class TogglePulseSync(Elaboratable):
    """Reliable pulse transfer from `src_domain` into `dst_domain`.

    - Source toggles `toggle` whenever an event occurs.
    - Destination synchronizes the toggle and detects edges.
    Guarantees ordering and no lost pulses for single-bit events.
    """

    def __init__(self, src_domain="src", dst_domain="dst"):
        self.src_domain = src_domain
        self.dst_domain = dst_domain
        self.src_pulse = Signal()
        self.dst_pulse = Signal()

    def elaborate(self, platform):
        m = Module()
        toggle = Signal()
        sync0 = Signal()
        sync1 = Signal()
        prev = Signal()
        edge = Signal()

        # Source domain: flip the toggle when a pulse arrives
        m.d[self.src_domain] += If(self.src_pulse, toggle.eq(~toggle))

        # Destination domain: two-flop synchronize the toggle
        m.d[self.dst_domain] += sync0.eq(toggle)
        m.d[self.dst_domain] += sync1.eq(sync0)

        # Detect the change in the destination domain
        m.d[self.dst_domain] += edge.eq(sync1 ^ prev)
        m.d[self.dst_domain] += prev.eq(sync1)
        m.d.comb += self.dst_pulse.eq(edge)

        return m


def _sim_toggle_pulse():
    """Simple simulation that drives pulses on the source domain and prints detections on the destination domain."""

    top = Module()
    t = TogglePulseSync(src_domain="src", dst_domain="dst")
    top.submodules.t = t

    sim = Simulator(top)
    # Create two asynchronous clocks (periods chosen arbitrarily for the sim)
    sim.add_clock(1e-6, domain="src")
    sim.add_clock(1.5e-6, domain="dst")

    def process():
        # Wait a little, then generate three source pulses at different phases
        for _ in range(5):
            yield

        for i in range(3):
            yield t.src_pulse.eq(1)
            yield
            yield t.src_pulse.eq(0)
            # let the domains run for a few cycles
            for _ in range(10):
                dp = (yield t.dst_pulse)
                if dp:
                    print(f"dst detected pulse at sim tick")
                yield

        # run a bit longer to observe behavior
        for _ in range(20):
            yield

    sim.add_sync_process(process, domain="src")
    sim.run_until(100e-6)


if __name__ == "__main__":
    _sim_toggle_pulse()
