import logging
import os
import platform
from abc import ABCMeta
from functools import cached_property
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Union

from varname import argname

from ...dataclass import Field, validator
from ...design import Design, DesignSource, Tuple012, VhdlSettings
from ...gtkwave import gen_gtkw
from ...tool import Docker, Tool
from ...utils import SDF, common_root
from ..flow import Flow, FlowSettingsError, SimFlow, SynthFlow

log = logging.getLogger(__name__)


def _get_wave_opt_signals(wave_opt_file, extra_top=None):
    signals = []
    with open(wave_opt_file, "r") as f:
        for line in f.read().splitlines():
            line = line.strip()
            if line.startswith("/"):
                sig = line[1:].split("/")
                if extra_top:
                    sig.insert(0, extra_top)
                signals.append(sig)
    root_group = common_root(signals)
    return signals, root_group


def setting_flag(variable: Any, assign=True) -> List[str]:
    """skip if none"""
    if variable is None:
        return []
    var_name = argname("variable")
    if not isinstance(variable, (list, tuple)):
        variable = [variable]
    flags = []
    for v in variable:
        if v:
            flag = "--" + (var_name.replace("_", "-"))
            if isinstance(v, bool):
                flags.append(flag)
            elif assign:
                flags.append(flag + "=" + str(v))
            else:
                flags += [flag, v]
    return flags


class GhdlTool(Tool):
    """GHDL VHDL simulation, synthesis, and linting tool: https://ghdl.readthedocs.io"""

    docker = Docker(image="hdlc/sim:osvb")
    executable = "ghdl"

    @cached_property
    def _info(self) -> Dict[str, str]:
        out = self.run_get_stdout(
            "--version",
        )
        lines = [line.strip() for line in out.splitlines()]
        if len(lines) < 3:
            return {}
        self._version = tuple(lines[0].split("."))

        return {
            "version": ".".join(self.version),
            "compiler": lines[1],
            "backend": lines[2],
        }


