#
# Copyright 2021-2022 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
X4x0 Motherboard Clocking Control

This module contains the X4xxClockCtrl class, which is the worker bee for the
X4xxClockManager class.

The code in this module controls the RPLL and SPLL as well as some of the muxing.
The eCPRI PLL is controlled from the ClockingAuxBrdControl class.
"""

from usrp_mpm import lib  # Pulls in everything from C++-land
from usrp_mpm.periph_manager.x4xx_clock_types import (
    BrcSource,
    RpllBrcSrcSel,
    RpllRefSel,
)
from usrp_mpm.periph_manager.x4xx_periphs import MboardRegsControl
from usrp_mpm.periph_manager.x4xx_reference_pll import LMK03328X4xx
from usrp_mpm.periph_manager.x4xx_sample_pll import LMK04832X4xx
from usrp_mpm.sys_utils import i2c_dev
from usrp_mpm.sys_utils.gpio import Gpio
from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev

X400_RPLL_I2C_LABEL = "rpll_i2c"


class X4xxClockCtrl:
    """
    X4x0 Clocking Control

    This class controls various parts of the X4xx clocking:
    - Sample PLL (LMK04832)
    - Reference PLL (LMK03328)
    - Base Reference Clock source selection (via GPIO)
    - Clocking-related motherboard registers
    - Clocking-related settings on the motherboard CPLD

    This class exposes APIs to control all of these components. The sequencing
    logic etc. is encoded in the X4xxClockManager class.
    """

    def __init__(self, cpld_control, log):
        # Store parent objects
        self.log = log.getChild("ClkMgr")
        self._cpld_control = cpld_control
        # Preallocate other objects to satisfy linter
        self.mboard_regs_control = None  # Needs to be set later from outside!
        self._sample_pll = None
        self._reference_pll = None
        self._base_ref_clk_select = None
        # Init peripherals
        self._init_clk_peripherals()

    ###########################################################################
    # Initialization code
    ###########################################################################
    def _init_clk_peripherals(self):
        """
        Initialize objects for peripherals owned by this class. Most importantly,
        this includes the RPLL and SPLL control classes.
        """
        # Create SPI and I2C interfaces to the LMK registers
        spll_spi_node = dt_symbol_get_spidev("spll")
        sample_lmk_regs_iface = lib.spi.make_spidev_regs_iface(
            spll_spi_node,
            1000000,  # Speed (Hz)
            0x3,  # SPI mode
            8,  # Addr shift
            0,  # Data shift
            1 << 23,  # Read flag
            0,  # Write flag
        )
        # Initialize I2C connection to RPLL
        rpll_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_RPLL_I2C_LABEL)
        if rpll_i2c_bus is None:
            raise RuntimeError("RPLL I2C bus could not be found")
        self.log.trace("Using RPLL I2C bus: %s", str(rpll_i2c_bus))
        reference_lmk_regs_iface = lib.i2c.make_i2cdev_regs_iface(
            rpll_i2c_bus, 0x54, False, 100, 1  # addr  # ten_bit_addr  # timeout_ms  # reg_addr_size
        )
        self._sample_pll = LMK04832X4xx(sample_lmk_regs_iface, self.log)
        self._reference_pll = LMK03328X4xx(reference_lmk_regs_iface, self.log)
        # Init BRC select GPIO control
        self._base_ref_clk_select = Gpio("BASE-REFERENCE-CLOCK-SELECT", Gpio.OUTPUT, 1)

    ###########################################################################
    # Low-level controls
    ###########################################################################
    def reset_clock(self, value, clock_to_reset):
        """
        Helper API to reset or re-enable a clock.
        """
        if clock_to_reset == "cpld":
            self._cpld_control.enable_pll_ref_clk(enable=not value)
        if clock_to_reset == "spll":
            self._sample_pll.reset(value, hard=True)
        if clock_to_reset == "rpll":
            self._reference_pll.reset(value, hard=True)

    def config_rpll(
        self,
        internal_brc_source: RpllRefSel,
        ref_rate: float,
        internal_brc_rate: float,
        usr_clk_rate: float,
    ):
        """
        Configures the LMK03328 to generate the desired MGT reference clocks
        and internal BRC rate.

        Currently, the MGT protocol selection is not supported, but a custom
        usr_clk_rate can be generated from PLL1.

        internal_brc_source - the reference source which drives the RPLL
                              (primary or secondary). Primary is the clocking
                              aux card, secondary is the 100 MHz oscillator.
        ref_rate - The frequency which is being provided as reference at the
                   input corresponding to internal_brc_source.
        internal_brc_rate - the rate to output as the BRC
        usr_clk_rate - the custom clock rate to generate from PLL1
        """
        assert internal_brc_source in RpllRefSel
        assert internal_brc_source == RpllRefSel.SECONDARY or ref_rate is not None

        # If the desired rate matches the rate of the primary reference source,
        # directly passthrough that reference source
        if internal_brc_source == RpllRefSel.PRIMARY and internal_brc_rate == ref_rate:
            brc_select = RpllBrcSrcSel.BYPASS
        else:
            brc_select = RpllBrcSrcSel.PLL

        # TODO: Questionable if we always need to call init() here
        self._reference_pll.init()
        self._reference_pll.config(
            internal_brc_source, ref_rate, internal_brc_rate, usr_clk_rate, brc_select
        )

    def config_spll(self, pll_settings: dict):
        """
        Configures the SPLL for the specified master clock rate.
        """
        # TODO: Questionable if we always need to call init() here
        self._sample_pll.init()
        self._sample_pll.config(pll_settings)

    def get_spll_freq(self):
        """
        Returns the output frequency setting of the SPLL that is routed to the
        ADCs/DACs.
        """
        return self._sample_pll.output_freq

    def get_prc_rate(self):
        """
        Returns the rate of the PLL Reference Clock (PRC) which is
        routed to the daughterboards (as an LO reference and CPLD clock) and to
        the FPGA (used to generate data_clk and rfdc_clk from the MMCM).
        Note: The PRC will change if the sample clock frequency is modified.
        """
        return self._sample_pll.prc_freq

    def select_brc_source(self, brc_src: BrcSource):
        """
        Configures the GaAs switch (U10/U11) that controls which BRC is used.

        """
        if brc_src == BrcSource.RPLL:
            self._base_ref_clk_select.set(1)
        elif brc_src == BrcSource.CLK_AUX:
            self._base_ref_clk_select.set(0)
        else:
            raise RuntimeError("Invalid BRC source.")

    def sync_spll_clocks(self, pps_source: str, ref_clock_freq: float):
        """
        Synchronize base reference clock (BRC) and PLL reference clock (PRC)
        at start of PPS trigger.

        Uses the LMK 04832 pll1_r_divider_sync to synchronize BRC with PRC.
        This sync method uses a callback to actual trigger the sync. Before
        the trigger is pulled (CLOCK_CTRL_PLL_SYNC_TRIGGER) PPS source is
        configured base on current reference clock and pps_source. After sync
        trigger the method waits for 1sec for the CLOCK_CTRL_PLL_SYNC_DONE
        to go high.

        :param pps_source: select whether internal ("internal_pps") or external
                           ("external_pps") PPS should be used. This parameter
                           is taken into account when the current clock source
                           is external. If the current clock source is set to
                           internal then this parameter is not taken into
                           account.
        :return:           success state of sync call
        :raises RuntimeError: for invalid combinations of reference clock and
                              pps_source
        """

        def select_pps():
            """
            Select PPS source based on current clock source and pps_source.

            This returns the bits for the motherboard CLOCK_CTRL register that
            control the PPS source.
            """
            EXT_PPS = "external_pps"
            INT_PPS = "internal_pps"
            PPS_SOURCES = (EXT_PPS, INT_PPS)
            assert pps_source in PPS_SOURCES, "%s not in %s" % (pps_source, PPS_SOURCES)

            supported_configs = {
                (10e6, EXT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_EXT,
                (10e6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_10MHz,
                (25e6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_25MHz,
            }

            config = (ref_clock_freq, pps_source)
            if config not in supported_configs:
                raise RuntimeError(
                    "Unsupported combination of reference clock "
                    "(%.2E) and PPS source (%s) for PPS sync." % config
                )
            return supported_configs[config]

        return self._sample_pll.pll1_r_divider_sync(
            lambda: self.mboard_regs_control.pll_sync_trigger(select_pps())
        )

    def configure_pps_forwarding(
        self, tk_idx: int, enable: bool, radio_clock_rate: float, delay: float = 1.0
    ):
        """
        Configures the PPS forwarding to the sample clock domain (master
        clock rate). This function assumes sync_spll_clocks function has
        already been executed.

        :param tk_idx: Which timekeeper to configure.
        :param enable: Boolean to choose whether PPS is forwarded to the
                       sample clock domain.

        :param delay:  Delay in seconds from the PPS rising edge to the edge
                       occurence in the application. This value has to be in
                       range 0 < x <= 1. In order to forward the PPS signal
                       from base reference clock to sample clock an aligned
                       rising edge of the clock is required. This can be
                       created by the sync_spll_clocks function. Based on the
                       greatest common divisor of the two clock rates there
                       are multiple occurences of an aligned edge each second.
                       One of these aligned edges has to be chosen for the
                       PPS forwarding by setting this parameter.

        :return:       None, Exception on error
        """
        # FIXME MULTI_RATE enable multiple MCRs / timekeepers, use tk_idx
        return self.mboard_regs_control.configure_pps_forwarding(
            enable, tk_idx, radio_clock_rate, self.get_prc_rate(), delay
        )

    def get_ref_locked(self):
        """
        Return lock status both RPLL and SPLL.
        """
        ref_pll_status = self._reference_pll.get_status()
        sample_pll_status = self._sample_pll.get_status()
        self.log.trace(
            f"RPLL1 lock: {ref_pll_status['PLL1 lock']} "
            f"RPLL2 lock: {ref_pll_status['PLL2 lock']} "
            f"SPLL1 lock: {sample_pll_status['PLL1 lock']} "
            f"SPLL2 lock: {sample_pll_status['PLL2 lock']}"
        )
        return all(
            [
                ref_pll_status["PLL1 lock"],
                ref_pll_status["PLL2 lock"],
                sample_pll_status["PLL1 lock"],
                sample_pll_status["PLL2 lock"],
            ]
        )

    ###########################################################################
    # Top-level BIST APIs
    #
    # These calls will be available as MPM calls. They are only needed by BIST.
    ###########################################################################
    def enable_ecpri_clocks(self, enable=True, clock="both"):
        """
        Enable or disable the export of FABRIC and GTY_RCV eCPRI
        clocks. Main use case until we support eCPRI is manufacturing
        testing.
        """
        self.mboard_regs_control.enable_ecpri_clocks(enable, clock)

    def get_fpga_aux_ref_freq(self):
        """
        Return the tick count of an FPGA counter which measures the width of
        the PPS signal on the FPGA_AUX_REF FPGA input using a 40 MHz clock.
        Main use case until we support eCPRI is manufacturing testing.
        A return value of 0 indicates absence of a valid PPS signal on the
        FPGA_AUX_REF line.
        """
        return self.mboard_regs_control.get_fpga_aux_ref_freq()
