#!/usr/bin/env python3
"""
Copyright 2019 Ettus Research, A National Instrument Brand

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import sys
import argparse
import hashlib
import logging
import os
import re
import yaml

logging.basicConfig(format='[%(levelname).3s] %(message)s')

from uhd.imgbuilder import image_builder
from uhd.imgbuilder import yaml_utils

def setup_parser():
    """
    Create argument parser
    """
    parser = argparse.ArgumentParser(
        description="Build UHD image using RFNoC blocks",
    )

    config_group = parser.add_mutually_exclusive_group(required=True)
    config_group.add_argument(
        "-y", "--yaml-config",
        help="Path to yml configuration file")
    config_group.add_argument(
        "-r", "--grc-config",
        help="Path to grc file to generate config from")
    parser.add_argument(
        "-F", "--fpga-dir",
        help="Path to directory for the FPGA source tree. "
             "Defaults to the FPGA source tree of the current repo.",
        required=False,
        default=None)
    parser.add_argument(
        "-o", "--image-core-output",
        help="Path to where to save the image core Verilog source. "
             "Defaults to the directory of the YAML file, filename <DEVICE>_rfnoc_image_core.v",
        default=None)
    parser.add_argument(
        "-x", "--router-hex-output",
        help="Path to where to save the static router hex file. "
             "Defaults to the directory of the YAML file, filename <DEVICE>_static_router.hex",
        default=None)
    parser.add_argument(
        "-I", "--include-dir",
        help="Path directory of the RFNoC Out-of-Tree module",
        action='append', default=[]
        )
    parser.add_argument(
        "-b", "--grc-blocks",
        help="Path directory of GRC block descriptions (needed for --grc-config only)",
        default=None)
    parser.add_argument(
        "-l", "--log-level",
        help="Adjust log level",
        default='info')
    parser.add_argument(
        "--generate-only",
        help="Just generate files without building IP",
        action="store_true")
    parser.add_argument(
        "-d", "--device",
        help="Device to be programmed [x300, x310, e310, e320, n300, n310, n320, x410]. "
             "Needs to be specified either here, or in the configuration file.",
        default=None)
    parser.add_argument(
        "-n", "--image_core_name",
        help="Name to use for the RFNoC image core. "
             "Defaults to the device name.",
        default=None)
    parser.add_argument(
        "-t", "--target",
        help="Build target (e.g. X310_HG, N320_XG, ...). Needs to be specified "
             "either here, on the configuration file.",
        default=None)
    parser.add_argument(
        "-g", "--GUI",
        help="Open Vivado GUI during the FPGA building process",
        action="store_true")
    parser.add_argument(
        "-c", "--clean-all",
        help="Cleans the IP before a new build",
        action="store_true")
    parser.add_argument(
        "-p", "--vivado-path",
        help="Path to the base install for Xilinx Vivado if not in default "
             "location (e.g., /tools/Xilinx/Vivado).",
        default=None)

    return parser


def image_config(args):
    """
    Load image configuration.

    The configuration can be either passed as RFNoC image configuration or as
    GNU Radio Companion grc. In latter case the grc files is converted into a
    RFNoC image configuration on the fly.
    :param args: arguments passed to the script.
    :return: image configuration as dictionary
    """
    if args.yaml_config:
        config = yaml_utils.load_config(args.yaml_config, get_config_path())
        device = config.get('device') if args.device is None else args.device
        target = config.get('default_target') if args.target is None else args.target
        image_core_name = config.get('image_core_name') if args.image_core_name is None else args.image_core_name
        if image_core_name is None:
            image_core_name = device
        return config, args.yaml_config, device, image_core_name, target
    with open(args.grc_config) as grc_file:
        config = yaml.load(grc_file)
        logging.info("Converting GNU Radio Companion file to image builder format")
        config = image_builder.convert_to_image_config(config, args.grc_blocks)
        image_core_name = args.device if args.image_core_name is None else args.image_core_name
        return config, args.grc_config, args.device, image_core_name, args.target


def resolve_path(path, local):
    """
    Replaced path by local if path is enclosed with "@" (placeholder markers)
    :param path: the path to check
    :param local: new path content if path is placeholder
    :return: path if path is not a placeholder else local
    """
    return re.sub("^@.*@$", local, path)


def get_fpga_path(args):
    """
    Returns FPGA path. This is the fpga_dir from the arguments. If fpga_dir
    does not exist, it is changed to the FPGA source path of the current repo.
    If current directory is not in a repo, an error is generated.
    :param args: arguments passed to the script
    :return: FPGA root path
    """
    # If a valid path is given, use that
    if args.fpga_dir:
        if os.path.isdir(args.fpga_dir):
            fpga_path = os.path.abspath(args.fpga_dir)
            logging.info("Using FPGA directory %s", fpga_path)
            return fpga_path
        else:
            logging.error("Bad fpga-dir argument. %s is not a valid directory.", args.fpga_dir)
            sys.exit(1)
    # Try to find an fpga directory at the base of the repo
    repo_base = os.getcwd()
    while True:
        repo_base = os.path.split(repo_base)[0]
        top_dir = os.path.split(repo_base)[1]
        if top_dir == 'uhd' or top_dir == 'uhddev':
            fpga_path = os.path.join(repo_base, 'fpga')
            break
        if top_dir == '':
            fpga_path = None
            break
    # If it's valid FPGA base path, use that
    if fpga_path and os.path.isdir(os.path.join(fpga_path, 'usrp3', 'top')):
        logging.info("Using FPGA directory %s", fpga_path)
        return fpga_path
    # No valid path found
    logging.error("FPGA path not found. Specify with --fpga-dir argument.")
    sys.exit(1)


def get_config_path():
    """
    Returns path that contains configurations files (yml descriptions for
    block, IO signatures and device bsp).
    :return: Configuration path
    """
    return os.path.normpath(resolve_path("@CONFIG_PATH@", os.path.join(
        os.path.dirname(__file__), '..', 'include', 'uhd')))


def main():
    """
    Wrapper for image_builder.build_image.
    :return: exit code
    """
    args = setup_parser().parse_args()
    if args.log_level is not None:
        logging.root.setLevel(args.log_level.upper())

    config, source, device, image_core_name, target = image_config(args)
    source_hash = hashlib.sha256()
    with open(source, "rb") as source_file:
        source_hash.update(source_file.read())

    image_builder.build_image(
        config=config,
        fpga_path=get_fpga_path(args),
        config_path=get_config_path(),
        device=device,
        image_core_name=image_core_name,
        target=target,
        generate_only=args.generate_only,
        clean_all=args.clean_all,
        GUI=args.GUI,
        source=source,
        source_hash=source_hash.hexdigest(),
        output_path=args.image_core_output,
        router_hex_path=args.router_hex_output,
        include_paths=args.include_dir,
        vivado_path=args.vivado_path,
        )

if __name__ == "__main__":
    sys.exit(main())
