#
# Copyright 2018 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
AD9695 driver for use with Rhodium
"""

import time
from builtins import object

from ..mpmlog import get_logger


class AD9695Rh(object):
    """
    This class provides an interface to configure the AD9695 IC through SPI.
    """

    ADC_CHIP_ID = 0xDE

    CHIP_CONFIGURATION_REG = 0x0002
    CHIP_ID_LSB_REG = 0x0004
    SCRATCH_PAD_REG = 0x000A

    def __init__(self, slot_idx, regs_iface, parent_log=None):
        self.log = (
            parent_log.getChild("AD9695")
            if parent_log is not None
            else get_logger("AD9695-{}".format(slot_idx))
        )
        self.regs = regs_iface
        assert hasattr(self.regs, "peek8")
        assert hasattr(self.regs, "poke8")

        def _verify_chip_id():
            chip_id = self.regs.peek8(self.CHIP_ID_LSB_REG)
            self.log.trace("ADC Chip ID: 0x{:X}".format(chip_id))
            if chip_id != self.ADC_CHIP_ID:
                self.log.error("Wrong Chip ID 0x{:X}".format(chip_id))
                return False
            return True

        if not _verify_chip_id():
            raise RuntimeError("Unable to locate AD9695")

    def assert_scratch(self, scratch_val=0xAD):
        """
        Method that validates the scratch register by poking and peeking.
        """
        self.regs.poke8(self.SCRATCH_PAD_REG, scratch_val)
        self.log.trace("Scratch write value: 0x{:X}".format(scratch_val))
        scratch_rb = self.regs.peek8(self.SCRATCH_PAD_REG) & 0xFF
        self.log.trace("Scratch readback: 0x{:X}".format(scratch_rb))
        if scratch_rb != scratch_val:
            raise RuntimeError("Wrong ADC scratch readback: 0x{:X}".format(scratch_rb))

    def pokes8(self, addr_vals):
        """
        Apply a series of pokes.
        pokes8((0,1),(0,2)) is the same as calling poke8(0,1), poke8(0,2).
        """
        for addr, val in addr_vals:
            self.regs.poke8(addr, val)

    def power_down_channel(self, power_down=False):
        """
        This method either powers up/down the channel according to register 0x0002 [1:0]:
          power_down = True  -> Power-down mode.
                                  Digital datapath clocks disabled; digital datapath held
                                  in reset; JESD204B interface disabled.
          power_down = False -> Normal mode.
                                  Channel powered up.
        """
        power_mode = 0b11 if power_down else 0b00
        self.regs.poke8(self.CHIP_CONFIGURATION_REG, power_mode)

    def init(self):
        """
        Basic init that resets the ADC and verifies it.
        """
        self.power_down_channel(False)  # Power-up the channel.
        self.log.trace("Reset ADC & Verify")
        # fmt: off
        self.regs.poke8(0x0000, 0x81) # Soft-reset the ADC (self-clearing).
        time.sleep(0.005)             # We must allow 5 ms for the ADC's bootloader.
        self.assert_scratch(0xAD)     # Verify scratch register R/W access.
        self.regs.poke8(0x0571, 0x15) # Powerdown the JESD204B serial transmit link.
        # fmt: on
        self.log.trace("ADC's JESD204B link powered down.")

    def config(self):
        """
        Check the clock status, and write configuration values!
        Before performing the above, the chip is soft-reset through the
        serial interface.
        """
        self.init()

        clock_status = self.regs.peek8(0x011B) & 0xFF
        self.log.trace("Clock status readback: 0x{:X}".format(clock_status))
        if clock_status != 0x01:
            self.log.error("Input clock not detected")
            raise RuntimeError("Input clock not detected for ADC")

        self.log.trace("ADC Configuration.")
        # fmt: off
        self.pokes8((
            (0x003F, 0x80), # Disable PDWN/STBY pin.
            (0x0040, 0x00), # FD_A|B pins configured as Fast Detect outputs.
            (0x0559, 0x11), # Configure tail bits as overrange bits (1:0) (Based on VST2).
            (0x055A, 0x01), # Configure tail bits as overrange bits (2) (Based on VST2).
            (0x058D, 0x17), # Set K = d23 (0x17) (Frames per multiframe).
            (0x0550, 0x00), # Test mode pattern generation: OFF.
            (0x0571, 0x15), # JESD Link mode: ILA enabled with K28.3 and K28.7; link powered down.
            (0x058F, 0x0D), # CS = 0 (Control bits); N = 14 (ADC resolution).
            (0x056E, 0x10), # JESD204B lane rate range: 3.375 Gbps to 6.75 Gbps.
            (0x058B, 0x03), # Scrambling disabled; L = 4 (number of lanes).
            (0x058C, 0x00), # F = 1 (0x0 + 1) (Number of octets per frame)
            (0x058E, 0x01), # M = 2 (0x1) (Number of converters per link).
            (0x05B2, 0x01), # SERDOUT0 mapped to lane 1.
            (0x05B3, 0x00), # SERDOUT1 mapped to lane 0.
            (0x05C0, 0x10), # SERDOUT0 voltage swing adjust (improves RX margin at FPGA).
            (0x05C1, 0x10), # SERDOUT1 voltage swing adjust (improves RX margin at FPGA).
            (0x05C2, 0x10), # SERDOUT2 voltage swing adjust (improves RX margin at FPGA).
            (0x05C3, 0x10), # SERDOUT3 voltage swing adjust (improves RX margin at FPGA).
        ))
        # fmt: on
        self.log.trace("ADC register dump finished.")

    def init_framer(self):
        """
        Initialize the ADC's framer, and check the PLL for lock.
        """

        def _check_pll_lock():
            pll_lock_status = self.regs.peek8(0x056F)
            if (pll_lock_status & 0x88) != 0x80:
                self.log.debug("PLL reporting unlocked... Status: 0x{:x}".format(pll_lock_status))
                return False
            return True

        self.log.trace("Initializing framer...")
        # fmt: off
        self.pokes8((
            (0x0571, 0x14), # Powerup the link before JESD204B initialization.
            (0x1228, 0x4F), # Reset JESD204B start-up circuit
            (0x1228, 0x0F), # JESD204B start-up circuit in normal operation. The value may be 0x00.
            (0x1222, 0x00), # JESD204B PLL force normal operation
            (0x1222, 0x04), # Reset JESD204B PLL calibration
            (0x1222, 0x00), # JESD204B PLL normal operation
            (0x1262, 0x08), # Clear loss of lock bit
            (0x1262, 0x00), # Loss of lock bit normal operation
        ))
        # fmt: on

        self.log.trace("Polling for PLL lock...")
        locked = False
        for _ in range(6):
            time.sleep(0.001)
            # Clear stickies possibly?
            if _check_pll_lock():
                locked = True
                self.log.info("ADC PLL Locked!")
                break
        if not locked:
            raise RuntimeError("ADC PLL did not lock! Check the logs for details.")

        self.log.trace("ADC framer initialized.")

    def enable_sysref_capture(self, enabled=False):
        """
        Enable the SYSREF capture block.
        """
        sysref_ctl1 = 0x00  # Default value is disabled.
        if enabled:
            sysref_ctl1 = 0b1 << 2  # N-Shot SYSREF mode

        self.log.trace("%s ADC SYSREF capture..." % {True: "Enabling", False: "Disabling"}[enabled])
        self.pokes8(
            (
                (0x0120, sysref_ctl1),  # Capture low-to-high N-shot SYSREF transitions on CLK's RE
                (0x0121, 0x00),  # Capture the next SYSREF only.
            )
        )
        self.log.trace("ADC SYSREF capture %s." % {True: "enabled", False: "disabled"}[enabled])

    def check_framer_status(self):
        """
        This function checks the status of the framer by checking SYSREF capture regs.
        """
        SYSREF_MONITOR_MESSAGES = {
            0: "Condition not defined!",
            1: "Possible setup error. The smaller the setup, the smaller its margin.",
            2: "No setup or hold error (best hold margin).",
            3: "No setup or hold error (best setup and hold margin).",
            4: "No setup or hold error (best setup margin).",
            5: "Possible hold error. The largest the hold, the smaller its margin.",
            6: "Possible setup or hold error.",
        }

        # This is based of Table 37 in the AD9695's datasheet.
        def _decode_setup_hold(setup, hold):
            status = 0
            if setup == 0x0:
                if hold == 0x0:
                    status = 6
                elif hold == 0x8:
                    status = 4
                elif (hold >= 0x9) and (hold <= 0xF):
                    status = 5
            elif (setup <= 0x7) and (hold == 0x0):
                status = 1
            elif (setup == 0x8) and ((hold >= 0x0) and (hold <= 0x8)):
                status = 2
            elif hold == 0x8:
                status = 3
            return status

        self.log.trace("Checking ADC's framer status.")

        # Read the SYSREF setup and hold monitor register.
        sysref_setup_hold_monitor = self.regs.peek8(0x0128)
        sysref_setup = sysref_setup_hold_monitor & 0x0F
        sysref_hold = (sysref_setup_hold_monitor & 0xF0) >> 4
        sysref_monitor_status = _decode_setup_hold(sysref_setup, sysref_hold)
        self.log.trace("SYSREF setup: 0x{:X}".format(sysref_setup))
        self.log.trace("SYSREF hold: 0x{:X}".format(sysref_hold))
        self.log.debug("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status])

        # Read the Clock divider phase when SYSREF is captured.
        sysref_phase = self.regs.peek8(0x0129) & 0x0F
        self.log.trace("SYSREF capture was %.2f cycle(s) delayed from clock.", (sysref_phase * 0.5))
        self.log.trace("Clk divider phase when SYSREF was captured: 0x{:X}".format(sysref_phase))

        # Read the SYSREF counter.
        sysref_count = self.regs.peek8(0x012A) & 0xFF
        self.log.trace("%d SYSREF events were captured." % sysref_count)

        if sysref_count == 0x0:
            self.log.error("A SYSREF event was not captured by the ADC.")
        elif sysref_monitor_status == 0:
            self.log.trace("The SYSREF setup & hold monitor status is not defined.")
        elif sysref_monitor_status in (1, 5, 6):
            self.log.warning("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status])
        elif sysref_phase > 0x0:
            self.log.trace(
                "SYSREF capture was %.2f cycle(s) delayed from clock." % (sysref_phase * 0.5)
            )
        return sysref_count >= 0x01