class Ghdl(Flow, metaclass=ABCMeta):
    """VHDL simulation using GHDL"""

    ghdl = GhdlTool()  # pyright: reportGeneralTypeIssues=none

    class Settings(Flow.Settings):
        analysis_flags: List[str] = []
        elab_flags: List[str] = ["--syn-binding"]
        warn_flags: List[str] = [
            "--warn-binding",
            "--warn-default-binding",
            "--warn-reserved",
            "--warn-library",
            "--warn-vital-generic",
            "--warn-shared",
            "--warn-runtime-error",
            "--warn-body",
            "--warn-specs",
            "--warn-unused",
            "--warn-static",
            "--warn-parenthesis",
        ]
        werror: bool = Field(
            False, description="warnings are always considered as errors"
        )
        elab_werror: bool = Field(
            False, description="During elaboration, warnings are considered as errors"
        )
        relaxed: bool = Field(
            True,
            description="Slightly relax some rules to be compatible with various other simulators or synthesizers.",
        )
        clean: bool = Field(False, description="Run 'clean' before elaboration")
        diagnostics: bool = Field(
            True, description="Enable both color and source line carret diagnostics."
        )
        work: Optional[str] = Field(
            None, description="Set the name of the WORK library"
        )
        expect_failure: bool = False

        def common_flags(self, vhdl: VhdlSettings) -> List[str]:
            cf: List[str] = []
            if vhdl.standard:
                cf.append(f"--std={vhdl.standard}")
            if vhdl.synopsys:
                cf.append("-fsynopsys")
            if self.work:
                cf.append(f"--work={self.work}")
            cf += [f"-P{p}" for p in self.lib_paths]
            return cf

        @staticmethod
        def generics_flags(generics: Optional[Dict[str, Any]]) -> List[str]:
            if not generics:
                return []

            def conv(v):
                if isinstance(v, bool):
                    return str(v).lower()
                return v

            return [f"-g{k}={conv(v)}" for k, v in generics.items()]

        def get_flags(self, vhdl: VhdlSettings, stage: str) -> List[str]:
            common = self.common_flags(vhdl)
            warn_flags: List[str] = self.warn_flags
            analysis_flags: List[str] = common + self.analysis_flags
            elab_flags: List[str] = common + self.elab_flags
            find_top_flags = common
            if stage == "common":
                return common
            if stage == "remove":
                return common
            if self.werror:
                warn_flags += ["--warn-error"]
            elif self.elab_werror:
                elab_flags += ["--warn-error"]
            if self.relaxed:
                analysis_flags.extend(["-frelaxed-rules", "-frelaxed", "--mb-comments"])
                elab_flags.extend(["-frelaxed"])
            if self.verbose:
                analysis_flags.append("-v")
                elab_flags.append("-v")
            if self.diagnostics:
                elab_flags.extend(
                    [
                        "-fcaret-diagnostics",
                        "-fcolor-diagnostics",
                        "-fdiagnostics-show-option",
                    ]
                )
            if self.work:
                elab_flags.append("-v")
            if self.expect_failure:
                elab_flags.append("--expect-failure")
            if vhdl.synopsys:
                analysis_flags.append("--ieee=synopsys")
            if stage in ("import", "analyze"):
                return analysis_flags + warn_flags
            if stage in ("make", "elaborate"):
                return elab_flags + warn_flags
            if stage in ("find-top"):
                return find_top_flags
            raise ValueError("unknown stage!")

    def elaborate(
        self, sources: List[DesignSource], top: Union[str, Tuple012], vhdl: VhdlSettings
    ) -> Tuple012:
        """returns top(s) as a list"""
        assert isinstance(self.settings, self.Settings)
        ss = self.settings
        if isinstance(top, str):
            top = (top,)
        steps = ["import", "make"]
        if ss.clean:
            steps.insert(0, "remove")
        if not top:
            # run find-top after import
            log.warning("added find-top to steps")
            steps.insert(steps.index("import") + 1, "find-top")
        for step in steps:
            args = ss.get_flags(vhdl, step)
            if isinstance(ss, SimFlow.Settings):
                args += ss.optimization_flags
            if step in ["import", "analyze"]:
                args += [str(s) for s in sources]
            elif step in ["make", "elaborate"]:
                if self.ghdl.info.get("backend", "").lower().startswith("llvm"):
                    if platform.system() == "Darwin" and platform.machine() == "arm64":
                        args += ["-Wl,-Wl,-no_compact_unwind"]
                args += list(top)
            if step == "find-top":
                out = self.ghdl.run_get_stdout(step, *args)
                top_list = out.strip().split()
                # clunky way of converting to tuple, just to be safe and also keep mypy happy
                top = (
                    ()
                    if len(top_list) == 0
                    else (top_list[0],)
                    if len(top_list) == 1
                    else (top_list[0], top_list[1])
                )
                if top:
                    log.info("find-top: top-module was set to %s", top)
                else:
                    log.warning("find-top: unable to determine the top-module")
            else:
                self.ghdl.run(step, *args)
        return top


