#!/usr/bin/env python3
#
# Copyright 2017 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Update the CPLD image for the N310
"""
import argparse
import os
import subprocess

import pyudev
from usrp_mpm.mpmlog import get_logger
from usrp_mpm.periph_manager.n3xx import MboardRegsControl

OPENOCD_DIR = "/usr/share/openocd/scripts"
CONFIGS = {
    "sysfsgpio": {
        "files": [
            "interface/sysfsgpio-ettus-magnesium-dba.cfg",
            "interface/sysfsgpio-ettus-magnesium-dbb.cfg",
            "cpld/altera-5m570z-cpld.cfg",
        ],
        "cmd": "init; svf -tap 5m570z.tap %s -progress -quiet;exit",
    },
    "axi_bitq": {
        "files": ["cpld/altera-5m570z-cpld.cfg"],
        "cmd": [
            "interface axi_bitq; axi_bitq_config %u %u; adapter_khz %u",
            "init; svf -tap 5m570z.tap %s -progress -quiet;exit",
        ],
    },
}

AXI_BITQ_ADAPTER_SPEED = 8000
AXI_BITQ_BUS_CLK = 40000000


def check_openocd_files(files, logger=None):
    """
    Check if all file required by OpenOCD exist
    :param logger: logger object
    """
    for ocd_file in files:
        if not os.path.exists(os.path.join(OPENOCD_DIR, ocd_file)):
            if logger is not None:
                logger.error("Missing file %s" % os.path.join(OPENOCD_DIR, ocd_file))
            return False
    return True


def check_fpga_state(which=0):
    """
    Check if the FPGA is operational
    :param which: the FPGA to check
    """
    logger = get_logger("update_cpld")
    try:
        context = pyudev.Context()
        fpga_mgrs = [dev for dev in context.list_devices(subsystem="fpga_manager")]
        if fpga_mgrs:
            state = fpga_mgrs[which].attributes.asstring("state")
            logger.trace("FPGA State: {}".format(state))
            return state == "operating"
    except OSError as ex:
        logger.error("Error while checking FPGA status: {}".format(ex))
        return False


def find_axi_bitq_uio(dboard):
    """
    Find the AXI Bitq UIO device
    :param dboard: the dboard, can be either 0 or 1
    """
    assert dboard < 2 and dboard >= 0
    label = "dboard-jtag-%u" % dboard

    logger = get_logger("update_cpld")

    try:
        context = pyudev.Context()
        uios = [dev for dev in context.list_devices(subsystem="uio")]
        for uio in uios:
            uio_label = uio.attributes.asstring("maps/map0/name")
            logger.trace(
                "UIO label: {}, match: {} number: {}".format(
                    uio_label, uio_label == label, uio.sys_number
                )
            )
            if uio_label == label:
                return int(uio.sys_number)
    except OSError as ex:
        logger.error("Error while looking for axi_bitq uio nodes: {}".format(ex))
        return -1


def do_update_cpld(filename, daughterboards):
    """
    Carry out update process for the CPLD
    :param filename: path (on device) to the new CPLD image
    :param daughterboards: iterable containing dboard numbers to update
    :return: True on success, False otherwise
    """
    logger = get_logger("update_cpld")
    logger.info("Programming CPLD of dboards {} with image {}".format(daughterboards, filename))

    if not daughterboards:
        logger.error("Invalid daughterboard selection.")
        return False

    if not os.path.exists(filename):
        logger.error("CPLD image file {} not found".format(filename))
        return False

    if not check_fpga_state():
        logger.error("CPLD lines are routed through fabric, FPGA is not programmed, giving up")
        return False

    regs = MboardRegsControl("mboard-regs", logger)
    compat_maj = regs.get_compat_number()[0]
    mode = "axi_bitq" if compat_maj > 3 else "sysfsgpio"

    logger.info("FPGA has compatibilty number {} using {}".format(compat_maj, mode))

    config = CONFIGS[mode]

    if check_openocd_files(config["files"], logger=logger):
        logger.trace("Found required OpenOCD files.")
    else:
        # check_openocd_files logs errors
        return False

    for dboard in daughterboards:
        logger.info("Updating daughterboard slot {}...".format(dboard))

        if mode == "sysfsgpio":
            cmd = [
                "openocd",
                "-f",
                (config["files"][int(dboard, 10)]).strip(),
                "-f",
                (config["files"][2]).strip(),
                "-c",
                config["cmd"] % filename,
            ]
        else:
            uio_id = find_axi_bitq_uio(int(dboard, 10))
            if uio_id < 0:
                logger.error(
                    "Failed to find axi_bitq uio devices, \
                             make sure overlays are up to date"
                )
                continue
            cmd = [
                "openocd",
                "-c",
                config["cmd"][0] % (uio_id, AXI_BITQ_BUS_CLK, AXI_BITQ_ADAPTER_SPEED),
                "-f",
                (config["files"][0]).strip(),
                "-c",
                config["cmd"][1] % filename,
            ]

        logger.trace("Update CPLD CMD: {}".format(" ".join(cmd)))
        subprocess.call(cmd)

    logger.trace("Done programming CPLD...")
    return True


def main():
    """
    Go, go, go!
    """
    # We need to make a logger if we're running stand-alone
    from usrp_mpm.mpmlog import get_main_logger

    log = get_main_logger()

    # Do some setup
    def parse_args():
        """Parse the command-line arguments"""
        parser = argparse.ArgumentParser(description="Update the CPLD image on NI Magnesium")
        parser.add_argument(
            "--file",
            help="Filename of CPLD image",
            default="/lib/firmware/ni/cpld-magnesium-revc.svf",
        )
        parser.add_argument("--dboards", help="Slot name to program", default="0,1")
        return parser.parse_args()

    args = parse_args()
    dboards = args.dboards.split(",")
    if any([x not in ("0", "1") for x in dboards]):
        log.error("Unsupported dboards requested: %s", dboards)
        return False

    return do_update_cpld(args.file, dboards)


if __name__ == "__main__":
    exit(not main())
