# Copyright FuseSoC contributors
# Licensed under the 2-Clause BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-2-Clause

import importlib
import logging
import os
import shutil
from collections import OrderedDict

from ipyxact.ipyxact import Component

from fusesoc import utils
from fusesoc.capi1 import section
from fusesoc.capi1.fusesocconfigparser import FusesocConfigParser
from fusesoc.capi1.plusargs import Plusargs
from fusesoc.provider import get_provider
from fusesoc.vlnv import Vlnv

logger = logging.getLogger(__name__)


class FileSet:
    def __init__(self, name="", file=[], usage=[], private=False):
        self.name = name
        self.file = file
        self.usage = usage
        self.private = private

    def __str__(self):
        s = """Name  : {}
 Scope : {}
 Usage : {}
 Files :\n""".format(
            self.name, "private" if self.private else "public", "/".join(self.usage)
        )
        if not self.file:
            s += " <No files>\n"
        else:
            _longest_name = max([len(x.name) for x in self.file])
            _longest_type = max([len(x.file_type) for x in self.file])
            for f in self.file:
                _s = "  {} {} {}\n"
                s += _s.format(
                    f.name.ljust(_longest_name),
                    f.file_type.ljust(_longest_type),
                    "(include file)" if f.is_include_file else "",
                )
        return s


