#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020-2021  The SymbiFlow Authors.
#
# Use of this source code is governed by a ISC-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/ISC
#
# SPDX-License-Identifier: ISC


import os.path
import re
from rr_graph import graph2
from rr_graph import tracks
import gc

import capnp
import capnp.lib.capnp
capnp.remove_import_hook()

CAMEL_CASE_CAPITALS = re.compile('([A-Z]+)')

ENUM_CACHE = {}


def enum_from_string(enum_type, s):
    if s == 'uxsdInvalid':
        return None

    s = str(s)
    key = (id(enum_type), s)
    if key not in ENUM_CACHE:
        ENUM_CACHE[key] = enum_type[CAMEL_CASE_CAPITALS.sub(r'_\1', s).upper()]
    return ENUM_CACHE[key]


CAPNP_ENUM_CACHE = {}


def to_capnp_enum(enum_type, e):
    key = (id(enum_type), e)

    if key not in CAPNP_ENUM_CACHE:
        # Convert from snake_case to camelCase.
        parts = []
        for idx, part in enumerate(e.name.split('_')):
            if idx == 0:
                parts.append(part.lower())
            else:
                parts.append(part.capitalize())
        camel_case_e = "".join(parts)

        CAPNP_ENUM_CACHE[key] = enum_type.__dict__[camel_case_e]

    return CAPNP_ENUM_CACHE[key]


def cleanup_capnp_leak(f):
    """ Cleanup capnp leak resulting from _parent pointers. """
    popped = set()
    strays = {}

    # Some strays hold a reference to the input file
    strays.update(
        (id(obj), obj)
        for obj in gc.get_referrers(f)
        if 'capnp' in str(type(obj))
    )

    # Some strays are "floating"
    for obj in gc.get_objects():
        type_str = str(type(obj))
        if 'capnp.lib.capnp._DynamicStructReader' in type_str:
            strays[id(obj)] = obj

    if len(strays) > 0:
        # First expand all strays and find other capnp objects that still hold
        # a reference to them (via the _parent pointer).
        for obj_id in set(strays.keys()) - popped:
            popped.add(obj_id)
            strays.update(
                (id(obj), obj)
                for obj in gc.get_referrers(strays[obj_id])
                if 'capnp' in str(type(obj))
            )

        # Clear their _parent pointer
        for obj in strays.values():
            obj._parent = None

        # Make sure none of the strays are still referred to by anything
        for obj in strays.values():
            capnp_refs = [
                None for obj in gc.get_referrers(strays[obj_id])
                if 'capnp' in str(type(obj))
            ]
            assert len(capnp_refs) == 0

        # Make sure the file is not referenced by any files.
        capnp_refs = [
            None for obj in gc.get_referrers(f) if 'capnp' in str(type(obj))
        ]
        assert len(capnp_refs) == 0


def read_switch(sw):
    timing = sw.timing
    sizing = sw.sizing

    return graph2.Switch(
        id=sw.id,
        name=str(sw.name),
        type=enum_from_string(graph2.SwitchType, sw.type),
        timing=graph2.SwitchTiming(
            r=timing.r,
            c_in=timing.cin,
            c_out=timing.cout,
            c_internal=timing.cinternal,
            t_del=timing.tdel,
        ),
        sizing=graph2.SwitchSizing(
            buf_size=sizing.bufSize,
            mux_trans_size=sizing.muxTransSize,
        ),
    )


def read_segment(seg):
    timing = seg.timing
    return graph2.Segment(
        id=seg.id,
        name=str(seg.name),
        timing=graph2.SegmentTiming(
            r_per_meter=timing.rPerMeter,
            c_per_meter=timing.cPerMeter,
        )
    )


def read_pin(pin):
    return graph2.Pin(
        ptc=pin.ptc,
        name=str(pin.value),
    )


def read_pin_class(pin_class):
    return graph2.PinClass(
        type=enum_from_string(graph2.PinType, pin_class.type),
        pin=[read_pin(pin) for pin in pin_class.pins]
    )


def read_block_type(block_type):
    return graph2.BlockType(
        id=block_type.id,
        name=str(block_type.name),
        width=block_type.width,
        height=block_type.height,
        pin_class=[
            read_pin_class(pin_class) for pin_class in block_type.pinClasses
        ]
    )


def read_grid_loc(grid_loc):
    return graph2.GridLoc(
        x=grid_loc.x,
        y=grid_loc.y,
        block_type_id=grid_loc.blockTypeId,
        width_offset=grid_loc.widthOffset,
        height_offset=grid_loc.heightOffset,
    )


def read_metadata(metadata):
    if len(metadata.metas) == 0:
        return None
    else:
        return [(str(m.name), str(m.value)) for m in metadata.metas]


