# Copyright 2023 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import json
from typing import List, Optional
from abc import abstractmethod

from .step import Step
from .tclstep import TclStep
from .common_variables import constraint_variables
from ..state import State
from ..state import DesignFormat
from ..common import get_script_dir
from ..config import Path, Variable, StringEnum


class YosysStep(TclStep):
    def get_command(self) -> List[str]:
        return ["yosys", "-c", self.get_script_path()]

    @abstractmethod
    def get_script_path(self):
        pass


@Step.factory.register()
class Synthesis(YosysStep):
    """
    Performs synthesis and technology mapping using Yosys and ABC, emitting a
    netlist. Requires Yosys 0.26 or higher.

    Some metrics will also be extracted and updated, namely:

    * ``design__instance__count``
    * ``design__instance_unmapped__count``

    If using Yosys 0.27 or higher:

    * ``design__instance__area`` is also updated.
    """

    id = "Yosys.Synthesis"
    inputs = []  # The input RTL is part of the configuration
    outputs = [DesignFormat.NETLIST]

    config_vars = constraint_variables + [
        Variable(
            "VERILOG_FILES",
            List[Path],
            "The paths of the design's Verilog files.",
        ),
        Variable(
            "QUIT_ON_SYNTH_CHECKS",
            bool,
            "Use yosys `check -assert` at the end of synthesis. This checks for combinational loops, conflicting drivers and wires with no drivers.",
            default=True,
        ),
        Variable(
            "SYNTH_AUTONAME",
            bool,
            "Generates names for  netlistinstances. This results in instance names that can be very long, but are more human-readable.",
            default=False,
        ),
        Variable(
            "SYNTH_STRATEGY",
            StringEnum(
                "SYNTH_STRATEGY",
                [
                    "AREA 0",
                    "AREA 1",
                    "AREA 2",
                    "AREA 3",
                    "AREA 4",
                    "DELAY 0",
                    "DELAY 1",
                    "DELAY 2",
                    "DELAY 3",
                    "DELAY 4",
                ],
            ),
            "Strategies for abc logic synthesis and technology mapping. AREA strategies usually result in a more compact design, while DELAY strategies usually result in a design that runs at a higher frequency. Please note that there is no way to know which strategy is the best before trying them.",
            default="AREA 0",
        ),
        Variable(
            "SYNTH_BUFFERING",
            bool,
            "Enables `abc` cell buffering.",
            default=True,
        ),
        Variable(
            "SYNTH_SIZING",
            bool,
            "Enables `abc` cell sizing (instead of buffering).",
            default=False,
        ),
        Variable(
            "SYNTH_READ_BLACKBOX_LIB",
            bool,
            "Additionally read the liberty file(s) as a blackbox. This will allow RTL designs to incorporate explicitly declared standard cells that will not be tech-mapped or reinterpreted.",
            default=False,
        ),
        Variable(
            "SYNTH_NO_FLAT",
            bool,
            "A flag that disables flattening the hierarchy during synthesis, only flattening it after synthesis, mapping and optimizations.",
            default=False,
        ),
        Variable(
            "SYNTH_SHARE_RESOURCES",
            bool,
            "A flag that enables yosys to reduce the number of cells by determining shareable resources and merging them.",
            default=True,
        ),
        Variable(
            "SYNTH_ADDER_TYPE",
            StringEnum("SYNTH_ADDER_TYPE", ["YOSYS", "FA", "RCA", "CSA"]),
            "Adder type to which the $add and $sub operators are mapped to.  Possible values are `YOSYS/FA/RCA/CSA`; where `YOSYS` refers to using Yosys internal adder definition, `FA` refers to full-adder structure, `RCA` refers to ripple carry adder structure, and `CSA` refers to carry select adder.",
            default="YOSYS",
        ),
        Variable(
            "SYNTH_EXTRA_MAPPING_FILE",
            Optional[Path],
            "Points to an extra techmap file for yosys that runs right after yosys `synth` before generic techmap.",
        ),
        Variable(
            "SYNTH_PARAMETERS",
            Optional[List[str]],
            "Key-value pairs to be `chparam`ed in Yosys, in the format `key1=value1`.",
        ),
        Variable(
            "SYNTH_ELABORATE_ONLY",
            bool,
            '"Elaborate" the design only without attempting any logic mapping. Useful when dealing with structural Verilog netlists.',
            default=False,
        ),
        Variable(
            "SYNTH_FLAT_TOP",
            bool,
            "Specifies whether or not the top level should be flattened during elaboration.",
            default=False,
        ),
        Variable(
            "VERILOG_INCLUDE_DIRS",
            Optional[List[str]],
            "Specifies the Verilog `include` directories.",
        ),
    ]

    def get_script_path(self):
        return os.path.join(get_script_dir(), "yosys", "synthesize.tcl")

    def run(
        self,
        **kwargs,
    ) -> State:
        assert isinstance(self.config["LIB"], list)

        kwargs, env = self.extract_env(kwargs)

        lib_synth = self.toolbox.remove_cells_from_lib(
            frozenset(self.config["LIB"]),
            excluded_cells=frozenset(
                [
                    self.config["BAD_CELL_LIST"],
                    self.config["NO_SYNTH_CELL_LIST"],
                ]
            ),
            as_cell_lists=True,
        )

        env["LIB_SYNTH"] = lib_synth
        state_out = super().run(env=env, **kwargs)

        stats_file = os.path.join(self.step_dir, "reports", "stat.json")
        stats_str = open(stats_file).read()
        stats = json.loads(stats_str)

        state_out.metrics["design__instance__count"] = stats["design"]["num_cells"]
        if chip_area := stats["design"].get("area"):  # needs nonzero area
            state_out.metrics["design__instance__area"] = chip_area

        cells = stats["design"]["num_cells_by_type"]
        unmapped_keyword = "$"
        unmapped_cells = [
            cells[y] for y in cells.keys() if y.startswith(unmapped_keyword)
        ]
        state_out.metrics["design__instance_unmapped__count"] = sum(unmapped_cells)

        return state_out
