#
# This file is part of LiteX.
#
# Copyright (c) 2014-2019 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2019 msloniewski <marcin.sloniewski@gmail.com>
# Copyright (c) 2019 vytautasb <v.buitvydas@limemicro.com>
# SPDX-License-Identifier: BSD-2-Clause

import os
import subprocess
import sys
import math
from shutil import which

from migen.fhdl.structure import _Fragment

from litex.build.generic_platform import Pins, IOStandard, Misc
from litex.build import tools

# IO/Placement Constraints (.qsf) ------------------------------------------------------------------

def _format_constraint(c, signame, fmt_r):
    # IO location constraints
    if isinstance(c, Pins):
        tpl = "set_location_assignment -comment \"{name}\" -to {signame} Pin_{pin}"
        return tpl.format(signame=signame, name=fmt_r, pin=c.identifiers[0])

    # IO standard constraints
    elif isinstance(c, IOStandard):
        tpl = "set_instance_assignment -name io_standard -comment \"{name}\" \"{std}\" -to {signame}"
        return tpl.format(signame=signame, name=fmt_r, std=c.name)

    # Others constraints
    elif isinstance(c, Misc):
        if not isinstance(c.misc, str) and len(c.misc) == 2:
            tpl = "set_instance_assignment -comment \"{name}\" -name {misc[0]} \"{misc[1]}\" -to {signame}"
            return tpl.format(signame=signame, name=fmt_r, misc=c.misc)
        else:
            tpl = "set_instance_assignment -comment \"{name}\"  -name {misc} -to {signame}"
            return tpl.format(signame=signame, name=fmt_r, misc=c.misc)

def _format_qsf_constraint(signame, pin, others, resname):
    fmt_r = "{}:{}".format(*resname[:2])
    if resname[2] is not None:
        fmt_r += "." + resname[2]
    fmt_c = [_format_constraint(c, signame, fmt_r) for c in ([Pins(pin)] + others)]
    return '\n'.join(fmt_c)

def _build_qsf_constraints(named_sc, named_pc):
    qsf = []
    for sig, pins, others, resname in named_sc:
        if len(pins) > 1:
            for i, p in enumerate(pins):
                qsf.append(_format_qsf_constraint("{}[{}]".format(sig, i), p, others, resname))
        else:
            qsf.append(_format_qsf_constraint(sig, pins[0], others, resname))
    if named_pc:
        qsf.append("\n\n".join(named_pc))
    return "\n".join(qsf)

# Timing Constraints (.sdc) ------------------------------------------------------------------------

def _build_sdc(clocks, false_paths, vns, named_sc, build_name, additional_sdc_commands):
    sdc = []

    # Clock constraints
    for clk, period in sorted(clocks.items(), key=lambda x: x[0].duid):
        is_port = False
        for sig, pins, others, resname in named_sc:
            if sig == vns.get_name(clk):
                is_port = True
        if is_port:
            tpl = "create_clock -name {clk} -period {period} [get_ports {{{clk}}}]"
            sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period)))
        else:
            tpl = "create_clock -name {clk} -period {period} [get_nets {{{clk}}}]"
            sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period)))

    # False path constraints
    for from_, to in sorted(false_paths, key=lambda x: (x[0].duid, x[1].duid)):
        tpl = "set_false_path -from [get_clocks {{{from_}}}] -to [get_clocks {{{to}}}]"
        sdc.append(tpl.format(from_=vns.get_name(from_), to=vns.get_name(to)))

    # Add additional commands
    sdc += additional_sdc_commands

    # Generate .sdc
    tools.write_to_file("{}.sdc".format(build_name), "\n".join(sdc))

# Project (.qsf) -----------------------------------------------------------------------------------

def _build_qsf(device, ips, sources, vincpaths, named_sc, named_pc, build_name, additional_qsf_commands):
    qsf = []

    # Set device
    qsf.append("set_global_assignment -name DEVICE {}".format(device))

    # Add sources
    for filename, language, library in sources:
        if language == "verilog": language = "systemverilog" # Enforce use of SystemVerilog
        tpl = "set_global_assignment -name {lang}_FILE {path} -library {lib}"
        # Do not add None type files
        if language is not None:
            qsf.append(tpl.format(lang=language.upper(), path=filename.replace("\\", "/"), lib=library))
        # Check if the file is a header. Those should not be explicitly added to qsf,
        # but rather included in include search_path
        else:
            if filename.endswith(".svh") or filename.endswith(".vh"):
                fpath = os.path.dirname(filename)
                if fpath not in vincpaths:
                    vincpaths.append(fpath)

    # Add ips
    for filename in ips:
        tpl = "set_global_assignment -name QSYS_FILE {filename}"
        qsf.append(tpl.replace(filename=filename.replace("\\", "/")))

    # Add include paths
    for path in vincpaths:
        qsf.append("set_global_assignment -name SEARCH_PATH {}".format(path.replace("\\", "/")))

    # Set top level
    qsf.append("set_global_assignment -name top_level_entity " + build_name)

    # Add io, placement constraints
    qsf.append(_build_qsf_constraints(named_sc, named_pc))

    # Set timing constraints
    qsf.append("set_global_assignment -name SDC_FILE {}.sdc".format(build_name))

    # Add additional commands
    qsf += additional_qsf_commands

    # Generate .qsf
    tools.write_to_file("{}.qsf".format(build_name), "\n".join(qsf))

