from amaranth import *
from amaranth_boards.nexys4ddr import Nexys4DDRPlatform
from amaranth_boards.icestick import ICEStickPlatform
from manta import Manta
from manta.utils import *
import pytest
from random import randint


class IOCoreLoopbackTest(Elaboratable):
    def __init__(self, platform, port):
        self.platform = platform
        self.port = port

        self.config = self.platform_specific_config()
        self.manta = Manta(self.config)

    def platform_specific_config(self):
        return {
            "cores": {
                "io_core": {
                    "type": "io",
                    "inputs": {"probe0": 1, "probe1": 2, "probe2": 8, "probe3": 20},
                    "outputs": {
                        "probe4": {"width": 1, "initial_value": 1},
                        "probe5": {
                            "width": 2,
                            "initial_value": 2,
                        },
                        "probe6": 8,
                        "probe7": {"width": 20, "initial_value": 65538},
                    },
                }
            },
            "uart": {
                "port": self.port,
                "baudrate": 3e6,
                "clock_freq": self.platform.default_clk_frequency,
            },
        }

    def get_probe(self, name):
        # This is a hack! And should be removed once the full Amaranth-native
        # API is built out
        for i in self.manta.io_core._inputs:
            if i.name == name:
                return i

        for o in self.manta.io_core._outputs:
            if o.name == name:
                return o

        return None

    def elaborate(self, platform):
        m = Module()
        m.submodules.manta = self.manta

        uart_pins = platform.request("uart")

        probe0 = self.get_probe("probe0")
        probe1 = self.get_probe("probe1")
        probe2 = self.get_probe("probe2")
        probe3 = self.get_probe("probe3")
        probe4 = self.get_probe("probe4")
        probe5 = self.get_probe("probe5")
        probe6 = self.get_probe("probe6")
        probe7 = self.get_probe("probe7")

        m.d.comb += [
            probe0.eq(probe4),
            probe1.eq(probe5),
            probe2.eq(probe6),
            probe3.eq(probe7),
            self.manta.interface.rx.eq(uart_pins.rx.i),
            uart_pins.tx.o.eq(self.manta.interface.tx),
        ]

        return m

    def build_and_program(self):
        self.platform.build(self, do_program=True)

    def verify_output_probe_initial_values(self):
        """
        Test that all output probes take their expected initial values.
        We can't really test for the same of input probes, since the
        strobe register pulses every time the get_probe() method is called.
        """

        # Test that all output probes take their initial values
        inputs = self.config["cores"]["io_core"]["inputs"]
        outputs = self.config["cores"]["io_core"]["outputs"]

        for name, attrs in outputs.items():
            actual = self.manta.io_core.get_probe(name)

            if isinstance(attrs, dict):
                if "initial_value" in attrs:
                    expected = attrs["initial_value"]

            else:
                expected = 0

            if actual != expected:
                raise ValueError(
                    f"Output probe {name} took initial value of {actual} instead of {expected}."
                )

    def verify_probes_update(self):
        """
        This design ties all the output probes to input probes, so this
        test sets the outputs to random values, and verifies the inputs match
        """
        inputs = self.config["cores"]["io_core"]["inputs"]
        outputs = self.config["cores"]["io_core"]["outputs"]

        # The config is specified in such a way that the first output is
        # connected to the first output, the second output is connected
        # to the second input, and so on...
        for input, output in zip(inputs, outputs):
            width = self.config["cores"]["io_core"]["inputs"][input]
            value = randint(0, 2**width - 1)

            self.manta.io_core.set_probe(output, value)
            readback = self.manta.io_core.get_probe(input)

            if readback != value:
                raise ValueError(
                    f"Reading {output} through {input} yielded {readback} instead of {value}!"
                )

            else:
                print(
                    f"Reading {output} through {input} yielded {readback} as expected."
                )

    def verify(self):
        self.build_and_program()
        self.verify_output_probe_initial_values()
        self.verify_probes_update()


@pytest.mark.skipif(not xilinx_tools_installed(), reason="no toolchain installed")
def test_output_probe_initial_values_xilinx():
    port = "/dev/serial/by-id/usb-Digilent_Digilent_USB_Device_210292696307-if01-port0"
    IOCoreLoopbackTest(Nexys4DDRPlatform(), port).verify()


@pytest.mark.skipif(not ice40_tools_installed(), reason="no toolchain installed")
def test_output_probe_initial_values_ice40():
    port = "/dev/serial/by-id/usb-Lattice_Lattice_FTUSB_Interface_Cable-if01-port0"
    IOCoreLoopbackTest(ICEStickPlatform(), port).verify()