class GhdlSynth(Ghdl, SynthFlow):
    """
    Convert a VHDL design using 'ghdl --synth'
     (Please take a look at 'Yosys' flow (or other synthesis flows) for general VHDL, Verilog, or mixed-language synthesis targeting FPGAs or ASICs)
    """

    class Settings(Ghdl.Settings, SynthFlow.Settings):
        vendor_library: Optional[str] = Field(
            None, description="Any unit from this library is a black box"
        )
        no_formal: bool = Field(
            True,
            description="Neither synthesize assert nor PSL. Required for yosys+nextpnr flow.",
        )
        no_assert_cover: bool = Field(
            False, description="Cover PSL assertion activation"
        )
        assert_assumes: bool = Field(
            False, description="Treat all PSL asserts like PSL assumes"
        )
        assume_asserts: bool = Field(
            False, description="Treat all PSL assumes like PSL asserts"
        )
        out: Optional[
            Literal["vhdl", "raw-vhdl", "verilog", "dot", "none", "raw", "dump"]
        ] = Field(None, description="Type of output to generate")
        out_file: Optional[str] = None

    def run(self) -> None:
        design = self.design
        assert isinstance(self.settings, self.Settings)
        ss = self.settings
        top = self.elaborate(design.rtl.sources, design.rtl.top, design.language.vhdl)
        args = self.synth_args(ss, design, one_shot_elab=False, top=top)
        self.ghdl.run_stdout_to_file("synth", *args, stdout=ss.out_file)

    @staticmethod
    def synth_args(
        ss: Settings, design: Design, one_shot_elab: bool = True, top: Tuple012 = ()
    ) -> List[str]:
        flags = ss.get_flags(design.language.vhdl, "elaborate")
        flags += setting_flag(ss.vendor_library)
        flags += setting_flag(ss.out)
        flags += setting_flag(ss.no_formal)
        flags += setting_flag(ss.no_assert_cover)
        flags += setting_flag(ss.assert_assumes)
        flags += setting_flag(ss.assume_asserts)

        flags.extend(ss.generics_flags(design.rtl.generics))
        if one_shot_elab:
            flags += [str(v) for v in design.rtl.sources if v.type == "vhdl"]
            flags.append("-e")
        if not top:
            top = (design.rtl.top,)
        flags.extend(list(top))
        return flags


# class GhdlLint(Ghdl, Flow):
#     """Lint VHDL sources using GHDL"""

#     class Settings(Ghdl.Settings):
#         pass


