# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
import os
from openram import debug
from openram import OPTS
from .hierarchy_layout import layout
from .hierarchy_spice import spice


class hierarchy_design(spice, layout):
    """
    Design Class for all modules to inherit the base features.
    Class consisting of a set of modules and instances of these modules
    """
    name_map = []

    def __init__(self, name, cell_name):
        self.drc_errors = "skipped"
        self.lvs_errors = "skipped"

        # Flag for library cells which is recomputed in hierachy_layout
        gds_file = OPTS.openram_tech + "gds_lib/" + cell_name + ".gds"
        is_library_cell = os.path.isfile(gds_file)
        # Uniquify names to address the flat GDS namespace
        # except for the top/output name
        if not is_library_cell and name != OPTS.output_name and not name.startswith(OPTS.output_name):
            name = OPTS.output_name + "_" + name
            cell_name = name

        spice.__init__(self, name, cell_name)
        layout.__init__(self, name, cell_name)
        self.init_graph_params()

    def get_layout_pins(self, inst):
        """ Return a map of pin locations of the instance offset """
        # find the instance
        for i in self.insts:
            if i.name == inst.name:
                break
        else:
            debug.error("Couldn't find instance {0}".format(inst.name), -1)
        inst_map = inst.mod.pin_map
        return inst_map

    def DRC_LVS(self, final_verification=False, force_check=False):
        """Checks both DRC and LVS for a module"""
        from openram import verify

        # No layout to check
        if OPTS.netlist_only:
            return
        # Unit tests will check themselves.
        elif not force_check and OPTS.is_unit_test:
            return
        elif not force_check and not OPTS.check_lvsdrc:
            return
        # Do not run if disabled in options.
        elif (OPTS.inline_lvsdrc or force_check or final_verification):

            tempspice = "{}.sp".format(self.name)
            self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True)
            tempgds = "{}.gds".format(self.name)
            self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds))
            # Final verification option does not allow nets to be connected by label.
            self.drc_errors = verify.run_drc(self.cell_name, tempgds, tempspice, extract=True, final_verification=final_verification)
            self.lvs_errors = verify.run_lvs(self.cell_name, tempgds, tempspice, final_verification=final_verification)

            # force_check is used to determine decoder height and other things, so we shouldn't fail
            # if that flag is set
            if OPTS.inline_lvsdrc and not force_check:
                debug.check(self.drc_errors == 0,
                            "DRC failed for {0} with {1} error(s)".format(self.cell_name,
                                                                          self.drc_errors))
                debug.check(self.lvs_errors == 0,
                            "LVS failed for {0} with {1} errors(s)".format(self.cell_name,
                                                                           self.lvs_errors))

    def DRC(self, final_verification=False):
        """Checks DRC for a module"""
        from openram import verify

        # Unit tests will check themselves.
        # Do not run if disabled in options.

        # No layout to check
        if OPTS.netlist_only:
            return
        elif (not OPTS.is_unit_test and OPTS.check_lvsdrc and (OPTS.inline_lvsdrc or final_verification)):
            tempspice = "{}.sp".format(self.name)
            self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True)
            tempgds = "{}.gds".format(self.cell_name)
            self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds))
            num_errors = verify.run_drc(self.cell_name, tempgds, tempspice, final_verification=final_verification)
            debug.check(num_errors == 0,
                        "DRC failed for {0} with {1} error(s)".format(self.cell_name,
                                                                      num_errors))

    def LVS(self, final_verification=False):
        """Checks LVS for a module"""
        from openram import verify

        # Unit tests will check themselves.
        # Do not run if disabled in options.

        # No layout to check
        if OPTS.netlist_only:
            return
        elif (not OPTS.is_unit_test and OPTS.check_lvsdrc and (OPTS.inline_lvsdrc or final_verification)):
            tempspice = "{}.sp".format(self.cell_name)
            self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True)
            tempgds = "{}.gds".format(self.name)
            self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds))
            num_errors = verify.run_lvs(self.name, tempgds, tempspice, final_verification=final_verification)
            debug.check(num_errors == 0,
                        "LVS failed for {0} with {1} error(s)".format(self.cell_name,
                                                                      num_errors))

    def init_graph_params(self):
        """
        Initializes parameters relevant to the graph creation
        """
        # Only initializes a set for checking instances which should not be added
        self.graph_inst_exclude = set()

    def build_graph(self, graph, inst_name, port_nets):
        """
        Recursively create graph from instances in module.
        """

        # Translate port names to external nets
        if len(port_nets) != len(self.pins):
            debug.error("Port length mismatch:\nExt nets={}, Ports={}".format(port_nets,
                                                                              self.pins),
                        1)
        port_dict = {pin: port for pin, port in zip(self.pins, port_nets)}
        debug.info(3, "Instance name={}".format(inst_name))
        for subinst, conns in zip(self.insts, self.conns):
            if subinst in self.graph_inst_exclude:
                continue
            subinst_name = inst_name + "{}x".format(OPTS.hier_seperator) + subinst.name
            subinst_ports = self.translate_nets(conns, port_dict, inst_name)
            subinst.mod.build_graph(graph, subinst_name, subinst_ports)

    def build_names(self, name_dict, inst_name, port_nets):
        """
        Collects all the nets and the parent inst of that net.
        """
        # Translate port names to external nets
        if len(port_nets) != len(self.pins):
            debug.error("Port length mismatch:\nExt nets={}, Ports={}".format(port_nets,
                                                                              self.pins),
                        1)
        port_dict = {pin: port for pin, port in zip(self.pins, port_nets)}
        debug.info(3, "Instance name={}".format(inst_name))
        for subinst, conns in zip(self.insts, self.conns):
            subinst_name = inst_name + "{}x".format(OPTS.hier_seperator) + subinst.name
            subinst_ports = self.translate_nets(conns, port_dict, inst_name)
            for si_port, conn in zip(subinst_ports, conns):
                # Only add for first occurrence
                if si_port.lower() not in name_dict:
                    mod_info = {'mod': self, 'int_net': conn}
                    name_dict[si_port.lower()] = mod_info
            subinst.mod.build_names(name_dict, subinst_name, subinst_ports)

    def translate_nets(self, subinst_ports, port_dict, inst_name):
        """
        Converts connection names to their spice hierarchy equivalent
        """
        converted_conns = []
        for conn in subinst_ports:
            if conn in port_dict:
                converted_conns.append(port_dict[conn])
            else:
                converted_conns.append("{0}{2}{1}".format(inst_name, conn, OPTS.hier_seperator))
        return converted_conns

    def add_graph_edges(self, graph, port_nets):
        """
        For every input, adds an edge to every output.
        Only intended to be used for gates and other simple modules.
        """
        # The final pin names will depend on the spice hierarchy, so
        # they are passed as an input.
        pin_dict = {pin: port for pin, port in zip(self.pins, port_nets)}
        input_pins = self.get_inputs()
        output_pins = self.get_outputs()
        inout_pins = self.get_inouts()
        for inp in input_pins + inout_pins:
            for out in output_pins + inout_pins:
                if inp != out: # do not add self loops
                    graph.add_edge(pin_dict[inp], pin_dict[out], self)

    def __str__(self):
        """ override print function output """
        pins = ",".join(self.pins)
        insts = ["    {}".format(x) for x in self.insts]
        objs = ["    {}".format(x) for x in self.objs]
        s = "********** design {0} **********".format(self.cell_name)
        s += "\n  pins ({0})={1}\n".format(len(self.pins), pins)
        s += "\n  objs ({0})=\n{1}\n".format(len(self.objs), "\n".join(objs))
        s += "\n  insts ({0})=\n{1}\n".format(len(self.insts), "\n".join(insts))
        return s

    def __repr__(self):
        """ override print function output """
        text="( design: " + self.name + " pins=" + str(self.pins) + " " + str(self.width) + "x" + str(self.height) + " )\n"
        for i in self.objs:
            text+=str(i) + ",\n"
        for i in self.insts:
            text+=str(i) + ",\n"
        return text

