import unittest
from types import SimpleNamespace
import random

from migen import *

from artiq.gateware.drtio import *
from artiq.gateware.drtio import rt_serializer
from artiq.gateware import rtio
from artiq.gateware.rtio import rtlink
from artiq.gateware.rtio.phy import ttl_simple
from artiq.coredevice.exceptions import *


class DummyTransceiverPair:
    def __init__(self, nwords):
        a2b_k = [Signal() for _ in range(nwords)]
        a2b_d = [Signal(8) for _ in range(nwords)]
        b2a_k = [Signal() for _ in range(nwords)]
        b2a_d = [Signal(8) for _ in range(nwords)]

        self.alice = SimpleNamespace(
            encoder=SimpleNamespace(k=a2b_k, d=a2b_d),
            decoders=[SimpleNamespace(k=k, d=d) for k, d in zip(b2a_k, b2a_d)],
            rx_ready=1
        )
        self.bob = SimpleNamespace(
            encoder=SimpleNamespace(k=b2a_k, d=b2a_d),
            decoders=[SimpleNamespace(k=k, d=d) for k, d in zip(a2b_k, a2b_d)],
            rx_ready=1
        )


class DummyRXSynchronizer:
    def resync(self, signal):
        return signal


class SimpleIOPHY(Module):
    def __init__(self, o_width, i_width):
        self.rtlink = rtlink.Interface(
            rtlink.OInterface(o_width),
            rtlink.IInterface(i_width, timestamped=True))
        self.received_data = Signal(o_width)
        self.sync.rio_phy += If(self.rtlink.o.stb,
            self.received_data.eq(self.rtlink.o.data))


class DUT(Module):
    def __init__(self, nwords):
        self.ttl0 = Signal()
        self.ttl1 = Signal()
        self.transceivers = DummyTransceiverPair(nwords)

        self.submodules.tsc_master = rtio.TSC()
        self.submodules.master = DRTIOMaster(self.tsc_master,
                                             self.transceivers.alice)
        self.submodules.master_ki = rtio.KernelInitiator(self.tsc_master,
            self.master.cri)

        rx_synchronizer = DummyRXSynchronizer()
        self.submodules.phy0 = ttl_simple.Output(self.ttl0)
        self.submodules.phy1 = ttl_simple.Output(self.ttl1)
        self.submodules.phy2 = SimpleIOPHY(512, 32)  # test wide output data
        rtio_channels = [
            rtio.Channel.from_phy(self.phy0),
            rtio.Channel.from_phy(self.phy1),
            rtio.Channel.from_phy(self.phy2),
        ]
        self.submodules.tsc_satellite = rtio.TSC()
        self.submodules.satellite = DRTIOSatellite(
            self.tsc_satellite, self.transceivers.bob, rx_synchronizer)
        self.satellite.reset.storage.reset = 0
        self.satellite.reset.storage_full.reset = 0
        self.satellite.reset_phy.storage.reset = 0
        self.satellite.reset_phy.storage_full.reset = 0
        self.submodules.satellite_rtio = SyncRTIO(
            self.tsc_satellite, rtio_channels, lane_count=4, fifo_depth=8)
        self.comb += [
            self.satellite.cri.connect(self.satellite_rtio.cri),
            self.satellite.async_errors.eq(self.satellite_rtio.async_errors),
        ]


class OutputsTestbench:
    def __init__(self):
        self.dut = DUT(2)
        self.now = 0

    def init(self):
        yield from self.dut.master.rt_controller.csrs.underflow_margin.write(100)
        while not (yield from self.dut.master.link_layer.rx_up.read()):
            yield
        yield from self.get_buffer_space()

    def get_buffer_space(self):
        csrs = self.dut.master.rt_controller.csrs
        yield from csrs.o_get_buffer_space.write(1)
        yield
        while (yield from csrs.o_wait.read()):
            yield
        r = (yield from csrs.o_dbg_buffer_space.read())
        return r

    def delay(self, dt):
        self.now += dt

    def sync(self):
        t = self.now + 15
        while (yield self.dut.tsc_master.full_ts_cri) < t:
            yield

    def write(self, channel, data):
        kcsrs = self.dut.master_ki
        yield from kcsrs.target.write(channel << 8)
        yield from kcsrs.now_hi.write(self.now >> 32)
        yield from kcsrs.now_lo.write(self.now & 0xffffffff)
        yield from kcsrs.o_data.write(data)
        yield
        status = 1
        wlen = 0
        while status:
            status = yield from kcsrs.o_status.read()
            if status & 0x2:
                raise RTIOUnderflow
            if status & 0x4:
                raise RTIODestinationUnreachable
            yield
            wlen += 1
        return wlen

    @passive
    def check_ttls(self, ttl_changes):
        cycle = 0
        old_ttls = [0, 0]
        while True:
            ttls = [(yield self.dut.ttl0), (yield self.dut.ttl1)]
            for n, (old_ttl, ttl) in enumerate(zip(old_ttls, ttls)):
                if ttl != old_ttl:
                    ttl_changes.append((cycle, n))
            old_ttls = ttls
            yield
            cycle += 1


