""" Graph object that handles serialization and deserialization from XML. """
from lib.rr_graph import graph2
from lib.rr_graph.graph2 import NodeDirection
from lib.rr_graph import tracks
import lxml.etree as ET

# Set to True once
# https://github.com/verilog-to-routing/vtr-verilog-to-routing/compare/c_internal
# is merged and included in VTR conda.
VPR_HAS_C_INTERNAL_SUPPORT = True

# For debugging purposes:
# 0 - debugging off,
# 1 - indent output XML,
# 2 - write only one element of each kind.
DEBUG = 0


def enum_from_string(enum_type, s):
    return enum_type[s.upper()]


def iterate_xml(xml_file, load_edges):
    """
    A generator function that allows to incrementally walk over an XML tree
    while reading it from a file thus allowing to greatly reduce memory
    usage.
    """
    doc = ET.iterparse(xml_file, events=('start', 'end'))
    _, root = next(doc)
    yield "", root
    path = root.tag
    in_edge = False
    for event, element in doc:
        if event == 'start':
            if in_edge:
                continue

            if not load_edges and element.tag == 'edge':
                in_edge = True
                continue
            path += "/" + element.tag
        if event == 'end':
            if in_edge:
                if element.tag == 'edge':
                    in_edge = False
                element.clear()
                continue

            path = path.rsplit('/', maxsplit=1)[0]
            yield path, element
            element.clear()

    root.clear()