class GhdlSim(Ghdl, SimFlow):
    """Simulate a VHDL design using GHDL"""

    cocotb_sim_name = "ghdl"

    class Settings(Ghdl.Settings, SimFlow.Settings):
        run_flags: List[str] = []
        optimization_flags: List[str] = Field(
            ["-O3"], description="Simulation optimization flags"
        )
        asserts: Optional[Literal["disable", "disable-at-0"]] = None
        ieee_asserts: Optional[Literal["disable", "disable-at-0"]] = "disable-at-0"
        sdf: SDF = Field(
            SDF(),
            description="Do VITAL annotation using SDF files(s). A single string is interpreted as a MAX SDF file.",
        )
        wave: Optional[str] = Field(
            None, description="Write the waveforms into a GHDL Waveform (GHW) file."
        )
        read_wave_opt: Optional[str] = Field(
            None,
            description="Filter signals to be dumped to the wave file according to the wave option file provided.",
        )
        write_wave_opt: Optional[str] = Field(
            None,
            description="Creates a wave option file with all the signals of the design. Overwrites the file if it already exists.",
        )
        fst: Optional[str] = Field(
            None, description="Write the waveforms into an _fst_ file."
        )
        stop_delta: Optional[str] = Field(
            None,
            description="Stop the simulation after N delta cycles in the same current time.",
        )
        disp_tree: Optional[Literal["inst", "proc", "port"]] = Field(
            None,
            description="Display the design hierarchy as a tree of instantiated design entities. See GHDL documentation for more details.",
        )
        vpi: Union[None, str, List[str]] = Field(
            None,
            description="Load VPI library (or multiple libraries)",
        )
        # TODO workdir?

        @validator("wave", "fst", pre=True)
        def validate_wave(cls, value, field):  # pylint: disable=no-self-argument
            if value is not None:
                ext = (
                    ".ghw"
                    if field.name == "wave"
                    else ".fst"
                    if field.name == "fst"
                    else ""
                )
                if isinstance(value, bool):
                    value = "dump" + ext if value else None
                else:
                    if not isinstance(value, str):
                        value = str(value)
                    if not value.endswith(ext):
                        value += ext
            return value

    def run(self) -> None:
        design = self.design
        assert design.tb
        assert isinstance(self.settings, self.Settings)
        ss = self.settings
        cf = ss.common_flags(design.language.vhdl)
        run_flags = self.settings.run_flags
        sdf_root = ss.sdf.root if ss.sdf.root else design.tb.uut
        for delay_type, sdf_file in ss.sdf.delay_items():
            if sdf_file:
                assert sdf_root, "neither SDF root nor tb.uut are provided"
                run_flags.append(f"--sdf={delay_type}={sdf_root}={sdf_file}")

        def fp(s: Union[str, os.PathLike]) -> str:
            if not os.path.isabs(s):
                return str(self.run_path.relative_to(Path.cwd()) / s)
            return str(s)

        if ss.vcd:
            if ss.vcd.endswith((".gz", ".vcdgz")):
                run_flags.append(f"--vcdgz={ss.vcd}")
            else:
                run_flags.append(f"--vcd={ss.vcd}")
            log.warning("Dumping VCD to %s", fp(ss.vcd))
        if ss.fst:
            run_flags += setting_flag(ss.fst)
            log.warning("Dumping fst to %s", fp(ss.fst))
        if ss.wave:
            run_flags += setting_flag(ss.wave)
            log.warning("Dumping GHW to %s", fp(ss.wave))

        if ss.wave or ss.vcd or ss.fst:
            if not ss.read_wave_opt and not ss.write_wave_opt:
                ss.write_wave_opt = "wave.opt"

        if ss.write_wave_opt:
            p = Path(ss.write_wave_opt)
            if p.exists():
                log.warning("Deleting existing wave option file: %s", p)
                p.unlink()
        run_flags += setting_flag(ss.write_wave_opt)
        if ss.read_wave_opt:
            if not Path(ss.read_wave_opt).exists():  # TODO move to validation
                raise FlowSettingsError(
                    [
                        (
                            "read_wave_opt",
                            f"File {ss.read_wave_opt} does not exist",
                            None,
                        )
                    ],
                    self.Settings,
                )
        run_flags += setting_flag(ss.read_wave_opt)
        run_flags += setting_flag(ss.disp_tree)
        run_flags += setting_flag(ss.asserts)
        run_flags += setting_flag(ss.ieee_asserts)

        vpi = [] if ss.vpi is None else [ss.vpi] if not isinstance(ss.vpi, (list, tuple)) else list(ss.vpi)
        # TODO factor out cocotb handling
        if design.tb.cocotb and self.cocotb:
            vpi.append(self.cocotb.vpi_path())
            # tb_generics = list(design.tb.generics)  # TODO pass to cocotb?
            design.tb.generics = design.rtl.generics
        run_flags += setting_flag(vpi)

        if ss.debug:
            run_flags.extend(
                [
                    "--trace-processes",
                    "--checks",
                ]
            )

        run_flags += setting_flag(ss.stop_time)
        run_flags += setting_flag(ss.stop_delta)

        run_flags.extend(ss.generics_flags(design.tb.generics))

        x = self.elaborate(design.sim_sources, design.tb.top, design.language.vhdl)
        design.tb.top = x
        assert self.cocotb
        self.ghdl.run(
            "run",
            *cf,
            *design.sim_tops,
            *run_flags,
            env=self.cocotb.env(design),
        )

    def parse_reports(self) -> bool:
        success = True
        assert isinstance(self.settings, self.Settings)
        ss = self.settings

        dump_file = ss.wave or ss.vcd or ss.fst
        if dump_file:
            print("dump_file=", dump_file)
            opt_file = ss.read_wave_opt or ss.write_wave_opt
            extra_top = "top" if ss.wave else None
            signals, root_group = _get_wave_opt_signals(opt_file, extra_top)
            if dump_file == ss.fst:  # fst removes the common hierarchy
                signals = [s[len(root_group) :] for s in signals]
                root_group = []
            gen_gtkw(dump_file, signals, root_group)

        # TODO move
        if self.cocotb and self.design.tb and self.design.tb.cocotb:
            success &= self.cocotb.add_results(self.results)
        return success
