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

import math
import time

from usrp_mpm.periph_manager.common import MboardRegsCommon
from usrp_mpm.sys_utils.sysfs_gpio import GPIOBank, SysFSGPIO

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

E320_FPGA_TYPES_BY_SFP = {
    (""): "",
    ("1G"): "1G",
    ("10G"): "XG",
    ("A"): "AA",
}


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,
            0xFF,  # use_mask
            ddr,
        )


class MboardRegsControl(MboardRegsCommon):
    """
    Control the FPGA Motherboard registers
    """
    # pylint: disable=bad-whitespace
    # fmt: off
    # Motherboard registers
    MB_CLOCK_CTRL      = 0x0018
    MB_XADC_RB         = 0x001C
    MB_BUS_CLK_RATE    = 0x0020
    MB_BUS_COUNTER     = 0x0024
    MB_SFP_PORT_INFO   = 0x0028
    MB_GPIO_CTRL       = 0x002C
    MB_GPIO_MASTER     = 0x0030
    MB_GPIO_RADIO_SRC  = 0x0034
    MB_GPS_CTRL        = 0x0038
    MB_GPS_STATUS      = 0x003C
    MB_DBOARD_CTRL     = 0x0040
    MB_DBOARD_STATUS   = 0x0044
    MB_GPIO_USER_MUX   = 0x0050
    # pylint: enable=bad-whitespace
    # fmt: on

    # Bitfield locations for the MB_CLOCK_CTRL register.
    MB_CLOCK_CTRL_PPS_SEL_INT = 0
    MB_CLOCK_CTRL_PPS_SEL_EXT = 1
    MB_CLOCK_CTRL_REF_SEL = 2
    MB_CLOCK_CTRL_REF_CLK_LOCKED = 3

    # Bitfield locations for the MB_GPIO_CTRL register.
    MB_GPIO_CTRL_BUFFER_OE_N = 0
    MB_GPIO_CTRL_EN_VAR_SUPPLY = 1
    MB_GPIO_CTRL_EN_2V5 = 2
    MB_GPIO_CTRL_EN_3V3 = 3

    # Bitfield locations for the MB_GPS_CTRL register.
    MB_GPS_CTRL_PWR_EN = 0
    # This is connected to pin 23 (manual reset) of the GPS module.
    # Section 2.3.9 (System Reset) of the data sheet says:
    # This pin [...] can be connected to a reset switch. Initiating a system
    # reset will cause the GPS receiver to not generate GPS fixes for up to 35
    # seconds if power had been off for prolonged periods of time, and down to
    # 3 seconds typically if the unit had been powered-on recently.
    #
    # Note that in this use case, we typically don't use the system reset during
    # normal operations. Instead, we always fully power the GPS down when it's
    # not needed. Standard operation is to power-up the GPS when the device boots
    # and leave it running. GPS can also be powered-up or -down for the duration
    # of a single UHD session, and the default state can be overriden in mpm.conf.
    MB_GPS_CTRL_RST_N = 1
    # This pin is for auto-survey mode, which we do not use. This pin is kept
    # low at all times, as this mode is not intended for stationary use.
    MB_GPS_CTRL_INITSURV_N = 2

    # Bitfield locations for the MB_GPS_STATUS register.
    MB_GPS_STATUS_LOCK = 0
    MB_GPS_STATUS_ALARM = 1
    MB_GPS_STATUS_PHASELOCK = 2
    MB_GPS_STATUS_SURVEY = 3
    MB_GPS_STATUS_WARMUP = 4

    # Bitfield locations for the MB_DBOARD_CTRL register.
    MB_DBOARD_CTRL_MIMO = 0
    MB_DBOARD_CTRL_TX_CHAN_SEL = 1

    # Bitfield locations for the MB_DBOARD_STATUS register.
    MB_DBOARD_STATUS_RX_LOCK = 6
    MB_DBOARD_STATUS_TX_LOCK = 7

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

    def enable_fp_gpio(self, enable):
        """Enable front panel GPIO buffers and power supply
        and set voltage 3.3 V
        """
        self.set_fp_gpio_voltage(3.3)
        mask = 0xFFFFFFFF ^ (
            (0b1 << self.MB_GPIO_CTRL_BUFFER_OE_N) | (0b1 << self.MB_GPIO_CTRL_EN_VAR_SUPPLY)
        )
        with self.regs:
            reg_val = self.peek32(self.MB_GPIO_CTRL) & mask
            reg_val = (
                reg_val
                | (not enable << self.MB_GPIO_CTRL_BUFFER_OE_N)
                | (enable << self.MB_GPIO_CTRL_EN_VAR_SUPPLY)
            )
            self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val))
            return self.poke32(self.MB_GPIO_CTRL, reg_val)

    def set_fp_gpio_voltage(self, value):
        """Set Front Panel GPIO voltage (in volts)
        3V3 2V5 | Voltage
        -----------------
         0   0  | 1.8 V
         0   1  | 2.5 V
         1   0  | 3.3 V
        Arguments:
            value : 3.3
        """
        assert any(
            [math.isclose(value, nn, abs_tol=0.1) for nn in (3.3,)]
        ), "FP GPIO currently only supports 3.3V"
        if math.isclose(value, 1.8, abs_tol=0.1):
            voltage_reg = 0
        elif math.isclose(value, 2.5, abs_tol=0.1):
            voltage_reg = 1
        elif math.isclose(value, 3.3, abs_tol=0.1):
            voltage_reg = 2
        mask = 0xFFFFFFFF ^ ((0b1 << self.MB_GPIO_CTRL_EN_3V3) | (0b1 << self.MB_GPIO_CTRL_EN_2V5))
        with self.regs:
            reg_val = self.peek32(self.MB_GPIO_CTRL) & mask
            reg_val = reg_val | (voltage_reg << self.MB_GPIO_CTRL_EN_2V5)
            self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val))
            return self.poke32(self.MB_GPIO_CTRL, reg_val)

    def get_fp_gpio_voltage(self):
        """
        Get Front Panel GPIO voltage (in volts)
        """
        mask = 0x3 << self.MB_GPIO_CTRL_EN_2V5
        voltage = [1.8, 2.5, 3.3]
        with self.regs:
            reg_val = (self.peek32(self.MB_GPIO_CTRL) & mask) >> self.MB_GPIO_CTRL_EN_2V5
        return voltage[reg_val]

    def set_fp_gpio_master(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is a single bit bit mask of 8 pins GPIO
        0: means the pin is driven by Radio ATR states
        1: means the pin is driven by PS or user application on PL
        """
        with self.regs:
            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 8 pins GPIO.
        0: means the pin is driven by Radio ATR states
        1: means the pin is driven by PS or user application on PL
        """
        with self.regs:
            return self.peek32(self.MB_GPIO_MASTER) & 0xFF

    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) & 0xFF

    def set_fp_gpio_radio_src(self, value):
        """set driver for front panel GPIO
        Arguments:
            value {unsigned} -- value is 2-bit bit mask of 8 pins GPIO
           00: means the pin is driven by radio 0
           01: means the pin is driven by radio 1
        """
        with self.regs:
            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 8 pins GPIO.
        00: means the pin is driven by radio 0
        01: means the pin is driven by radio 1
        """
        with self.regs:
            return self.peek32(self.MB_GPIO_RADIO_SRC) & 0xFFFF

    def set_time_source(self, time_source, ref_clk_freq):
        """
        Set time source
        """
        pps_sel_val = 0x0
        if time_source == "internal" or time_source == "gpsdo":
            self.log.trace(
                "Setting time source to internal (GPSDO)"
                "({:.1f} MHz reference)...".format(ref_clk_freq)
            )
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT
        elif time_source == "external":
            self.log.debug("Setting time source to external...")
            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT
        else:
            assert False, "Cannot set to invalid time source: {}".format(time_source)

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

    def set_clock_source(self, clock_source, ref_clk_freq):
        """
        Set clock source
        """
        if clock_source == "internal" or clock_source == "gpsdo":
            self.log.trace(
                "Setting clock source to internal (GPSDO)"
                "({:.1f} MHz reference)...".format(ref_clk_freq)
            )
            ref_sel_val = 0b0
        elif clock_source == "external":
            self.log.debug(
                "Setting clock source to external..."
                "({:.1f} MHz reference)...".format(ref_clk_freq)
            )
            ref_sel_val = 0b1
        else:
            assert False, "Cannot set to invalid clock source: {}".format(clock_source)
        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_REF_SEL)
        with self.regs:
            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
            reg_val = reg_val | (ref_sel_val << self.MB_CLOCK_CTRL_REF_SEL)
            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_CLOCK_CTRL, reg_val)

    def get_fpga_type(self):
        """
        Reads the type of the FPGA image currently loaded
        Returns a string with the type (ie 1G, XG, AU, etc.)
        """
        with self.regs:
            sfp_info_rb = self.peek32(self.MB_SFP_PORT_INFO)
        # Print the registers values as 32-bit hex values
        self.log.trace("SFP Info: 0x{0:0{1}X}".format(sfp_info_rb, 8))
        sfp_type = E320_SFP_TYPES.get((sfp_info_rb & 0x0000FF00) >> 8, "")
        self.log.trace("SFP type: {}".format(sfp_type))
        try:
            return E320_FPGA_TYPES_BY_SFP[(sfp_type)]
        except KeyError:
            self.log.warning("Unrecognized SFP type: {}".format(sfp_type))
        return ""

    def get_gps_locked_val(self):
        """
        Get GPS LOCK status
        """
        mask = 0b1 << self.MB_GPS_STATUS_LOCK
        with self.regs:
            reg_val = self.peek32(self.MB_GPS_STATUS) & mask
            gps_locked = reg_val & 0x1  # FIXME
        if gps_locked:
            self.log.trace("GPS locked!")
        # Can return this value because the gps_locked value is on the LSB
        return gps_locked

    def get_gps_status(self):
        """
        Get GPS status
        """
        mask = 0x1F
        with self.regs:
            gps_status = self.peek32(self.MB_GPS_STATUS) & mask
        return gps_status

    def enable_gps(self, enable):
        """Turn power to the GPS (CLK_GPS_PWR_EN) off or on.

        When enabling the power, we sequence the power such that we first power
        the chip, then bring it out of reset with a short delay.
        When powering down the GPS, we make sure that all pins are low.

        Power signal is GPS_3V3.

        Note: This logic keeps MB_GPS_CTRL_INITSURV_N low at all times. Should
        a different behaviour be desired, we need to change this to also
        factor in the desired state of INITSURV_N. However, we are intentionally
        keeping INITSURV mode off, as it's not intended for stationary use.
        """
        self.log.trace("{} power to GPS".format("Enabling" if enable else "Disabling"))
        pwr_en_mask = 0b1 << self.MB_GPS_CTRL_PWR_EN
        rstn_mask = 0b1 << self.MB_GPS_CTRL_RST_N
        enabled_mask = pwr_en_mask | rstn_mask
        with self.regs:
            cur_reg_val = self.peek32(self.MB_GPS_CTRL)
            if enable and (cur_reg_val & enabled_mask != enabled_mask):
                # First bring up supply, then bring out of reset
                self.log.trace(f"Setting MB_GPS_CTRL to 0x{pwr_en_mask:08X} (power enable)")
                self.poke32(self.MB_GPS_CTRL, pwr_en_mask)
                # Jackson Labs data sheet says: "Pull to ground for >50ms to
                # initiate a system reset." Technically, we're powering the device
                # on and pulling it out of reset, so the full reset time is not
                # necessary, but we like sticking to values in data sheets and
                # this is a safe number.
                time.sleep(0.060)

                self.log.trace(f"Setting MB_GPS_CTRL to 0x{enabled_mask:08X} (out of reset)")
                return self.poke32(self.MB_GPS_CTRL, enabled_mask)
            elif not enable:
                # All controls pins low to avoid backfeeding I/O pins when unpowered
                self.log.trace("Setting MB_GPS_CTRL to 0x00000000")
                return self.poke32(self.MB_GPS_CTRL, 0)

    def get_refclk_lock(self):
        """
        Check the status of the reference clock (adf4002) in FPGA.
        """
        mask = 0b1 << self.MB_CLOCK_CTRL_REF_CLK_LOCKED
        with self.regs:
            reg_val = self.peek32(self.MB_CLOCK_CTRL)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning(
                "Reference Clock reporting unlocked. " "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)
            )
        else:
            self.log.trace("Reference Clock locked!")
        return locked

    def set_channel_mode(self, channel_mode):
        """
        Set channel mode in FPGA and select which tx channel to use
        channel mode = "MIMO" for mimo
        channel mode = "SISO_TX1", "SISO_TX0" for siso tx1, tx0 respectively.
        """
        with self.regs:
            reg_val = self.peek32(self.MB_DBOARD_CTRL)
            if channel_mode == "MIMO":
                reg_val = 0b1 << self.MB_DBOARD_CTRL_MIMO
                self.log.trace(
                    "Setting channel mode in AD9361 interface: %s",
                    "2R2T" if channel_mode == 2 else "1R1T",
                )
            else:
                # Warn if user tries to set either tx0/tx1 in mimo mode
                # as both will be set automatically
                if channel_mode == "SISO_TX1":
                    # in SISO mode, Channel 1
                    reg_val = (0b1 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (
                        0b0 << self.MB_DBOARD_CTRL_MIMO
                    )
                    self.log.trace("Setting TX channel in AD9361 interface to: TX1")
                elif channel_mode == "SISO_TX0":
                    # in SISO mode, Channel 0
                    reg_val = (0b0 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (
                        0b0 << self.MB_DBOARD_CTRL_MIMO
                    )
                    self.log.trace("Setting TX channel in AD9361 interface to: TX0")
            self.log.trace("Writing MB_DBOARD_CTRL to 0x{:08X}".format(reg_val))
            self.poke32(self.MB_DBOARD_CTRL, reg_val)

    def get_ad9361_tx_lo_lock(self):
        """
        Check the status of TX LO lock from CTRL_OUT pins from Catalina
        """
        mask = 0b1 << self.MB_DBOARD_STATUS_TX_LOCK
        with self.regs:
            reg_val = self.peek32(self.MB_DBOARD_STATUS)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning("TX RF PLL reporting unlocked. ")
        else:
            self.log.trace("TX RF PLL locked")
        return locked

    def get_ad9361_rx_lo_lock(self):
        """
        Check the status of RX LO lock from CTRL_OUT pins from Catalina
        """
        mask = 0b1 << self.MB_DBOARD_STATUS_RX_LOCK
        with self.regs:
            reg_val = self.peek32(self.MB_DBOARD_STATUS)
        locked = (reg_val & mask) > 0
        if not locked:
            self.log.warning("RX RF PLL reporting unlocked. ")
        else:
            self.log.trace("RX RF PLL locked")
        return locked