def graph_from_xml(
        input_file_name, progressbar=None, filter_nodes=True, load_edges=False
):
    """
    Loads relevant information about the routing resource graph from an XML
    file.
    """

    if progressbar is None:
        progressbar = lambda x: x  # noqa: E731

    root_attrib = {}
    switches = []
    segments = []
    block_types = []
    grid = []
    nodes = []
    edges = []

    # Itertate over XML elements
    switch_timing = None
    switch_sizing = None
    segment_timing = None
    pins = []
    pin_classes = []
    node_loc = None
    node_timing = None
    node_segment = None

    for path, element in progressbar(iterate_xml(input_file_name,
                                                 load_edges=load_edges)):

        # Root tag
        if path == "" and element.tag == "rr_graph":
            root_attrib = dict(element.attrib)

        # Switch timing
        if path == "rr_graph/switches/switch" and element.tag == "timing":
            switch_timing = graph2.SwitchTiming(
                r=float(element.attrib.get('R', 0)),
                c_in=float(element.attrib.get('Cin', 0)),
                c_out=float(element.attrib.get('Cout', 0)),
                c_internal=float(element.attrib.get('Cinternal', 0)),
                t_del=float(element.attrib.get('Tdel', 0)),
            )

        # Switch sizing
        if path == "rr_graph/switches/switch" and element.tag == "sizing":
            switch_sizing = graph2.SwitchSizing(
                mux_trans_size=float(element.attrib['mux_trans_size']),
                buf_size=float(element.attrib['buf_size']),
            )

        # Switch
        if path == "rr_graph/switches" and element.tag == "switch":
            switches.append(
                graph2.Switch(
                    id=int(element.attrib['id']),
                    type=enum_from_string(
                        graph2.SwitchType, element.attrib['type']
                    ),
                    name=element.attrib['name'],
                    timing=switch_timing,
                    sizing=switch_sizing,
                )
            )

            switch_timing = None
            switch_sizing = None

        # Segment timing
        if path == "rr_graph/segments/segment" and element.tag == "timing":
            segment_timing = graph2.SegmentTiming(
                r_per_meter=float(element.get('R_per_meter', 0.0)),
                c_per_meter=float(element.get('C_per_meter', 0.0)),
            )

        # Segment
        if path == "rr_graph/segments" and element.tag == "segment":
            segments.append(
                graph2.Segment(
                    id=int(element.attrib['id']),
                    name=element.attrib['name'],
                    timing=segment_timing,
                )
            )

            segment_timing = None

        # Block type - pin
        if path == "rr_graph/block_types/block_type/pin_class" and element.tag == "pin":
            pins.append(
                graph2.Pin(
                    ptc=int(element.attrib['ptc']),
                    name=element.text,
                )
            )

        # Block type - pin_class
        if path == "rr_graph/block_types/block_type" and element.tag == "pin_class":
            pin_classes.append(
                graph2.PinClass(
                    type=enum_from_string(
                        graph2.PinType, element.attrib['type']
                    ),
                    pin=pins,
                )
            )

            pins = []

        # Block type
        if path == "rr_graph/block_types" and element.tag == "block_type":
            block_types.append(
                graph2.BlockType(
                    id=int(element.attrib['id']),
                    name=element.attrib['name'],
                    width=int(element.attrib['width']),
                    height=int(element.attrib['height']),
                    pin_class=pin_classes,
                )
            )

            pin_classes = []

        # Grid
        if path == "rr_graph/grid" and element.tag == "grid_loc":
            grid.append(
                graph2.GridLoc(
                    x=int(element.attrib['x']),
                    y=int(element.attrib['y']),
                    block_type_id=int(element.attrib['block_type_id']),
                    width_offset=int(element.attrib['width_offset']),
                    height_offset=int(element.attrib['height_offset']),
                )
            )

        # Node - loc
        if path == "rr_graph/rr_nodes/node" and element.tag == "loc":
            if 'side' in element.attrib:
                side = enum_from_string(
                    tracks.Direction, element.attrib['side']
                )
            else:
                side = None

            node_loc = graph2.NodeLoc(
                x_low=int(element.attrib['xlow']),
                y_low=int(element.attrib['ylow']),
                x_high=int(element.attrib['xhigh']),
                y_high=int(element.attrib['yhigh']),
                ptc=int(element.attrib['ptc']),
                side=side
            )

        # Node - timing
        if path == "rr_graph/rr_nodes/node" and element.tag == "timing":
            node_timing = graph2.NodeTiming(
                r=float(element.attrib['R']),
                c=float(element.attrib['C']),
            )

        # Node - segment
        if path == "rr_graph/rr_nodes/node" and element.tag == "segment":
            node_segment = int(element.attrib['segment_id'])

        # Node
        if path == "rr_graph/rr_nodes" and element.tag == "node":
            node_type = enum_from_string(
                graph2.NodeType, element.attrib['type']
            )

            if filter_nodes and node_type not in [
                    graph2.NodeType.SOURCE, graph2.NodeType.SINK,
                    graph2.NodeType.OPIN, graph2.NodeType.IPIN
            ]:
                continue

            # Dropping metadata for now
            metadata = None

            nodes.append(
                graph2.Node(
                    id=int(element.attrib['id']),
                    type=node_type,
                    direction=graph2.NodeDirection.NO_DIR,
                    capacity=int(element.attrib['capacity']),
                    loc=node_loc,
                    timing=node_timing,
                    metadata=metadata,
                    segment=node_segment,
                )
            )

            node_loc = None
            node_timing = None
            node_segment = None

        # Edge
        if path == "rr_graph/rr_edges" and element.tag == "edge":
            if load_edges:
                edges.append(
                    graph2.Edge(
                        src_node=int(element.attrib['src_node']),
                        sink_node=int(element.attrib['sink_node']),
                        switch_id=int(element.attrib['switch_id']),
                        metadata=None  # FIXME: Add reading edge metadata
                    )
                )

    return dict(
        root_attrib=root_attrib,
        switches=switches,
        segments=segments,
        block_types=block_types,
        grid=grid,
        nodes=nodes,
        edges=edges
    )


