from amaranth import *
from i2c import *
from amlib.io.i2c import *
from amaranth.lib.io import pin_layout
from tests import BaseTestClass, provide_testcase_name


__all__ = ["i2c_layout", "I2CBusSimulator", "TestHarness", "TestCSROperation"]


class TestHarness(Elaboratable):
    def __init__(self):
        self.i2c = I2CBusSimulator()

        self.uut = I2C(10_000_000, 100_000, self.i2c.create_interface())
        self.i2c_target = I2CTarget(self.i2c.create_interface())

        self.start_latch = Signal()
        self.clear_start = Signal()


    def elaborate(self, platform):
        assert platform is None

        m = Module()

        m.submodules.i2c = self.i2c
        m.submodules.uut = self.uut
        m.submodules.i2c_target = self.i2c_target

        m.d.comb += self.i2c_target.address.eq(0xAA >> 1)
        # Always ACK
        m.d.comb += self.i2c_target.ack_o.eq(1)

        with m.If(self.i2c_target.start):
            m.d.sync += self.start_latch.eq(self.i2c_target.start)

        with m.If(self.clear_start):
            m.d.sync += self.start_latch.eq(0)

        return m


class TestCSROperation(BaseTestClass):
    def setUp(self):
        self.harness = TestHarness()


    # NOTE So ideally there are more test cases... but the initiator itself is well tested,
    # and we only really need it to work for a limited set of use cases, so exhaustive testing
    # isn't a huge deal. As well, we can cover all valid uses of the signals with one test.
    @provide_testcase_name
    def test_operation(self, test_name):
        def test():
            #send start (and set ACK)
            yield from self._write_csr(self.harness.uut.bus, 0, 1 + (1 << 4) + (1 << 5))

            yield from self._wait_for_signal(self.harness.uut._initiator.busy, require_edge=True)

            # Set data
            yield from self._write_csr(self.harness.uut.bus, 2, 0xAA)
            # Write data
            yield from self._write_csr(self.harness.uut.bus, 0, 1 << 2)

            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # First byte has been written
            did_start = yield self.harness.start_latch
            self.assertTrue(did_start)

            did_ack = yield self.harness.uut._initiator.ack_o
            self.assertTrue(did_ack)

            # Write data again
            yield from self._write_csr(self.harness.uut.bus, 0, 1 << 2)

            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # Repeated start
            yield from self._write_csr(self.harness.uut.bus, 0, 1)
            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # Write read thing
            yield from self._write_csr(self.harness.uut.bus, 2, 0xAB) # Set R/W bit for a read
            yield from self._write_csr(self.harness.uut.bus, 0, 1 << 2)
            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # Read
            yield from self._write_csr(self.harness.uut.bus, 0, 1 << 3)
            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # Stop
            yield from self._write_csr(self.harness.uut.bus, 0, 1 << 1)
            yield from self._wait_for_signal(self.harness.uut._initiator.busy)

            # I just feel weird seeing it cut out *right* at the end
            for i in range(500):
                yield Tick()

        self._run_test(test, test_name)


i2c_layout = [
    ("sda", pin_layout(1, "io")),
    ("scl", pin_layout(1, "io")),
]


class I2CBusSimulator(Elaboratable):
    def __init__(self):
        self.interfaces = []
        self.sda = Signal()
        self.scl = Signal()


    def elaborate(self, target):
        assert target is None, "This bus simulator should never be used in real hardware!"

        n = len(self.interfaces)

        m = Module()

        m.d.comb += self.sda.eq(1)
        m.d.comb += self.scl.eq(1)

        # TODO maybe output a bus contention signal?
        # First interfaces get priority over interfaces added after
        for i in reversed(range(n)):
            # Emulate bus drivers
            with m.If(self.interfaces[i].sda.oe):
                m.d.comb += self.sda.eq(self.interfaces[i].sda.o)
            with m.If(self.interfaces[i].scl.oe):
                m.d.comb += self.scl.eq(self.interfaces[i].scl.o)
                pass

            # Connect inputs to bus value
            m.d.comb += [
                self.interfaces[i].sda.i.eq(self.sda),
                self.interfaces[i].scl.i.eq(self.scl),
            ]

        return m


    def create_interface(self) -> Record:
        new_interface = Record(i2c_layout)
        self.interfaces.append(new_interface)
        return new_interface

