#
# Copyright 2017 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Magnesium dboard peripherals (CPLD, port expander, dboard regs)
"""

import time

from usrp_mpm.mpmutils import poll_with_timeout
from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO


class TCA6408(object):
    """
    Abstraction layer for the port/gpio expander
    """

    pins = (
        "PWR-GOOD-3.6V",  # 3.6V
        "PWR-EN-3.6V",  # 3.6V
        "PWR-GOOD-1.5V",  # 1.5V
        "PWR-EN-1.5V",  # 1.5V
        "PWR-GOOD-5.5V",  # 5.5V
        "PWR-EN-5.5V",  # 5.5V
        "6",
        "LED",
    )

    def __init__(self, i2c_dev):
        if i2c_dev is None:
            raise RuntimeError("Need to specify i2c device to use the TCA6408")
        self._gpios = SysFSGPIO({"device/name": "tca6408"}, 0xBF, 0xAA, 0xAA, i2c_dev)

    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 DboardClockControl(object):
    """
    Control the FPGA MMCM for Radio Clock control.
    """

    # Clocking Register address constants
    RADIO_CLK_MMCM = 0x0020
    PHASE_SHIFT_CONTROL = 0x0024
    RADIO_CLK_ENABLES = 0x0028
    MGT_REF_CLK_STATUS = 0x0030

    def __init__(self, regs, log):
        self.log = log
        self.regs = regs
        self.poke32 = self.regs.poke32
        self.peek32 = self.regs.peek32

    def enable_outputs(self, enable=True):
        """
        Enables or disables the MMCM outputs.
        """
        if enable:
            self.poke32(self.RADIO_CLK_ENABLES, 0x011)
        else:
            self.poke32(self.RADIO_CLK_ENABLES, 0x000)

    def reset_mmcm(self):
        """
        Uninitialize and reset the MMCM
        """
        self.log.trace("Disabling all Radio Clocks, then resetting MMCM...")
        self.enable_outputs(False)
        self.poke32(self.RADIO_CLK_MMCM, 0x1)

    def enable_mmcm(self):
        """
        Unreset MMCM and poll lock indicators

        If MMCM is not locked after unreset, an exception is thrown.
        """
        self.log.trace("Enabling FPGA Radio Clock MMCM...")
        self.poke32(self.RADIO_CLK_MMCM, 0x2)
        if not poll_with_timeout(
            lambda: bool(self.peek32(self.RADIO_CLK_MMCM) & 0x10),
            500,
            10,
        ):
            self.log.error("FPGA Radio Clock MMCM not locked!")
            raise RuntimeError("FPGA Radio Clock MMCM not locked!")
        self.log.trace("Radio Clock MMCM locked. Enabling clocks to design...")
        self.enable_outputs(True)

    def check_refclk(self):
        """
        Not technically a clocking reg, but related.
        """
        return bool(self.peek32(self.MGT_REF_CLK_STATUS) & 0x1)


class MgCPLD(object):
    """
    Control class for the CPLD
    """

    CPLD_SIGNATURE = 0xCAFE  # Expected signature ("magic number")
    CPLD_MINOR_REV = 0
    CPLD_MAJOR_REV = 5

    REG_SIGNATURE = 0x0000
    REG_MINOR_REVISION = 0x0001
    REG_MAJOR_REVISION = 0x0002
    REG_BUILD_CODE_LSB = 0x0003
    REG_BUILD_CODE_MSB = 0x0004
    REG_SCRATCH = 0x0005
    REG_CPLD_CTRL = 0x0010
    REG_LMK_CTRL = 0x0011
    REG_LO_STATUS = 0x0012
    REG_MYK_CTRL = 0x0013

    def __init__(self, regs, log):
        self.log = log.getChild("CPLD")
        self.log.debug("Initializing CPLD...")
        self.regs = regs
        self.poke16 = self.regs.poke16
        self.peek16 = self.regs.peek16
        self.reset()
        signature = self.peek16(self.REG_SIGNATURE)
        if signature != self.CPLD_SIGNATURE:
            self.log.error(
                "CPLD Signature Mismatch! "
                "Expected: 0x{:04X} Got: 0x{:04X}".format(self.CPLD_SIGNATURE, signature)
            )
            raise RuntimeError("CPLD Signature Check Failed! " "Incorrect signature readback.")
        self.minor_rev = self.peek16(self.REG_MINOR_REVISION)
        self.major_rev = self.peek16(self.REG_MAJOR_REVISION)
        if self.major_rev != self.CPLD_MAJOR_REV:
            self.log.error(
                "CPLD Major Revision check mismatch! Expected: %d Got: %d",
                self.CPLD_MAJOR_REV,
                self.major_rev,
            )
            raise RuntimeError(
                "CPLD Revision Check Failed! MPM is not " "compatible with the loaded CPLD image."
            )
        date_code = self.peek16(self.REG_BUILD_CODE_LSB) | (
            self.peek16(self.REG_BUILD_CODE_MSB) << 16
        )
        self.log.debug(
            "CPLD Signature: 0x{:04X} "
            "Revision: {}.{} "
            "Date code: 0x{:08X}".format(signature, self.major_rev, self.minor_rev, date_code)
        )

    def set_scratch(self, val):
        "Write to the scratch register"
        self.poke16(self.REG_SCRATCH, val & 0xFFFF)

    def get_scratch(self):
        "Read from the scratch register"
        return self.peek16(self.REG_SCRATCH)

    def reset(self):
        "Reset entire CPLD"
        self.log.trace("Resetting CPLD...")
        self.poke16(self.REG_CPLD_CTRL, 0x1)
        self.poke16(self.REG_CPLD_CTRL, 0x0)

    def set_pdac_control(self, enb):
        """
        If enb is True, the Phase DAC will exclusively control the VCXO voltage
        """
        self.log.trace(
            "Giving Phase %s control over VCXO voltage...",
            "exclusive" if bool(enb) else "non-exclusive",
        )
        reg_val = (1 << 4) if enb else 0
        self.poke16(self.REG_LMK_CTRL, reg_val)

    def get_lo_lock_status(self, which):
        """
        Returns True if the 'which' LO is locked. 'which' is either 'tx' or
        'rx'.
        """
        mask = (1 << 4) if which.lower() == "tx" else 1
        return bool(self.peek16(self.REG_LO_STATUS) & mask)

    def reset_mykonos(self, keep_in_reset=False):
        """
        Hard-resets Mykonos.

        Arguments:
        keep_in_reset -- If True, Mykonos will stay in reset. Otherwise, it will
                         simply pulse the reset and Mykonos will be out of reset
                         once the function returns
        """
        self.log.debug("Resetting AD9371!")
        self.poke16(self.REG_MYK_CTRL, 0x1)
        if keep_in_reset:
            return
        time.sleep(0.001)  # No spec here, but give it some time to reset.
        self.poke16(self.REG_MYK_CTRL, 0x0)
        time.sleep(0.001)  # No spec here, but give it some time to reset.
