#
# Copyright 2018 Ettus Research, a National Instruments Company
# Copyright 2019 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
N3xx peripherals
"""

from usrp_mpm import lib
from usrp_mpm.chips.ds125df410 import DS125DF410
from usrp_mpm.periph_manager.common import MboardRegsCommon
from usrp_mpm.sys_utils import i2c_dev
from usrp_mpm.sys_utils.sysfs_gpio import GPIOBank, SysFSGPIO

# Map register values to SFP transport types
N3XX_SFP_TYPES = {
    0: "",  # Port not connected
    1: "1G",
    2: "10G",
    3: "A",  # Aurora
    4: "W",  # White Rabbit
}

N3XX_FPGA_TYPES_BY_SFP = {
    ("", ""): "",
    ("1G", "10G"): "HG",
    ("10G", "10G"): "XG",
    ("10G", "A"): "XA",
    ("A", "A"): "AA",
    ("W", "10G"): "WX",
}


class TCA6424(object):
    """
    Abstraction layer for the port/gpio expander
    pins_list is  an array of different version of TCA6424 pins map.
    First element of this array corresponding to revC, second is revD etc...
    """

    pins_list = [
        (
            "PWREN-CLK-MGT156MHz",
            "NETCLK-CE",  # revC name: 'PWREN-CLK-WB-CDCM',
            "NETCLK-RESETn",  # revC name: 'WB-CDCM-RESETn',
            "NETCLK-PR0",  # revC name: 'WB-CDCM-PR0',
            "NETCLK-PR1",  # revC name: 'WB-CDCM-PR1',
            "NETCLK-OD0",  # revC name: 'WB-CDCM-OD0',
            "NETCLK-OD1",  # revC name: 'WB-CDCM-OD1',
            "NETCLK-OD2",  # revC name: 'WB-CDCM-OD2',
            "PWREN-CLK-MAINREF",
            "CLK-MAINSEL-25MHz",  # revC name: 'CLK-MAINREF-SEL1',
            "CLK-MAINSEL-EX_B",  # revC name: 'CLK-MAINREF-SEL0',
            "12",
            "CLK-MAINSEL-GPS",  # revC name: '13',
            "FPGA-GPIO-EN",
            "PWREN-CLK-WB-20MHz",
            "PWREN-CLK-WB-25MHz",
            "GPS-PHASELOCK",
            "GPS-nINITSURV",
            "GPS-nRESET",
            "GPS-WARMUP",
            "GPS-SURVEY",
            "GPS-LOCKOK",
            "GPS-ALARM",
            "PWREN-GPS",
        ),
        (
            "NETCLK-PR1",
            "NETCLK-PR0",
            "NETCLK-CE",
            "NETCLK-RESETn",
            "NETCLK-OD2",
            "NETCLK-OD1",
            "NETCLK-OD0",
            "PWREN-CLK-MGT156MHz",
            "PWREN-CLK-MAINREF",
            "CLK-MAINSEL-25MHz",
            "CLK-MAINSEL-EX_B",
            "12",
            "CLK-MAINSEL-GPS",
            "FPGA-GPIO-EN",
            "PWREN-CLK-WB-20MHz",
            "PWREN-CLK-WB-25MHz",
            "GPS-PHASELOCK",
            "GPS-nINITSURV",
            "GPS-nRESET",
            "GPS-WARMUP",
            "GPS-SURVEY",
            "GPS-LOCKOK",
            "GPS-ALARM",
            "PWREN-GPS",
        ),
    ]

    def __init__(self, rev):
        # Default state: Turn on GPS power, take GPS out of reset or
        # init-survey, turn on 156.25 MHz clock
        # min Support from revC or rev = 2
        if rev == 2:
            self.pins = self.pins_list[0]
        else:
            self.pins = self.pins_list[1]

        default_val = 0x860101 if rev == 2 else 0x860780
        self._gpios = SysFSGPIO(
            {"device/name": "tca6424", "device/of_node/name": "gpio"},
            0xFFF7FF,
            0x86F7FF,
            default_val,
        )

    def set(self, name, value=None):
        """
        Assert a pin by name
        """
        assert name in self.pins
        self._gpios.set(self.pins.index(name), value=value)

    def reset(self, name):
        """
        Deassert a pin by name
        """
        self.set(name, value=0)

    def get(self, name):
        """
        Read back a pin by name
        """
        assert name in self.pins
        return self._gpios.get(self.pins.index(name))


class FrontpanelGPIO(GPIOBank):
    """
    Abstraction layer for the front panel GPIO
    """

    EMIO_BASE = 54
    FP_GPIO_OFFSET = 32  # Bit offset within the ps_gpio_* pins

    def __init__(self, ddr):
        GPIOBank.__init__(
            self,
            {"label": "zynq_gpio"},
            self.FP_GPIO_OFFSET + self.EMIO_BASE,
            0xFFF,  # use_mask
            ddr,
        )


class BackpanelGPIO(GPIOBank):
    """
    Abstraction layer for the back panel GPIO
    """

    EMIO_BASE = 54
    BP_GPIO_OFFSET = 45
    LED_LINK = 0
    LED_REF = 1
    LED_GPS = 2

    def __init__(self):
        GPIOBank.__init__(
            self,
            {"label": "zynq_gpio"},
            self.BP_GPIO_OFFSET + self.EMIO_BASE,
            0x7,  # use_mask
            0x7,  # ddr
        )


class MboardRegsControl(MboardRegsCommon):
    """
    Control the FPGA Motherboard registers
    """
    # pylint: disable=bad-whitespace
    # fmt: off
    # Motherboard registers (on top of the ones in MboardRegsCommon)
    MB_CLOCK_CTRL     = 0x0018
    MB_XADC_RB        = 0x001C
    MB_BUS_CLK_RATE   = 0x0020
    MB_BUS_COUNTER    = 0x0024
    MB_SFP0_INFO      = 0x0028
    MB_SFP1_INFO      = 0x002C
    MB_GPIO_MASTER    = 0x0030
    MB_GPIO_RADIO_SRC = 0x0034
    MB_GPIO_USER_MUX = 0x0050

    # Bitfield locations for the MB_CLOCK_CTRL register.
    MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0 # pps_sel is one-hot encoded!
    MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1
    MB_CLOCK_CTRL_PPS_SEL_EXT    = 2
    MB_CLOCK_CTRL_PPS_SEL_GPSDO  = 3
    MB_CLOCK_CTRL_PPS_SEL_SFP0   = 5
    MB_CLOCK_CTRL_PPS_SEL_SFP1   = 6
    MB_CLOCK_CTRL_PPS_OUT_EN = 4 # output enabled = 1
    MB_CLOCK_CTRL_MEAS_CLK_RESET = 12 # set to 1 to reset mmcm, default is 0
    MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13 # locked indication for meas_clk mmcm
    MB_CLOCK_CTRL_DISABLE_REF_CLK = 16 # to disable the ref_clk, write a '1'
    # pylint: enable=bad-whitespace
    # fmt: on

    def __init__(self, label, log):
        MboardRegsCommon.__init__(self, label, log)

    def set_fp_gpio_master(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is a single bit bit mask of 12 pins GPIO
           0: means the pin is driven by Radio ATR states
           1: means the pin is driven by PS or user application
        """
        with self.regs:
            return self.poke32(self.MB_GPIO_MASTER, value)

    def get_fp_gpio_master(self):
        """get "who" is driving front panel gpio
        The return value is a bit mask of 12 pins GPIO.
        0: means the pin is driven by Radio ATR states
        1: means the pin is driven by PS or user application
        """
        with self.regs:
            return self.peek32(self.MB_GPIO_MASTER) & 0xFFF

    def set_fp_gpio_user_mux(self, value):
        """set user mux for front panel GPIO
        Arguments:
            value {unsigned} -- value is a single bit bit mask of 12 pins GPIO
           0: means the pin is driven by PS
           1: means the pin is driven by user application
        """
        with self.regs:
            self.poke32(self.MB_GPIO_USER_MUX, value)

    def get_fp_gpio_user_mux(self):
        """get user mux for front panel GPIO
        The return value is a bit mask of 12 pins GPIO.
        0: means the pin is driven by PS
        1: means the pin is driven by user application
        """
        with self.regs:
            return self.peek32(self.MB_GPIO_USER_MUX) & 0xFFF

    def set_fp_gpio_radio_src(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is 2x12 bits, two bits per GPIO pin
           00: means the pin is driven by radio 0
           01: means the pin is driven by radio 1
           10: means the pin is driven by radio 2
           11: means the pin is driven by radio 3
        """
        with self.regs:
            return self.poke32(self.MB_GPIO_RADIO_SRC, value)

    def get_fp_gpio_radio_src(self):
        """get which radio is driving front panel gpio
        The return value is 2-bit bit mask of 12 pins GPIO.
        00: means the pin is driven by radio 0
        01: means the pin is driven by radio 1
        10: means the pin is driven by radio 2
        11: means the pin is driven by radio 3
        """
        with self.regs:
            return self.peek32(self.MB_GPIO_RADIO_SRC) & 0xFFFFFF

    def set_time_source(self, time_source, ref_clk_freq):
        """
        Set time source
        """
        pps_sel_val = 0x0
        if time_source == "internal":
            assert ref_clk_freq in (10e6, 25e6), (
                "Invalid reference frequency for time source 'internal'. Must "
                "be either 10 MHz or 25 MHz. Check clock and time source match."
            )
            if ref_clk_freq == 10e6:
                self.log.debug("Setting time source to internal " "(10 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10
            elif ref_clk_freq == 25e6:
                self.log.debug("Setting time source to internal " "(25 MHz reference)...")
                pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25
        elif time_source == "external":
            self.log.debug("Setting time source to external...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT
        elif time_source == "gpsdo":
            self.log.debug("Setting time source to gpsdo...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO
        elif time_source == "sfp0":
            self.log.debug("Setting time source to sfp0...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_SFP0
        elif time_source == "sfp1":
            self.log.debug("Setting time source to sfp1...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_SFP1
        else:
            raise RuntimeError("Invalid time source: {}".format(time_source))

        with self.regs:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFF90
            # prevent glitches by writing a cleared value first, then the final value.
            self.poke32(self.MB_CLOCK_CTRL, reg_val)
            reg_val = reg_val | (pps_sel_val & 0x6F)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def enable_pps_out(self, enable):
        """
        Enables the PPS/Trig output on the back panel
        """
        self.log.trace("%s PPS/Trig output!", "Enabling" if enable else "Disabling")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
        with self.regs:
            # mask the bit to clear it:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if enable:
                # set the bit if desired:
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def enable_ref_clk(self, enable):
        """
        Enables the reference clock internal to the FPGA
        """
        self.log.trace("%s the Reference Clock!", "Enabling" if enable else "Disabling")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_DISABLE_REF_CLK)
        with self.regs:
            # mask the bit to clear it and therefore enable the clock:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if not enable:
                # set the bit if not enabled (note this is a DISABLE bit when = 1):
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_DISABLE_REF_CLK)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def reset_meas_clk_mmcm(self, reset=True):
        """
        Reset or unreset the MMCM for the measurement clock in the FPGA TDC.
        """
        self.log.trace("%s measurement clock MMCM reset...", "Asserting" if reset else "Clearing")
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
        with self.regs:
            # mask the bit to clear it
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            if reset:
                # set the bit if desired
                reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def get_meas_clock_mmcm_lock(self):
        """
        Check the status of the MMCM for the measurement clock in the FPGA TDC.
        """
        mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_LOCKED
        with self.regs:
            reg_val = self.peek32(self.MB_CLOCK_CTRL)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning(
                "Measurement clock MMCM reporting unlocked. "
                "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)
            )
        else:
            self.log.trace("Measurement clock MMCM locked!")
        return locked

    def get_fpga_type(self):
        """
        Reads the type of the FPGA image currently loaded
        Returns a string with the type (ie HG, XG, AA, etc.)
        """
        with self.regs:
            sfp0_info_rb = self.peek32(self.MB_SFP0_INFO)
            sfp1_info_rb = self.peek32(self.MB_SFP1_INFO)
        # Print the registers values as 32-bit hex values
        self.log.trace("SFP0 Info: 0x{0:0{1}X}".format(sfp0_info_rb, 8))
        self.log.trace("SFP1 Info: 0x{0:0{1}X}".format(sfp1_info_rb, 8))
        sfp0_type = N3XX_SFP_TYPES.get((sfp0_info_rb & 0x0000FF00) >> 8, "")
        sfp1_type = N3XX_SFP_TYPES.get((sfp1_info_rb & 0x0000FF00) >> 8, "")
        self.log.trace("SFP types: ({}, {})".format(sfp0_type, sfp1_type))
        try:
            return N3XX_FPGA_TYPES_BY_SFP[(sfp0_type, sfp1_type)]
        except KeyError:
            self.log.warning(
                "Unrecognized SFP type combination: ({}, {})".format(sfp0_type, sfp1_type)
            )
        return ""


class RetimerQSFP(DS125DF410):
    """
    Thin wrapper around an I2C device that controls the QSFP retimer
    """

    # (deemphasis, swing)
    DRIVER_PRESETS = {"1m": (0x00, 0x07), "3m": (0x41, 0x06), "Optical": (0x41, 0x04)}

    def __init__(self, i2c_bus):
        regs_iface = lib.i2c.make_i2cdev_regs_iface(i2c_bus, 0x18, False, 100, 1)
        super(RetimerQSFP, self).__init__(regs_iface)
