"""
SPDX-License-Identifier: LGPL-2.1-or-later
SPDX-FileCopyrightText: 2020 CERN
"""

import pytest
import select
import time
import os
from PyFmcFineDelay import FmcFineDelay, FmcFineDelayTime
import random


@pytest.fixture(scope="function", params=pytest.channels)
def fmcfd_chan(request, fmcfd):
    yield fmcfd.chan[request.param]
    fmcfd.chan[request.param].disable()


MIN_COUNT = 1
MAX_COUNT = 0xFFFF


@pytest.fixture(scope="function")
def fmcfd_tdc(fmcfd):
    fmcfd.tdc.enable_input = False
    fmcfd.tdc.enable_tstamp = False
    fmcfd.tdc.termination = False
    fmcfd.tdc.flush()

    fmcfd.tdc.enable_tstamp = True
    fmcfd.tdc.enable_input = True
    yield fmcfd.tdc
    fmcfd.tdc.enable_input = False
    fmcfd.tdc.enable_tstamp = False


def timeout_compute(start, period_ps, count):
    proportional = ((count * period_ps) / 1000000000000.0)
    return time.time() + proportional + start + 5


class TestFmcfdLoop(object):
    """
    The test needs a lemo cable (1ns) that connects all outputs
    to the input channel

    input
      chan   o------.
    output           |
      chan 1 o-------+
      chan 2 o-------+
      chan 3 o-------+
      chan 4 o-------`
    """

    @pytest.mark.parametrize("period_ps", [400000])
    def test_output_flush(self, fmcfd, fmcfd_chan, fmcfd_tdc, period_ps):
        start = fmcfd.time + FmcFineDelayTime(0, 500000, 0)
        fmcfd_chan.pulse_generate(start, int(period_ps / 2), period_ps, 1)
        assert len(fmcfd_tdc.poll(2000)) > 0
        fmcfd_tdc.flush()
        assert len(fmcfd_tdc.poll(1000)) == 0

    @pytest.mark.parametrize("count, period_ps",
                             [(i + 1, 1000000) for i in range(15)]
                             + [(100, 4500000000), (MAX_COUNT, 4500000000)])
    def test_output_counter(self, fmcfd, fmcfd_chan, fmcfd_tdc, count, period_ps):
        """In pulse mode, the Fine-Delay generates the exact number of
        required pulses and we are able to read them all from the input
        channel with an assigned sequential number.

        Internal TDC used a FIFO, therefore it can't handle a too fast and long
        burst. Maximumg 15 pulses. On VME it is slower due to the bus: a period
        of 4.5ms is the best compromise for this test.

        We test the extremes to make the test a bit faster.
        """
        ts = []
        start = fmcfd.time + FmcFineDelayTime(0, 1000000, 0)
        fmcfd_chan.pulse_generate(start, int(period_ps / 2), period_ps, count)
        fmcfd_tdc.poll(1000)
        while len(fmcfd_tdc.poll(500)) > 0:
            try:
                t = fmcfd_tdc.read(100, os.O_NONBLOCK)
                assert len(t) > 0
                ts = ts + t
            except BlockingIOError:
                continue
        assert len(ts) == count
        assert len(fmcfd_tdc.poll(50)) == 0

        prev_ts = None
        for i in range(len(ts)):
            if prev_ts is not None:
                assert ts[i].seq_id == (prev_ts.seq_id + 1) & 0xFFFF, f"i:{i}, cur: {ts[i]}, prev: {prev_ts}"
            prev_ts = ts[i]
        del ts

    @pytest.mark.parametrize("start_rel",
                             [FmcFineDelayTime(0, random.randrange(1, 125000000), 0) for i in range(50)]
                             + [FmcFineDelayTime(1, random.randrange(1, 125000000), 0) for i in range(5)]
                             + [FmcFineDelayTime(2, random.randrange(1, 125000000), 0) for i in range(5)])
    @pytest.mark.parametrize("wr", [False, True])
    def test_output_input_start(self, fmcfd_chan, fmcfd_tdc, wr, start_rel):
        """
        The output channel generates a pulse at a given time and the input
        channel timestamps it. The two times must be almost the same excluding
        the propagation time (cable length).
        """
        period_ps = 1000000
        count = 1

        fmcfd_chan.dev.whiterabbit_mode = wr
        ts = []

        start = fmcfd_chan.dev.time + start_rel
        fmcfd_chan.pulse_generate(start, int(period_ps / 2), period_ps, count)
        assert len(fmcfd_tdc.poll(int(float(start_rel) * 1000) + 2000)) > 0
        ts = fmcfd_tdc.read(count, os.O_NONBLOCK)
        assert len(ts) == count
        assert start.seconds == ts[0].seconds
        assert ts[0].coarse - start.coarse <= 3  # there is < 3ns cable

    @pytest.mark.parametrize("period_ps",
                             [random.randrange(1000000, 10000000) for x in range(128)]
                             + [random.randrange(10000000, 100000000) for x in range(64)]
                             + [random.randrange(100000000, 1000000000) for x in range(32)]
                             + [random.randrange(1000000000, 10000000000) for x in range(16)]
                             + [random.randrange(10000000000, 100000000000) for x in range(8)]
                             + [random.randrange(100000000000, 1000000000000) for x in range(4)]
                             )
    @pytest.mark.parametrize("count", [15])
    def test_output_period(self, fmcfd_chan, fmcfd_tdc, period_ps, count):
        """
        The test produces pulses on the given channels and catch them using
        the on board TDC. The period between two timestamps must be as close
        as possible (ideally equal) to the period used to generate them.
        """
        ts = []

        start = fmcfd_chan.dev.time + FmcFineDelayTime(0, 500000, 0, 0, 0)
        fmcfd_chan.pulse_generate(start, int(period_ps / 2), period_ps, count)
        time.sleep(0.02 + (count * period_ps) / 1000000000000.0)
        assert len(fmcfd_tdc.poll(100)) > 0
        ts = fmcfd_tdc.read(count, os.O_NONBLOCK)
        assert len(ts) == count

        prev_ts = None
        for i in range(len(ts)):
            if prev_ts is not None:
                period_ts = ts[i] - prev_ts
                period = FmcFineDelayTime.from_pico(period_ps)
                if period > period_ts:
                    diff = period - period_ts
                else:
                    diff = period_ts - period
                assert diff < FmcFineDelayTime(0, 1, 0), \
                    "period difference {:s}\n\tcurr: {:s}\n\tprev: {:s}\n\tperi: {:s}".format(str(diff),
                                                                                              str(ts[i]),
                                                                                              str(prev_ts),
                                                                                              str(period_ts))
            prev_ts = ts[i]