class Core:
    capi_version = 1

    def __init__(self, core_file, cache_root=""):
        basename = os.path.basename(core_file)
        self.core_file = core_file

        # Populated by CoreDB._solve(). TODO: Find a better solution for that.
        self.direct_deps = []

        self.depend = []
        self.simulators = []

        self.plusargs = None
        self.provider = None
        self.backend = None

        for s in section.SECTION_MAP:
            assert not hasattr(self, s)
            if section.SECTION_MAP[s].named:
                setattr(self, s, OrderedDict())
            else:
                setattr(self, s, None)

        self.core_root = os.path.dirname(core_file)
        self.files_root = self.core_root

        self.export_files = []

        config = FusesocConfigParser(core_file)

        # Add .system options to .core file
        system_file = os.path.join(
            self.core_root, basename.split(".core")[0] + ".system"
        )
        if os.path.exists(system_file):
            self._merge_system_file(system_file, config)

        # FIXME : Make simulators part of the core object
        self.simulator = config.get_section("simulator")
        if not "toplevel" in self.simulator:
            self.simulator["toplevel"] = "orpsoc_tb"

        for s in section.load_all(config, core_file):
            if type(s) == tuple:
                _l = getattr(self, s[0].TAG)
                _l[s[1]] = s[0]
                setattr(self, s[0].TAG, _l)
            else:
                setattr(self, s.TAG, s)

        if not self.main:
            self.main = section.MainSection()
        if self.main.name:
            self.name = Vlnv(self.main.name)
        else:
            self.name = Vlnv(basename.split(".core")[0])

        self.sanitized_name = self.name.sanitized_name

        self.depend = self.main.depend[:]

        self.simulators = self.main.simulators

        if self.main.backend:
            try:
                self.backend = getattr(self, self.main.backend)
            except AttributeError:
                raise SyntaxError('Invalid backend "{}"'.format(self.main.backend))

        self._collect_filesets()

        cache_root = os.path.join(cache_root, self.sanitized_name)
        if config.has_section("plusargs"):
            self._warning(
                "plusargs section is deprecated and will not be parsed by FuseSoC. Please migrate to parameters"
            )
            self.plusargs = Plusargs(dict(config.items("plusargs")))
        if config.has_section("verilator") and config.has_option(
            "verilator", "define_files"
        ):
            self._warning("verilator define_files are deprecated")
        if config.has_section("provider"):
            items = dict(config.items("provider"))
            patch_root = os.path.join(self.core_root, "patches")
            patches = self.main.patches
            if os.path.exists(patch_root):
                for p in sorted(os.listdir(patch_root)):
                    patches.append(os.path.join("patches", p))
            items["patches"] = patches
            provider_name = items.get("name")
            if provider_name is None:
                raise RuntimeError('Missing "name" in section [provider]')
            self.provider = get_provider(provider_name)(
                items, self.core_root, cache_root
            )
        if self.provider:
            self.files_root = self.provider.files_root

        # We need the component file here, but it might not be
        # available until the core is fetched. Try to fetch first if any
        # of the component files are missing
        if False in [os.path.exists(f) for f in self.main.component]:
            self.setup()

        for f in self.main.component:
            self._parse_component(f)

    def __repr__(self):
        return str(self.name)

    def cache_status(self):
        if self.provider:
            return self.provider.status()
        else:
            return "local"

    def get_depends(self, flags={}):
        self._debug("Getting dependencies for flags {}".format(str(flags)))
        _depends = self.depend
        try:
            _depends += getattr(self, flags["tool"]).depend
        except (AttributeError, KeyError):
            pass
        return _depends

    def get_files(self, flags={}):
        files = []
        flow = self._get_flow(flags)
        usage = {flow, flags["tool"]}

        for fs in self.file_sets:
            if (not fs.private or flags["is_toplevel"]) and (usage & set(fs.usage)):
                files += [vars(f) for f in fs.file]
        return files

    def get_parameters(self, flags={}, _parameters=None):
        self._debug("Getting parameters for flags '{}'".format(str(flags)))
        parameters = {}
        for k, v in self.parameter.items():
            if (v.scope == "public") or flags["is_toplevel"]:
                parameters[k] = {"datatype": v.datatype, "paramtype": v.paramtype}
                for field in ["default", "description"]:
                    if getattr(v, field):
                        parameters[k][field] = getattr(v, field)
        self._debug("Found parameters {}".format(parameters))
        return parameters

    def get_scripts(self, files_root, flags):
        def _build_dict(v, env):
            return [
                {"name": x, "cmd": ["sh", os.path.join(files_root, x)], "env": env}
                for x in v
            ]

        scripts = {}
        if self.scripts:
            env = {
                "FILES_ROOT": files_root,
            }
            flow = self._get_flow(flags)
            if flow == "sim":
                for s in ["pre_build_scripts", "pre_run_scripts", "post_run_scripts"]:
                    v = getattr(self.scripts, s)
                    if v:
                        scripts[s[0:-8]] = _build_dict(v, env)
            # For backwards compatibility we only use the script from the
            # top-level core in synth flows. We also rename them here to match
            # the backend stages and set the SYSTEM_ROOT env var
            elif flow == "synth" and flags["is_toplevel"]:
                env["SYSTEM_ROOT"] = self.files_root
                v = self.scripts.pre_synth_scripts
                if v:
                    scripts["pre_build"] = _build_dict(v, env)
                v = self.scripts.post_impl_scripts
                if v:
                    scripts["post_build"] = _build_dict(v, env)
        return scripts

    def get_toplevel(self, flags={}):
        self._debug("Getting toplevel for flags {}".format(str(flags)))
        if flags["tool"] == "verilator":
            toplevel = self.verilator.top_module
        elif self.backend and self._get_flow(flags) == "synth":
            toplevel = self.backend.top_module
        elif "testbench" in flags and flags["testbench"]:
            toplevel = flags["testbench"]
        else:
            toplevel = self.simulator["toplevel"]
        self._debug("Matched toplevel {}".format(toplevel))
        return toplevel

    def get_tool(self, flags):
        self._debug("Getting tool for flags {}".format(str(flags)))
        tool = None
        flow = self._get_flow(flags)
        if flags["tool"]:
            tool = flags["tool"]
        elif flags["target"] == "synth":
            if hasattr(self.main, "backend"):
                tool = self.main.backend
        else:
            if len(self.simulators) > 0:
                tool = self.simulators[0]
        self._debug(" Matched tool {}".format(tool))
        return tool

    def get_tool_options(self, flags):
        self._debug("Getting tool options for flags {}".format(str(flags)))
        options = {}
        if hasattr(self, flags["tool"]):
            section = getattr(self, flags["tool"])
        else:
            self._debug("CAPI1 does not support tool '{}'".format(flags["tool"]))
            section = None

        if section:

            # Special cases for verilator
            if flags["tool"] == "verilator":
                # Pick up verilator libs from all dependencies
                options["libs"] = section.libs

                # Set the mode option
                if flags["is_toplevel"]:
                    if self.verilator.source_type == "systemC":
                        options["mode"] = "sc"
                    else:
                        options["mode"] = "cc"
                    del self.verilator.source_type

                if self.verilator.cli_parser == "fusesoc":
                    self.verilator.cli_parser = "managed"
                elif self.verilator.cli_parser == "":
                    self.verilator.cli_parser = "passthrough"

            # Otherwise, only care about options from toplevel core
            if flags["is_toplevel"]:

                # Special case for isim. isim_options are really fuse_options
                if flags["tool"] == "isim":
                    section._members["fuse_options"] = section._members.pop(
                        "isim_options"
                    )
                    section.fuse_options = section.isim_options

                # Special case for xsim. xsim_options are really xelab_options
                # Also add flags that were previously hardcoded in the backend
                if flags["tool"] == "xsim":
                    if "xsim_options" in section._members:
                        section._members["xelab_options"] = section._members.pop(
                            "xsim_options"
                        )
                        section.xelab_options = [
                            "--timescale 1ps/1ps",
                            "--debug typical",
                        ]
                        section.xelab_options += section.xsim_options

                for member in section._members:
                    if (
                        hasattr(section, member)
                        and getattr(section, member)
                        and not member == "depend"
                    ):
                        # Strip quoted strings
                        _member = getattr(section, member)
                        if (
                            (type(_member) == str)
                            and _member.startswith('"')
                            and _member.endswith('"')
                        ):
                            _member = _member[1:-1]
                        options[member] = _member
        self._debug("Found tool options {}".format(str(options)))
        return options

    def get_vpi(self, flags):
        self._debug("Getting VPI libraries for flags {}".format(flags))
        vpi = []
        if self.vpi:
            vpi.append(
                {
                    "name": self.sanitized_name,
                    "src_files": [f.name for f in self.vpi.src_files],
                    "include_dirs": self.vpi.include_dirs,
                    "libs": [l[2:] for l in self.vpi.libs],
                }
            )
        self._debug(" Matched VPI libraries {}".format([v["name"] for v in vpi]))
        return vpi

    def get_work_root(self, flags):
        if self._get_flow(flags) == "synth":
            s = "bld-"
        else:
            s = "sim-"
        return s + flags["tool"]

    def setup(self):
        if self.provider:
            self.provider.fetch()

    def export(self, dst_dir, flags={}):
        if os.path.exists(dst_dir):
            shutil.rmtree(dst_dir)

        src_files = [f["name"] for f in self.get_files(flags)]
        if self.vpi and flags["tool"] in ["icarus", "modelsim", "rivierapro"]:
            src_files += [f.name for f in self.vpi.src_files + self.vpi.include_files]
        for section in self.get_scripts(dst_dir, flags).values():
            for script in section:
                src_files.append(script["name"])

        self._debug("Exporting {}".format(str(src_files)))

        dirs = list(set(map(os.path.dirname, src_files)))
        for d in dirs:
            if not os.path.exists(os.path.join(dst_dir, d)):
                os.makedirs(os.path.join(dst_dir, d))

        for f in src_files:
            if not os.path.isabs(f):
                if os.path.exists(os.path.join(self.core_root, f)):
                    shutil.copy2(
                        os.path.join(self.core_root, f), os.path.join(dst_dir, f)
                    )
                elif os.path.exists(os.path.join(self.files_root, f)):
                    shutil.copy2(
                        os.path.join(self.files_root, f), os.path.join(dst_dir, f)
                    )
                else:
                    raise RuntimeError(
                        "Cannot find %s in :\n\t%s\n\t%s"
                        % (f, self.files_root, self.core_root)
                    )

    def _get_flow(self, flags):
        flow = None
        if "tool" in flags:
            if flags["tool"] in [
                "ghdl",
                "icarus",
                "isim",
                "modelsim",
                "rivierapro",
                "xsim",
                "vcs",
            ]:
                flow = "sim"
            elif flags["tool"] in [
                "icestorm",
                "ise",
                "quartus",
                "verilator",
                "vivado",
                "spyglass",
                "trellis",
            ]:
                flow = "synth"
        elif "target" in flags:
            if flags["target"] == "synth":
                flow = "synth"
            elif flags["target"] == "sim":
                flow = "sim"
        return flow

    def _merge_system_file(self, system_file, config):
        def _replace(sec, src=None, dst=None):
            if not system.has_section(sec):
                return

            if not config.has_section(sec):
                config.add_section(sec)

            if src:
                if system.has_option(sec, src):
                    items = [src]
                else:
                    items = []
            else:
                items = system.options(sec)

            for item in items:
                if dst:
                    _dst = dst
                else:
                    _dst = item
                if not config.has_option(sec, _dst):
                    config.set(sec, _dst, system.get(sec, item))

        system = FusesocConfigParser(system_file)
        for section in ["icestorm", "ise", "quartus", "vivado", "trellis"]:
            _replace(section)

        _replace("main", "backend")
        _replace("scripts", "pre_build_scripts", "pre_synth_scripts")
        _replace("scripts", "post_build_scripts", "post_impl_scripts")

    def _collect_filesets(self):
        def _append_files(section, file_type, is_include_file=False):
            _files = []
            for f in section:
                if not f.file_type:
                    f.file_type = file_type
                f.is_include_file = is_include_file
                _files.append(f)
            return _files

        self.file_sets = []

        _v = self.verilog
        if _v:
            _files = _append_files(_v.include_files, _v.file_type, True)
            _files += _append_files(_v.src_files, _v.file_type)
            self.file_sets.append(
                FileSet(name="verilog_src_files", file=_files, usage=["sim", "synth"])
            )

            _files = _append_files(_v.tb_include_files, _v.file_type, True)
            _files += _append_files(_v.tb_src_files, _v.file_type)
            self.file_sets.append(
                FileSet(name="verilog_tb_src_files", file=_files, usage=["sim"])
            )
            _files = _append_files(_v.tb_private_src_files, _v.file_type)
            self.file_sets.append(
                FileSet(
                    name="verilog_tb_private_src_files",
                    file=_files,
                    usage=["sim"],
                    private=True,
                )
            )

        for k, v in self.fileset.items():
            self.file_sets.append(
                FileSet(
                    name=k, file=v.files, usage=v.usage, private=(v.scope == "private")
                )
            )

        _bname = self.main.backend
        _b = self.backend
        if _b and _bname in ["icestorm", "ise", "quartus", "trellis"]:
            _files = []
            if _bname == "icestorm":
                _files += _append_files(_b.pcf_file, "PCF")
                del _b.pcf_file
            elif _bname == "ise":
                _files += _append_files(_b.tcl_files, "tclSource")
                _files += _append_files(_b.ucf_files, "UCF")
                del _b.ucf_files
            elif _bname == "quartus":
                _files += _append_files(_b.qsys_files, "QSYS")
                _files += _append_files(_b.sdc_files, "SDC")
                _files += _append_files(_b.tcl_files, "tclSource")
                del _b.qsys_files
                del _b.sdc_files
                del _b.tcl_files
            if _files:
                self.file_sets.append(
                    FileSet(
                        name="backend_files", file=_files, usage=[_bname], private=True
                    )
                )
                self.export_files += [f.name for f in _files]
        if self.verilator:
            if self.verilator.source_type == "CPP":
                _file_type = "cppSource"
            elif self.verilator.source_type == "systemC":
                _file_type = "systemCSource"
            elif self.verilator.source_type == "C":
                _file_type = "cSource"
            else:
                raise RuntimeError(
                    "Invalid verilator file type '{}'".format(
                        self.verilator.source_type
                    )
                )
            _files = _append_files(self.verilator.src_files, _file_type)
            _files += _append_files(self.verilator.include_files, _file_type, True)
            del self.verilator.src_files
            self.file_sets.append(
                FileSet(name="verilator_src_files", file=_files, usage=["verilator"])
            )
            self.export_files += [f.name for f in _files]

            _files = _append_files(self.verilator.tb_toplevel, _file_type)
            self.file_sets.append(
                FileSet(
                    name="verilator_tb_toplevel",
                    file=_files,
                    usage=["verilator"],
                    private=True,
                )
            )
            del self.verilator.tb_toplevel
            self.export_files += [f.name for f in _files]

    def _debug(self, msg):
        logger.debug("{} : {}".format(str(self.name), msg))

    def _warning(self, msg):
        logger.warning("{} : {}".format(str(self.name), msg))

    def _parse_component(self, component_file):
        component_dir = os.path.dirname(component_file)
        component = Component()
        component.load(os.path.join(self.files_root, component_file))

        if not self.main.description:
            self.main.description = component.description

        _file_sets = []
        for file_set in component.fileSets.fileSet:
            _name = file_set.name
            for f in file_set.file:
                f.name = os.path.normpath(os.path.join(component_dir, f.name))
                self.export_files.append(f.name)
                # FIXME: Harmonize underscore vs camelcase
                f.file_type = f.fileType
                if f.isIncludeFile == "true":
                    f.is_include_file = True
                else:
                    f.is_include_file = False
                f.logical_name = f.logicalName
                f.copyto = ""
            # FIXME: Handle duplicates. Resolution function? (merge/replace, prio ipxact/core)
            _taken = False
            for fs in self.file_sets:
                if fs.name == file_set.name:
                    _taken = True
            if not _taken:
                _file_sets.append(
                    FileSet(
                        name=file_set.name,
                        file=file_set.file[:],
                        usage=["sim", "synth"],
                    )
                )
        self.file_sets += _file_sets

    def info(self):
        HEADER = """CORE INFO
Name                 : {}
Core root            : {}
Supported simulators : {}
Common dependencies  : {}\n\n"""

        s = HEADER.format(
            str(self.name),
            self.core_root,
            " ".join(self.simulators),
            " ".join([x.depstr() for x in self.main.depend]),
        )
        for sec in sorted(section.SECTION_MAP):
            if sec in ["main", "verilog", "fileset"]:
                continue
            obj = getattr(self, sec)
            if obj:
                if type(obj) == OrderedDict:
                    for k, v in obj.items():
                        s += "== {} {} ==\n{}\n".format(sec, k, v)
                else:
                    s += "== {} ==\n{}\n".format(sec, obj)
        s += "File sets:\n"
        for fs in self.file_sets:
            s += str(fs) + "\n"

        if self.main.backend:
            s += "\n== Backend {} ==\n{}\n".format(self.main.backend, self.backend)
        return s