class Graph(object):
    def __init__(
            self,
            input_file_name,
            output_file_name=None,
            progressbar=None,
            build_pin_edges=True,
            rebase_nodes=True,
            filter_nodes=True,
    ):
        if progressbar is None:
            progressbar = lambda x: x  # noqa: E731

        self.input_file_name = input_file_name
        self.progressbar = progressbar
        self.output_file_name = output_file_name

        graph_input = graph_from_xml(
            input_file_name, progressbar, filter_nodes=filter_nodes
        )
        graph_input['build_pin_edges'] = build_pin_edges

        self.root_attrib = graph_input["root_attrib"]
        del graph_input["root_attrib"]

        if rebase_nodes:
            rebase_nodes = []
            for node in graph_input['nodes']:
                node_d = node._asdict()
                node_d['id'] = len(rebase_nodes)
                rebase_nodes.append(graph2.Node(**node_d))

            graph_input['nodes'] = rebase_nodes

        self.graph = graph2.Graph(**graph_input)

        self.xf = None
        self.xf_tag = []

        if DEBUG > 0:
            self._write_xml = self._write_xml_debug
        else:
            self._write_xml = self._write_xml_no_debug

    def _write_xml_debug(self, text):
        """
        Writes to the XML file
        """
        self.xf.write(" " * len(self.xf_tag) + text + "\n")

    def _write_xml_no_debug(self, text):
        self.xf.write(text)

    def _begin_xml_tag(self, tag, attrib={}, value=None, term=False):
        """
        Writes beginning of an XML tag. If term=True then terminates it
        immediately.
        """
        s = "<{}".format(tag)
        s += "".join([' {}="{}"'.format(k, str(v)) for k, v in attrib.items()])
        if value and term:
            s += ">{}</{}>".format(value, tag)
        else:
            s += "/>" if term is True else ">"
        self._write_xml(s)

        if not term:
            self.xf_tag.append(tag)

    def _end_xml_tag(self):
        """
        Finishes the current XML tag.
        """
        assert len(self.xf_tag)

        tag = self.xf_tag[-1]
        self.xf_tag = self.xf_tag[:-1]
        self._write_xml("</{}>".format(tag))

    def _write_xml_tag(self, tag, attrib={}, value=None):
        """
        A wrapper func. to write a tag and immediately close it
        """
        self._begin_xml_tag(tag, attrib, value, True)

    def _write_xml_header(self):
        """
        Writes the RR graph XML header.
        """
        self._begin_xml_tag("rr_graph", self.root_attrib)

    def _write_channels(self, channels):
        """
        Writes the RR graph channels.
        """
        self._begin_xml_tag("channels")

        attrib = {
            "chan_width_max": channels.chan_width_max,
            "x_min": channels.x_min,
            "y_min": channels.y_min,
            "x_max": channels.x_max,
            "y_max": channels.y_max,
        }
        self._write_xml_tag("channel", attrib)

        for list in channels.x_list:
            self._write_xml_tag(
                "x_list", {
                    "index": list.index,
                    "info": list.info
                }
            )
            if DEBUG >= 2:
                break
        for list in channels.y_list:
            self._write_xml_tag(
                "y_list", {
                    "index": list.index,
                    "info": list.info
                }
            )
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def _write_nodes(self, nodes, node_remap):
        """ Serialize list of Node objects to XML.

        Note that this method is extremely hot, len(nodes) is order 1-10 million.
        Almost any modification of this function has a significant effect on
        performance, so any modification to this function should be tested for
        performance and correctness before commiting.

        """

        self._begin_xml_tag("rr_nodes")

        for node in nodes:
            attrib = {
                "id": node_remap(node.id),
                "type": node.type.name,
                "capacity": node.capacity
            }

            if node.direction != NodeDirection.NO_DIR:
                attrib["direction"] = node.direction.name

            self._begin_xml_tag("node", attrib)

            attrib = {
                "xlow": node.loc.x_low,
                "xhigh": node.loc.x_high,
                "ylow": node.loc.y_low,
                "yhigh": node.loc.y_high,
                "ptc": node.loc.ptc,
            }

            if node.loc.side is not None:
                attrib["side"] = node.loc.side.name

            self._write_xml_tag("loc", attrib)

            if node.timing is not None:
                attrib = {
                    "R": node.timing.r,
                    "C": node.timing.c,
                }
                self._write_xml_tag("timing", attrib)

            if node.metadata is not None and len(node.metadata) > 0:
                self._begin_xml_tag("metadata")
                for m in node.metadata:
                    self._write_xml_tag("meta", {"name": m.name}, m.value)

                self._end_xml_tag()

            if node.segment is not None:
                attrib = {"segment_id": node.segment.segment_id}
                self._write_xml_tag("segment", attrib)

            self._end_xml_tag()
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def _write_edges(self, edges, node_remap):
        """ Serialize list of edge tuples objects to XML.

        edge tuples are (src_node(int), sink_node(int), switch_id(int), metadata(NodeMetadata)).

        metadata may be None.

        Note that this method is extremely hot, len(edges) is order 5-50 million.
        Almost any modification of this function has a significant effect on
        performance, so any modification to this function should be tested for
        performance and correctness before commiting.

        """
        self._begin_xml_tag("rr_edges")

        for src_node, sink_node, switch_id, metadata in edges:
            attrib = {
                "src_node": node_remap(src_node),
                "sink_node": node_remap(sink_node),
                "switch_id": switch_id,
            }

            if metadata is not None and len(metadata) > 0:
                self._begin_xml_tag("edge", attrib)
                self._begin_xml_tag("metadata")
                for name, value in metadata:
                    self._write_xml_tag("meta", {"name": name}, value)
                self._end_xml_tag()
                self._end_xml_tag()

            else:
                self._write_xml_tag("edge", attrib)

        self._end_xml_tag()

    def _write_switches(self):
        """
        Writes the RR graph switches.
        """
        self._begin_xml_tag("switches")

        for switch in self.graph.switches:
            attrib = {
                "id": switch.id,
                "type": switch.type.name.lower(),
                "name": switch.name,
            }
            self._begin_xml_tag("switch", attrib)

            if switch.timing:
                attrib = {
                    "R": switch.timing.r,
                    "Cin": switch.timing.c_in,
                    "Cout": switch.timing.c_out,
                    "Tdel": switch.timing.t_del,
                }

                if VPR_HAS_C_INTERNAL_SUPPORT:
                    attrib["Cinternal"] = switch.timing.c_internal

                self._write_xml_tag("timing", attrib)

            if switch.sizing:
                attrib = {
                    "mux_trans_size": switch.sizing.mux_trans_size,
                    "buf_size": switch.sizing.buf_size
                }
                self._write_xml_tag("sizing", attrib)

            self._end_xml_tag()
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def _write_segments(self):
        """
        Writes the RR graph segments.
        """
        self._begin_xml_tag("segments")

        for segment in self.graph.segments:
            attrib = {
                "id": segment.id,
                "name": segment.name,
            }
            self._begin_xml_tag("segment", attrib)

            if segment.timing:
                attrib = {
                    "R_per_meter": segment.timing.r_per_meter,
                    "C_per_meter": segment.timing.c_per_meter,
                }
                self._write_xml_tag("timing", attrib)

            self._end_xml_tag()
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def _write_block_types(self):
        """
        Writes the RR graph block types.
        """
        self._begin_xml_tag("block_types")

        for blk in self.graph.block_types:
            attrib = {
                "id": blk.id,
                "name": blk.name,
                "width": blk.width,
                "height": blk.height,
            }
            self._begin_xml_tag("block_type", attrib)

            for pin_class in blk.pin_class:
                self._begin_xml_tag(
                    "pin_class", {"type": pin_class.type.name.upper()}
                )

                for pin in pin_class.pin:
                    self._write_xml_tag("pin", {"ptc": pin.ptc}, pin.name)
                    if DEBUG >= 2:
                        break

                self._end_xml_tag()
                if DEBUG >= 2:
                    break

            self._end_xml_tag()
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def _write_grid(self):
        """
        Writes the RR graph grid.
        """
        self._begin_xml_tag("grid")

        for loc in self.graph.grid:
            attrib = {
                "x": loc.x,
                "y": loc.y,
                "block_type_id": loc.block_type_id,
                "width_offset": loc.width_offset,
                "height_offset": loc.height_offset,
            }
            self._write_xml_tag("grid_loc", attrib)
            if DEBUG >= 2:
                break

        self._end_xml_tag()

    def serialize_to_xml(
            self, channels_obj, nodes_obj, edges_obj, node_remap=lambda x: x
    ):
        """
        Writes the routing graph to the XML file.
        """

        self.graph.check_ptc()

        # Open the file
        with open(self.output_file_name, "w") as xf:
            self.xf = xf
            self.xf_tag = []

            # Write header
            self._write_xml_header()

            self._write_channels(channels_obj)

            self._write_switches()
            self._write_segments()
            self._write_block_types()
            self._write_grid()

            self._write_nodes(nodes_obj, node_remap)
            self._write_edges(edges_obj, node_remap)

            # Write footer
            self._end_xml_tag()

    def add_switch(self, switch):
        """ Add switch into graph model.

        Typically switches are imported from the architecture definition,
        however VPR will not save unused switches from the arch.  In this
        case, the switches must be added back during routing import.

        Important note: any switch present in the rr graph must also be present
        in the architecture definition.

        """

        # Add to Graph2 data structure
        switch_id = self.graph.add_switch(switch)

        return switch_id
