from spydrnet.ir import OuterPin
import spydrnet as sdn


class Comparer:
    def __init__(self, ir_orig, ir_composer):
        assert (
            ir_orig and ir_composer
        ), "Both orig_netlist and composer netlist must be specified"
        self.ir_orig = ir_orig
        self.ir_composer = ir_composer

    def run(self):
        self.compare()

    def compare(self):
        # try:
        assert self.get_identifier(self.ir_orig) == self.get_identifier(
            self.ir_composer
        ), "Environments do not have the same identifier"
        assert self.get_original_identifier(
            self.ir_orig
        ) == self.get_original_identifier(
            self.ir_composer
        ), "Environments do not have the same original identifier"
        # if there is no top instance in either then this test passes.
        if self.ir_composer.top_instance is not None or self.ir_orig.top_instance is not None:
            self.compare_instances(
                self.ir_orig.top_instance, self.ir_composer.top_instance
            )
        assert len(self.ir_orig.libraries) == len(
            self.ir_composer.libraries
        ), "Environments do not have the same number of libraries"
        # except Exception:
        #     import pdb; pdb.set_trace()
        for orig_library in self.ir_orig.libraries:
            if orig_library.name is None:
                # ports with no name are not compared
                print("WARNING: library with name is None exists and is not compared")
                continue
            else:
                patterns = orig_library.name
            composer_library = next(sdn.get_libraries(self.ir_composer, patterns))
            self.compare_libraries(orig_library, composer_library)

    def compare_libraries(self, library_orig, library_composer):
        # try:
        assert self.get_identifier(library_orig) == self.get_identifier(
            library_composer
        ), "Libraries do not have the same identifier"
        assert self.get_original_identifier(
            library_orig
        ) == self.get_original_identifier(
            library_composer
        ), "Libraries do not have the same original identifier"
        assert len(library_orig.definitions) == len(library_composer.definitions), (
            "Libraries do not have the same amount of definitions "
            + str(library_orig)
            + " "
            + str(len(library_orig.definitions))
            + " "
            + str(library_composer)
            + " "
            + str(len(library_composer.definitions))
        )
        # except Exception:
        #     import pdb; pdb.set_trace()
        for orig_definition in library_orig.definitions:
            if orig_definition.name is None:
                # ports with no name are not compared
                print(
                    "WARNING: definitions with name is None exist but are not compared"
                )
                continue
            else:
                patterns = orig_definition.name
            composer_definition = next(sdn.get_definitions(library_composer, patterns))
            self.compare_definition(orig_definition, composer_definition)

    def compare_definition(
        self, definition_orig, definition_composer, check_identifier=True
    ):
        if check_identifier:
            assert self.get_identifier(definition_orig) == self.get_identifier(
                definition_composer
            ), "Definitions do not have the same identifier"
            assert self.get_original_identifier(
                definition_orig
            ) == self.get_original_identifier(
                definition_composer
            ), "Definitions do not have the same original identifier"

        assert len(definition_orig.ports) == len(
            definition_composer.ports
        ), "Definitions do not have the same number of ports"
        # for orig_port, composer_port in zip(definition_orig.ports, definition_composer.ports):
        #     self.compare_ports(orig_port, composer_port)
        # do a smarter compare
        for orig_port in definition_orig.ports:
            if orig_port.name is None:
                # ports with no name are not compared
                print("WARNING: ports with name is None exist and are not compared")
                continue
            else:
                patterns = orig_port.name
            composer_port = next(sdn.get_ports(definition_composer, patterns))
            self.compare_ports(orig_port, composer_port)

        assert len(definition_orig.cables) == len(definition_composer.cables), (
            "Definitions do not have the same number of cables "
            + definition_orig.name
            + " "
            + str(len(definition_orig.cables))
            + " "
            + definition_composer.name
            + " "
            + str(len(definition_composer.cables))
        )

        for orig_cable in definition_orig.cables:
            if orig_cable.name is None:
                # ports with no name are not compared
                print("WARNING: cables with name is None exist and are not compared")
                continue
            else:
                patterns = orig_cable.name
            composer_cable = next(sdn.get_cables(definition_composer, patterns))
            self.compare_cables(orig_cable, composer_cable)

        assert len(definition_orig.children) == len(
            definition_composer.children
        ), "Definitions do not have the same number of instances"
        # for orig_instance, composer_cable in zip(definition_orig.children, definition_composer.children):
        #     self.compare_instances(orig_instance, composer_cable)
        for orig_instance in definition_orig.children:
            if orig_instance.name is None:
                # ports with no name are not compared
                print("WARNING: children with name is None exist and are not compared")
                continue
            else:
                if orig_instance.name.startswith("SDN_Assignment_"):
                    # skip assignment statements for now
                    continue
                else:
                    patterns = orig_instance.name

            composer_instance = next(sdn.get_instances(definition_composer, patterns))

            self.compare_instances(orig_instance, composer_instance)

        # compare assignemnt statements
        pattern = "SDN_Assignment_*"
        composer_generator = sdn.get_instances(definition_composer, pattern)
        orig_generator = sdn.get_instances(definition_orig, pattern)
        # i just need to make sure that they both contain the same number of each width assignment
        composer_dict = {}
        orig_dict = {}

        ci = next(composer_generator, None)
        while ci is not None:
            num = ci.name.split("_")[3]
            if num in composer_dict:
                composer_dict[num] += 1
            else:
                composer_dict[num] = 1
            ci = next(composer_generator, None)

        oi = next(orig_generator, None)
        while oi is not None:
            num = oi.name.split("_")[3]
            if num in orig_dict:
                orig_dict[num] += 1
            else:
                orig_dict[num] = 1
            oi = next(orig_generator, None)

        for k in composer_dict.keys():
            assert k in orig_dict and orig_dict[k] == composer_dict[k], (
                "there are a different number of " + str(k) + " width assignment"
            )

    def compare_cables(self, cable_orig, cable_composer):
        assert self.get_identifier(cable_orig) == self.get_identifier(
            cable_composer
        ), "Cables do not have the same identifier"
        assert self.get_original_identifier(cable_orig) == self.get_original_identifier(
            cable_composer
        ), "Cables do not have the same original identifier"
        assert len(cable_orig.wires) == len(cable_composer.wires), (
            "Cables do not have the same number of wires "
            + cable_orig.name
            + " "
            + str(len(cable_orig.wires))
            + " "
            + cable_composer.name
            + " "
            + str(len(cable_composer.wires))
        )
        # zip is left here because the order of wires in cables matters.
        for orig_wire, composer_wire in zip(cable_orig.wires, cable_composer.wires):
            assert len(orig_wire.pins) == len(composer_wire.pins), (
                "wires connect to a different number of pins "
                + orig_wire.cable.name
                + " "
                + composer_wire.cable.name
            )
            for orig_pin, composer_pin in zip(orig_wire.pins, composer_wire.pins):
                assert type(orig_pin) == type(
                    composer_pin
                ), "pin types do not match up."
                if isinstance(orig_pin, OuterPin):
                    self.compare_outer_pins(orig_pin, composer_pin)
                else:
                    self.compare_inner_pins(orig_pin, composer_pin)

    def compare_outer_pins(self, pin_orig, pin_composer):
        assert (
            pin_orig.instance.reference == pin_orig.inner_pin.port.definition
            and pin_composer.instance.reference
            == pin_composer.inner_pin.port.definition
        ), "DRC failure, outer pin instance reference on associated pin definition not the same"
        # try:
        assert self.are_instances_equivalent(
            pin_orig.instance, pin_composer.instance
        ), "Net does not connect to a pin on the same instance"
        # except Exception:
        #     import pdb; pdb.set_trace()

        assert self.are_inner_pins_equivalent(
            pin_orig.inner_pin, pin_orig.inner_pin
        ), "Net does not connect the same pin"

    def compare_inner_pins(self, pin_orig, pin_composer):
        assert self.are_inner_pins_equivalent(
            pin_orig, pin_composer
        ), "Net does not connect the same pin"

    def are_instances_equivalent(self, orig_instance, composer_instance):
        orig_name = self.get_identifier(orig_instance)
        composer_name = self.get_identifier(composer_instance)
        # assignment instances are not very well kept when written out so just compare the width
        if orig_name.startswith("SDN_Assignment_") and composer_name.startswith(
            "SDN_Assignment_"
        ):
            orig_split = orig_name.split("_")
            composer_split = composer_name.split("_")
            assert orig_split[3] == composer_split[3], (
                "the widths of the assignments are off "
                + orig_split[3]
                + " "
                + composer_split[3]
            )
        else:
            assert orig_name == composer_name, (
                "Names are not the same " + orig_name + " " + composer_name
            )
        assert (
            self.get_identifier(orig_instance.reference)
            == self.get_identifier(composer_instance.reference)
            and self.get_identifier(orig_instance.reference.library)
            == self.get_identifier(composer_instance.reference.library)
            and self.get_identifier(orig_instance.parent)
            == self.get_identifier(composer_instance.parent)
            and self.get_identifier(orig_instance.parent.library)
            == self.get_identifier(composer_instance.parent.library)
        ), (
            "Instances are not equivalent "
            + orig_instance.name
            + " "
            + composer_instance.name
        )
        return True

    def are_inner_pins_equivalent(self, orig_pin, composer_pin):
        assert orig_pin.port.pins.index(orig_pin) == composer_pin.port.pins.index(
            composer_pin
        ), "Pin indices do not match"
        assert (
            self.get_identifier(orig_pin.port) == self.get_identifier(composer_pin.port)
            and self.get_identifier(orig_pin.port.definition)
            == self.get_identifier(composer_pin.port.definition)
            and self.get_identifier(orig_pin.port.definition.library)
            == self.get_identifier(composer_pin.port.definition.library)
        ), "Pins are not from equivalent ports"
        return True

    def compare_ports(self, port_orig, port_composer):
        assert self.get_identifier(port_orig) == self.get_identifier(
            port_composer
        ), "Ports do not have the same identifier"
        assert self.get_original_identifier(port_orig) == self.get_original_identifier(
            port_composer
        ), "Ports do not have the same original identifier"

        assert port_orig.direction == port_composer.direction, (
            "Ports are not facing the same direction, "
            + str(port_orig.direction)
            + " "
            + str(port_composer.direction)
        )

        assert port_orig.is_array == port_composer.is_array, "Ports Array mismatch"

        assert (
            len(port_orig.pins) > 0 and len(port_composer.pins) > 0
        ), "DRC failure, ports should have at least one pin"

        assert len(port_orig.pins) == len(port_composer.pins), (
            "Ports do not have the same number of pins, "
            + port_orig.name
            + " "
            + str(len(port_orig.pins))
            + " "
            + port_composer.name
            + " "
            + str(len(port_composer.pins))
        )

        assert self.get_identifier(port_orig.definition) == self.get_identifier(
            port_composer.definition
        ) and self.get_identifier(port_orig.definition.library) == self.get_identifier(
            port_composer.definition.library
        ), "Ports do not belong to an equivalent definition"

    def compare_instances(self, instances_orig, instances_composer):
        instances_orig_identifier = self.get_identifier(instances_orig)
        instances_composer_identifier = self.get_identifier(instances_composer)
        assert instances_orig_identifier == instances_composer_identifier, (
            'Instances do not have the same identifier, orig "{}" '
            'composer "{}"'.format(
                instances_orig_identifier, instances_composer_identifier
            )
        )

        instances_orig_original_identifier = self.get_original_identifier(
            instances_orig
        )
        instances_composer_original_identifier = self.get_original_identifier(
            instances_composer
        )
        assert (
            instances_orig_original_identifier == instances_composer_original_identifier
        ), (
            "Instances do not have the same original identifier, orig "
            '"{}" composer "{}"'.format(
                instances_orig_original_identifier,
                instances_composer_original_identifier,
            )
        )

        assert (
            instances_orig.reference is None and instances_composer.reference is None
        ) or (
            self.get_identifier(instances_orig.reference)
            == self.get_identifier(instances_composer.reference)
            and self.get_identifier(instances_orig.reference.library)
            == self.get_identifier(instances_composer.reference.library)
        ), "Instances do not have the same reference definition."

        if "EDIF.properties" in instances_orig:
            assert (
                "EDIF.properties" in instances_composer
            ), "Composer is missing properties"
            properties_orig = instances_orig["EDIF.properties"]
            properties_composer = instances_composer["EDIF.properties"]
            for x in range(len(properties_orig)):
                for key, value in properties_orig[x].items():
                    assert (
                        properties_orig[x][key] == properties_composer[x][key]
                    ), "Instances do not have the same properties"

    @staticmethod
    def get_identifier(obj):
        if obj is None:
            return None
        return obj.name
        # if "EDIF.identifier" in obj:
        #     return obj["EDIF.identifier"]

    @staticmethod
    def get_original_identifier(obj):
        if obj is None:
            return None
        if "EDIF.original_identifier" in obj:
            return obj["EDIF.original_identifier"]