class TestFullStack(unittest.TestCase):
    clocks = {"sys": 8, "rtio_rx": 8,
              "rio": 8, "rio_phy": 8}

    def test_pulses(self):
        tb = OutputsTestbench()
        ttl_changes = []
        correct_ttl_changes = [
            (208, 0),
            (213, 0),
            (213, 1),
            (219, 1),
        ]

        def test():
            yield from tb.init()
            tb.delay(200)
            yield from tb.write(0, 1)
            tb.delay(5)
            yield from tb.write(0, 0)
            yield from tb.write(1, 1)
            tb.delay(6)
            yield from tb.write(1, 0)
            yield from tb.sync()

        run_simulation(tb.dut,
            {"sys": [test(), tb.check_ttls(ttl_changes)]}, self.clocks)
        self.assertEqual(ttl_changes, correct_ttl_changes)

    def test_underflow(self):
        tb = OutputsTestbench()

        def test():
            yield from tb.init()
            with self.assertRaises(RTIOUnderflow):
                yield from tb.write(0, 0)

        run_simulation(tb.dut, {"sys": test()}, self.clocks)

    def test_large_data(self):
        tb = OutputsTestbench()

        def test():
            yield from tb.init()
            correct_large_data = random.Random(0).randrange(2**512-1)
            self.assertNotEqual((yield tb.dut.phy2.received_data), correct_large_data)
            tb.delay(200)
            yield from tb.write(2, correct_large_data)
            yield from tb.sync()
            self.assertEqual((yield tb.dut.phy2.received_data), correct_large_data)

        run_simulation(tb.dut, {"sys": test()}, self.clocks)

    def test_buffer_space(self):
        tb = OutputsTestbench()
        ttl_changes = []
        correct_ttl_changes = [(258 + 40*i, 0) for i in range(10)]

        def test():
            yield from tb.init()
            tb.delay(250)
            max_wlen = 0
            for i in range(10):
                wlen = yield from tb.write(0, (i + 1) % 2)
                max_wlen = max(max_wlen, wlen)
                tb.delay(40)
            # check that some writes caused buffer space requests
            self.assertGreater(max_wlen, 5)
            yield from tb.sync()

        run_simulation(tb.dut,
            {"sys": [test(), tb.check_ttls(ttl_changes)]}, self.clocks)
        self.assertEqual(ttl_changes, correct_ttl_changes)

    def test_write_underflow(self):
        tb = OutputsTestbench()

        def test():
            saterr = tb.dut.satellite.rt_errors
            csrs = tb.dut.master.rt_controller.csrs
            yield from tb.init()
            errors = yield from saterr.protocol_error.read()
            self.assertEqual(errors, 0)
            yield from csrs.underflow_margin.write(0)
            tb.delay(80)
            yield from tb.write(42, 1)
            for i in range(21):
               yield
            errors = yield from saterr.protocol_error.read()
            underflow_channel = yield from saterr.underflow_channel.read()
            underflow_timestamp_event = yield from saterr.underflow_timestamp_event.read()
            self.assertEqual(errors, 8)  # write underflow
            self.assertEqual(underflow_channel, 42)
            self.assertEqual(underflow_timestamp_event, 80)
            yield from saterr.protocol_error.write(errors)
            yield
            errors = yield from saterr.protocol_error.read()
            self.assertEqual(errors, 0)

        run_simulation(tb.dut, {"sys": test()}, self.clocks)

    def test_inputs(self):
        dut = DUT(2)
        kcsrs = dut.master_ki

        def get_input(timeout):
            yield from kcsrs.target.write(2 << 8)
            yield from kcsrs.i_timeout.write(10)
            yield
            status = yield from kcsrs.i_status.read()
            while status & 0x4:
                yield
                status = yield from kcsrs.i_status.read()
            if status & 0x1:
                return "timeout"
            if status & 0x2:
                return "overflow"
            if status & 0x8:
                return "destination unreachable"
            return ((yield from kcsrs.i_data.read()),
                    (yield from kcsrs.i_timestamp.read()))

        def test():
            while not (yield from dut.master.link_layer.rx_up.read()):
                yield

            i1 = yield from get_input(10)
            i2 = yield from get_input(20)
            self.assertEqual(i1, (0x600d1dea, 6))
            self.assertEqual(i2, "timeout")

        def generate_input():
            for i in range(5):
                yield
            yield dut.phy2.rtlink.i.data.eq(0x600d1dea)
            yield dut.phy2.rtlink.i.stb.eq(1)
            yield
            yield dut.phy2.rtlink.i.data.eq(0)
            yield dut.phy2.rtlink.i.stb.eq(0)

        run_simulation(dut,
            {"sys": [test(), generate_input()]}, self.clocks)

    def test_echo(self):
        dut = DUT(2)
        packet = dut.master.rt_packet

        def test():
            while not (yield from dut.master.link_layer.rx_up.read()):
                yield

            self.assertEqual((yield dut.master.rt_packet.packet_cnt_tx), 0)
            self.assertEqual((yield dut.master.rt_packet.packet_cnt_rx), 0)

            yield dut.master.rt_packet.echo_stb.eq(1)
            yield
            while not (yield dut.master.rt_packet.echo_ack):
                yield
            yield dut.master.rt_packet.echo_stb.eq(0)

            for i in range(17):
                yield

            self.assertEqual((yield dut.master.rt_packet.packet_cnt_tx), 1)
            self.assertEqual((yield dut.master.rt_packet.packet_cnt_rx), 1)

        run_simulation(dut, test(), self.clocks)