# Script -------------------------------------------------------------------------------------------

def _build_script(build_name, create_rbf):
    if sys.platform in ["win32", "cygwin"]:
        script_contents = "REM Autogenerated by LiteX / git: " + tools.get_litex_git_revision()
        script_file = "build_" + build_name + ".bat"
    else:
        script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision()
        script_file = "build_" + build_name + ".sh"
    script_contents += """
quartus_map --read_settings_files=on  --write_settings_files=off {build_name} -c {build_name}
quartus_fit --read_settings_files=off --write_settings_files=off {build_name} -c {build_name}
quartus_asm --read_settings_files=off --write_settings_files=off {build_name} -c {build_name}
quartus_sta {build_name} -c {build_name}"""
    if create_rbf:
        script_contents += """
if [ -f "{build_name}.sof" ]
then
    quartus_cpf -c {build_name}.sof {build_name}.rbf
fi
"""
    script_contents = script_contents.format(build_name=build_name)
    tools.write_to_file(script_file, script_contents, force_unix=True)

    return script_file

def _run_script(script):
    if sys.platform in ["win32", "cygwin"]:
        shell = ["cmd", "/c"]
    else:
        shell = ["bash"]

    if which("quartus_map") is None:
        msg = "Unable to find Quartus toolchain, please:\n"
        msg += "- Add Quartus toolchain to your $PATH."
        raise OSError(msg)

    if subprocess.call(shell + [script]) != 0:
        raise OSError("Error occured during Quartus's script execution.")

# AlteraQuartusToolchain ---------------------------------------------------------------------------

class AlteraQuartusToolchain:
    attr_translate = {}

    def __init__(self):
        self.clocks      = dict()
        self.false_paths = set()
        self.additional_sdc_commands = []
        self.additional_qsf_commands = []

    def build(self, platform, fragment,
        build_dir      = "build",
        build_name     = "top",
        run            = True,
        **kwargs):

        # Create build directory
        cwd = os.getcwd()
        os.makedirs(build_dir, exist_ok=True)
        os.chdir(build_dir)

        # Finalize design
        if not isinstance(fragment, _Fragment):
            fragment = fragment.get_fragment()
        platform.finalize(fragment)

        # Generate verilog
        v_output = platform.get_verilog(fragment, name=build_name, **kwargs)
        named_sc, named_pc = platform.resolve_signals(v_output.ns)
        v_file = build_name + ".v"
        v_output.write(v_file)
        platform.add_source(v_file)

        # Generate design timing constraints file (.sdc)
        _build_sdc(
            clocks                  = self.clocks,
            false_paths             = self.false_paths,
            vns                     = v_output.ns,
            named_sc                = named_sc,
            build_name              = build_name,
            additional_sdc_commands = self.additional_sdc_commands)

        # Generate design project and location constraints file (.qsf)
        _build_qsf(
            device                  = platform.device,
            ips                     = platform.ips,
            sources                 = platform.sources,
            vincpaths               = platform.verilog_include_paths,
            named_sc                = named_sc,
            named_pc                = named_pc,
            build_name              = build_name,
            additional_qsf_commands = self.additional_qsf_commands)

        # Generate build script
        script = _build_script(build_name, platform.create_rbf)

        # Run
        if run:
            _run_script(script)

        os.chdir(cwd)

        return v_output.ns

    def add_period_constraint(self, platform, clk, period):
        clk.attr.add("keep")
        period = math.floor(period*1e3)/1e3 # round to lowest picosecond
        if clk in self.clocks:
            if period != self.clocks[clk]:
                raise ValueError("Clock already constrained to {:.2f}ns, new constraint to {:.2f}ns"
                    .format(self.clocks[clk], period))
        self.clocks[clk] = period

    def add_false_path_constraint(self, platform, from_, to):
        from_.attr.add("keep")
        to.attr.add("keep")
        if (to, from_) not in self.false_paths:
            self.false_paths.add((from_, to))

[(159, 'litex.build.tools.write_to_file', 'tools.write_to_file', False, 'from litex.build import tools\n'), (169, 'shutil.which', 'which', False, 'from shutil import which\n'), (174, 'subprocess.call', 'subprocess.call', False, 'import subprocess\n'), (195, 'os.getcwd', 'os.getcwd', False, 'import os\n'), (196, 'os.makedirs', 'os.makedirs', False, 'import os\n'), (197, 'os.chdir', 'os.chdir', False, 'import os\n'), (238, 'os.chdir', 'os.chdir', False, 'import os\n'), (141, 'litex.build.tools.get_litex_git_revision', 'tools.get_litex_git_revision', False, 'from litex.build import tools\n'), (144, 'litex.build.tools.get_litex_git_revision', 'tools.get_litex_git_revision', False, 'from litex.build import tools\n'), (244, 'math.floor', 'math.floor', False, 'import math\n'), (109, 'os.path.dirname', 'os.path.dirname', False, 'import os\n'), (46, 'litex.build.generic_platform.Pins', 'Pins', False, 'from litex.build.generic_platform import Pins, IOStandard, Misc\n')]