def read_node(node, new_node_id=None):
    node_loc = node.loc
    node_timing = node.timing

    return graph2.Node(
        id=new_node_id if new_node_id is not None else node.id,
        type=enum_from_string(graph2.NodeType, node.type),
        direction=enum_from_string(graph2.NodeDirection, node.direction),
        capacity=node.capacity,
        loc=graph2.NodeLoc(
            x_low=node_loc.xlow,
            y_low=node_loc.ylow,
            x_high=node_loc.xhigh,
            y_high=node_loc.yhigh,
            ptc=node_loc.ptc,
            side=enum_from_string(tracks.Direction, node_loc.side),
        ),
        timing=graph2.NodeTiming(r=node_timing.r, c=node_timing.c),
        metadata=None,
        segment=graph2.NodeSegment(segment_id=node.segment.segmentId),
    )


def read_edge(edge):
    return graph2.Edge(
        src_node=edge.srcNode,
        sink_node=edge.sinkNode,
        switch_id=edge.switchId,
        metadata=read_metadata(edge.metadata),
    )


def graph_from_capnp(
        rr_graph_schema,
        input_file_name,
        progressbar=None,
        filter_nodes=True,
        load_nodes=True,
        load_edges=False,
        rebase_nodes=False,
):
    """
    Loads relevant information about the routing resource graph from an capnp
    file.
    """
    if rebase_nodes:
        assert load_nodes
        assert not load_edges

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

    with open(input_file_name, 'rb') as f:
        graph = rr_graph_schema.RrGraph.read(
            f, traversal_limit_in_words=2**63 - 1
        )

        root_attrib = {
            'tool_comment': str(graph.toolComment),
            'tool_name': str(graph.toolName),
            'tool_version': str(graph.toolVersion),
        }

        switches = [read_switch(sw) for sw in graph.switches.switches]
        segments = [read_segment(seg) for seg in graph.segments.segments]
        block_types = [
            read_block_type(block_type)
            for block_type in graph.blockTypes.blockTypes
        ]
        grid = [read_grid_loc(g) for g in graph.grid.gridLocs]

        nodes = []
        if load_nodes:
            for n in progressbar(graph.rrNodes.nodes):
                if filter_nodes and n.type not in ['source', 'sink', 'opin', 'ipin'
                                                ]:
                    continue

                if rebase_nodes:
                    node = read_node(n, new_node_id=len(nodes))
                else:
                    node = read_node(n)

                nodes.append(node)

        edges = []
        if load_edges:
            edges = [read_edge(e) for e in graph.rrEdges.edges]

        # File back capnp objects cannot outlive their input file,
        # so verify that no dangling references exist.
        del graph
        gc.collect()

        # Cleanup leaked capnp objects due to _parent in Cython.
        cleanup_capnp_leak(f)

        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,
            rr_graph_schema_fname,
            input_file_name,
            output_file_name=None,
            progressbar=None,
            build_pin_edges=True,
            rebase_nodes=True,
            filter_nodes=True,
            load_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

        self.rr_graph_schema = capnp.load(
            rr_graph_schema_fname,
            imports=[os.path.dirname(os.path.dirname(capnp.__file__))]
        )

        graph_input = graph_from_capnp(
            rr_graph_schema=self.rr_graph_schema,
            input_file_name=input_file_name,
            progressbar=progressbar,
            filter_nodes=filter_nodes,
            rebase_nodes=rebase_nodes,
            load_nodes=load_nodes,
        )
        graph_input['build_pin_edges'] = build_pin_edges

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

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

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

        rr_graph.channels.channel.chanWidthMax = channels.chan_width_max
        rr_graph.channels.channel.xMax = channels.x_max
        rr_graph.channels.channel.xMin = channels.x_min
        rr_graph.channels.channel.yMax = channels.y_max
        rr_graph.channels.channel.yMin = channels.y_min

        xLists = rr_graph.channels.init('xLists', len(channels.x_list))
        for out_x_list, x_list in zip(xLists, channels.x_list):
            out_x_list.index = x_list.index
            out_x_list.info = x_list.info

        yLists = rr_graph.channels.init('yLists', len(channels.y_list))
        for out_y_list, y_list in zip(yLists, channels.y_list):
            out_y_list.index = y_list.index
            out_y_list.info = y_list.info

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

        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.

        """

        rr_nodes = rr_graph.rrNodes.init('nodes', num_nodes)

        nodes_written = 0

        node_iter = iter(nodes)

        for out_node, node in zip(rr_nodes, node_iter):
            nodes_written += 1

            out_node.id = node_remap(node.id)
            out_node.type = to_capnp_enum(
                self.rr_graph_schema.NodeType, node.type
            )
            out_node.capacity = node.capacity

            if node.direction is not None:
                out_node.direction = to_capnp_enum(
                    self.rr_graph_schema.NodeDirection, node.direction
                )

            node_loc = out_node.loc
            node_loc.ptc = node.loc.ptc
            if node.loc.side is not None:
                node_loc.side = to_capnp_enum(
                    self.rr_graph_schema.LocSide, node.loc.side
                )
            node_loc.xhigh = node.loc.x_high
            node_loc.xlow = node.loc.x_low
            node_loc.yhigh = node.loc.y_high
            node_loc.ylow = node.loc.y_low

            if node.timing is not None:
                timing = out_node.timing
                timing.c = node.timing.c
                timing.r = node.timing.r

            if node.segment is not None:
                segment = out_node.segment
                segment.segmentId = node.segment.segment_id

            if node.metadata is not None and len(node.metadata) > 0:
                metas = out_node.metadata.init('metas', len(node.metadata))
                for out_meta, meta in zip(metas, node.metadata):
                    out_meta.name = meta.name
                    out_meta.value = meta.value

        assert nodes_written == num_nodes, 'Unwritten nodes!'

        try:
            _ = next(node_iter)
            assert False, 'Unwritten nodes!'
        except StopIteration:
            pass

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

        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.

        """

        out_edges = rr_graph.rrEdges.init('edges', num_edges)

        edges_written = 0
        edges_iter = iter(edges)
        for out_edge, (src_node, sink_node, switch_id,
                       metadata) in zip(out_edges, edges_iter):
            edges_written += 1
            out_edge.srcNode = node_remap(src_node)
            out_edge.sinkNode = node_remap(sink_node)
            out_edge.switchId = switch_id

            if metadata is not None and len(metadata) > 0:
                metas = out_edge.metadata.init('metas', len(metadata))
                for out_meta, (name, value) in zip(metas, metadata):
                    out_meta.name = name
                    out_meta.value = value

        assert edges_written == num_edges, 'Unwritten edges!'

        try:
            _ = next(edges_iter)
            assert False, 'Unwritten edges!'
        except StopIteration:
            pass

    def _write_switches(self, rr_graph):
        """
        Writes the RR graph switches.
        """
        switches = rr_graph.switches.init('switches', len(self.graph.switches))
        for out_switch, switch in zip(switches, self.graph.switches):
            out_switch.id = switch.id
            out_switch.name = switch.name
            out_switch.type = to_capnp_enum(
                self.rr_graph_schema.SwitchType, switch.type
            )

            if switch.timing:
                timing = out_switch.timing
                timing.cin = switch.timing.c_in
                timing.cinternal = switch.timing.c_internal
                timing.cout = switch.timing.c_out
                timing.r = switch.timing.r
                timing.tdel = switch.timing.t_del

            if switch.sizing:
                sizing = out_switch.sizing
                sizing.bufSize = switch.sizing.buf_size
                sizing.muxTransSize = switch.sizing.mux_trans_size

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

        segments = rr_graph.segments.init('segments', len(self.graph.segments))

        for out_segment, segment in zip(segments, self.graph.segments):
            out_segment.id = segment.id
            out_segment.name = segment.name

            if segment.timing:
                timing = out_segment.timing
                timing.cPerMeter = segment.timing.c_per_meter
                timing.rPerMeter = segment.timing.r_per_meter

    def _write_block_types(self, rr_graph):
        """
        Writes the RR graph block types.
        """

        block_types = rr_graph.blockTypes.init(
            'blockTypes', len(self.graph.block_types)
        )

        for out_blk, blk in zip(block_types, self.graph.block_types):
            out_blk.id = blk.id
            out_blk.name = blk.name
            out_blk.width = blk.width
            out_blk.height = blk.height

            pin_classes = out_blk.init('pinClasses', len(blk.pin_class))

            for out_pin_class, pin_class in zip(pin_classes, blk.pin_class):
                out_pin_class.type = to_capnp_enum(
                    self.rr_graph_schema.PinType, pin_class.type
                )

                pins = out_pin_class.init('pins', len(pin_class.pin))

                for out_pin, pin in zip(pins, pin_class.pin):
                    out_pin.ptc = pin.ptc
                    out_pin.value = pin.name

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

        grid_locs = rr_graph.grid.init('gridLocs', len(self.graph.grid))
        for out_grid_loc, grid_loc in zip(grid_locs, self.graph.grid):
            out_grid_loc.x = grid_loc.x
            out_grid_loc.y = grid_loc.y
            out_grid_loc.blockTypeId = grid_loc.block_type_id
            out_grid_loc.widthOffset = grid_loc.width_offset
            out_grid_loc.heightOffset = grid_loc.height_offset

    def serialize_to_capnp(
            self,
            channels_obj,
            num_nodes,
            nodes_obj,
            num_edges,
            edges_obj,
            node_remap=lambda x: x
    ):
        """
        Writes the routing graph to the capnp file.
        """

        self.graph.check_ptc()

        rr_graph = self.rr_graph_schema.RrGraph.new_message()
        rr_graph.toolComment = self.root_attrib['tool_comment']
        rr_graph.toolName = self.root_attrib['tool_name']
        rr_graph.toolVersion = self.root_attrib['tool_version']

        self._write_channels(rr_graph, channels_obj)
        self._write_switches(rr_graph)
        self._write_segments(rr_graph)
        self._write_block_types(rr_graph)
        self._write_grid(rr_graph)
        self._write_nodes(rr_graph, num_nodes, nodes_obj, node_remap)
        self._write_edges(rr_graph, num_edges, edges_obj, node_remap)

        # Open the file
        with open(self.output_file_name, "wb") as f:
            rr_graph.write(f)

    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
