#!/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


""" rr graph library that is not tied to the underlying serialization format
and provides simple fast lookup required to build real FPGA rr graph fabrics.
"""

from __future__ import print_function
from collections import namedtuple
from enum import Enum
from .tracks import Track
from . import channel2
from .utils import progressbar_utils


class SwitchType(Enum):
    """Enumeration of allowed VPR switch type
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-switches-switch
    """  # noqa: E501
    INVALID_SWITCH_TYPE = 0
    MUX = 1
    TRISTATE = 2
    PASS_GATE = 3
    SHORT = 4
    BUFFER = 5


class NodeType(Enum):
    """VPR Node type. This is a superset of Type in channel.py
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-node
    """  # noqa: E501
    INVALID_NODE_TYPE = 0
    CHANX = 1
    CHANY = 2
    SOURCE = 3
    SINK = 4
    OPIN = 5
    IPIN = 6


class NodeDirection(Enum):
    """VPR Node Direction. This is a superset of Direction in channel.py
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-node
    """  # noqa: E501
    NO_DIR = 0
    INC_DIR = 1
    DEC_DIR = 2
    BI_DIR = 3


class PinType(Enum):
    """Enum for PinClass type
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-blocks-pin_class
    """  # noqa: E501
    NONE = 0
    OPEN = 1
    OUTPUT = 2
    INPUT = 3


class ChannelList(namedtuple('ChannelList', 'index info')):
    """VPR `x_list` and `y_list` tags in the channels
    """


class Channels(namedtuple(
        'Channels', 'chan_width_max x_min y_min x_max y_max x_list y_list')):
    """Encapsulation for VPR channels tag
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-channel-channel
    """  # noqa: E501


class SwitchTiming(namedtuple('SwitchTiming',
                              'r c_in c_out c_internal t_del')):
    """Encapsulation for timing attributes of a VPR switch
    see: https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#switches
    """


class SwitchSizing(namedtuple('SwitchSizing', 'mux_trans_size buf_size')):
    """Encapsulation for sizing attributes of a VPR switch
    see: https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#switches
    """


class Switch(namedtuple('Switch', 'id name type timing sizing')):
    """Encapsulate VPR switch tag. Contains SwitchTiming and SwitchSizing tuples.
    see: https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#switches
    """


class SegmentTiming(namedtuple('SegmentTiming', 'r_per_meter c_per_meter')):
    """Encapsulation for timing attributes of a VPR segment.
    see: https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#wire-segments
    """


class Segment(namedtuple('Segment', 'id name timing')):
    """Encapsulate VPR segment tag. Contains SegmentTiming to encapsulate the timing attributes
    see: https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#wire-segments
    """


class Pin(namedtuple('Pin', 'ptc name')):
    """Encapsulation for VPR Pin tag
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-blocks-pin
    """  # noqa: E501


class PinClass(namedtuple('PinClass', 'type pin')):
    """Encapsulation for VPR PinClass tag
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-blocks-pin_class
    """  # noqa: E501


class BlockType(namedtuple('BlockType', 'id name width height pin_class')):
    """Encapsulation for VPR BlockType tag
    See: https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-blocks-block_type
    """  # noqa: E501


class GridLoc(namedtuple('GridLoc',
                         'x y block_type_id width_offset height_offset')):
    """
    """


class NodeTiming(namedtuple('NodeTiming', 'r c')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-timing
    """


class NodeLoc(namedtuple('NodeLoc', 'x_low y_low x_high y_high side ptc')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-loc
    """


class NodeMetadata(namedtuple('NodeMetadata',
                              'name x_offset y_offset z_offset value')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/arch/reference.html#architecture-metadata
    """


class NodeSegment(namedtuple('NodeSegment', 'segment_id')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-segment
    """


class Node(namedtuple(
        'Node', 'id type direction capacity loc timing metadata segment')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-nodes-node
    """


class Edge(namedtuple('Edge', 'src_node sink_node switch_id metadata')):
    """https://vtr-verilog-to-routing.readthedocs.io/en/latest/vpr/file_formats.html#tag-edges-edge
    """


class GraphInput(namedtuple('GraphInput',
                            'switches segments block_types grid')):
    """Top level encapsulation of input Graph
    """


def process_track(track):
    channel_model = channel2.Channel(track)
    channel_model.pack_tracks()

    return channel_model


class Graph(object):
    """ Simple object for working with VPR RR graph.

    This class does not handle serialization.  A format specific class handles
    serdes takes.
    """

    def __init__(
            self,
            switches,
            segments,
            block_types,
            grid,
            nodes,
            edges=None,
            build_pin_edges=True
    ):
        self.switches = switches
        self.next_switch_id = max(switch.id for switch in self.switches) + 1

        self.switch_name_map = {}
        self.delayless_switch = None

        for idx, switch in enumerate(self.switches):
            assert idx == switch.id
            assert switch.name not in self.switch_name_map
            self.switch_name_map[switch.name] = switch.id

        assert '__vpr_delayless_switch__' in self.switch_name_map, self.switch_name_map.keys(
        )
        self.delayless_switch = self.switch_name_map['__vpr_delayless_switch__'
                                                     ]

        self.segments = segments
        self.segment_name_map = {}

        for idx, segment in enumerate(self.segments):
            assert idx == segment.id
            assert segment.name not in self.segment_name_map
            self.segment_name_map[segment.name] = segment.id

        self.block_types = block_types
        self.grid = grid

        self.tracks = []
        self.nodes = nodes
        self.nodes.sort(key=lambda node: node.id)
        self.edges = edges if edges is not None else []

        # Map of (x, y) to GridLoc definitions.
        self.loc_map = {}

        # Maps grid location and pin class index to node index
        # (x, y, pin class idx) -> node_idx
        self.loc_pin_class_map = {}

        # Maps grid location and pin index to node index
        # (x, y, pin idx) -> [(node_idx, side)]
        self.loc_pin_map = {}

        # Maps pin name to block type id and pin idx.
        # pin name -> block type id, pin class idx, pin idx
        self.pin_name_map = {}

        self.pin_ptc_to_name_map = {}

        # Create pin_name_map and sanity check block_types.
        for idx, block_type in enumerate(self.block_types):
            assert idx == block_type.id
            for pin_class_idx, pin_class in enumerate(block_type.pin_class):
                for pin in pin_class.pin:
                    assert pin.name not in self.pin_name_map
                    self.pin_name_map[
                        pin.name] = (block_type.id, pin_class_idx, pin.ptc)
                    self.pin_ptc_to_name_map[(block_type.id,
                                              pin.ptc)] = pin.name

        # Create mapping from grid locations and pins to nodes.
        for idx, node in enumerate(self.nodes):
            assert node.id == idx, (idx, node)

            if node.type in (
                    NodeType.IPIN,
                    NodeType.OPIN,
            ):
                key = (node.loc.x_low, node.loc.y_low, node.loc.ptc)
                if key not in self.loc_pin_map:
                    self.loc_pin_map[key] = []
                self.loc_pin_map[key].append((node.id, node.loc.side))

            if node.type in (
                    NodeType.SOURCE,
                    NodeType.SINK,
            ):
                key = (node.loc.x_low, node.loc.y_low, node.loc.ptc)
                assert key not in self.loc_pin_class_map, (
                    node, self.loc_pin_class_map[key]
                )
                self.loc_pin_class_map[key] = node.id

        # Rebuild initial edges of IPIN -> SINK and SOURCE -> OPIN.
        for loc in grid:
            assert loc.block_type_id >= 0 and loc.block_type_id <= len(
                self.block_types
            ), loc.block_type_id
            block_type = self.block_types[loc.block_type_id]

            key = (loc.x, loc.y)
            assert key not in self.loc_map
            self.loc_map[key] = loc

            # Skip building IPIN -> SINK and OPIN -> SOURCE graph if edges
            # are not required.
            if not build_pin_edges:
                continue

            for pin_class_idx, pin_class in enumerate(block_type.pin_class):
                pin_class_node = self.loc_pin_class_map[
                    (loc.x, loc.y, pin_class_idx)]

                for pin in pin_class.pin:
                    for pin_node, _ in self.loc_pin_map[(loc.x, loc.y,
                                                        pin.ptc)]:
                        if pin_class.type == PinType.OUTPUT:
                            self.add_edge(
                                src_node=pin_class_node,
                                sink_node=pin_node,
                                switch_id=self.delayless_switch
                            )
                        elif pin_class.type == PinType.INPUT:
                            self.add_edge(
                                src_node=pin_node,
                                sink_node=pin_class_node,
                                switch_id=self.delayless_switch,
                            )
                        else:
                            assert False, (loc, pin_class)

    def _create_node(
            self,
            type,
            direction,
            loc,
            segment,
            timing,
            capacity=1,
            metadata=None,
    ):

        if timing is None:
            if type in (NodeType.CHANX, NodeType.CHANY):
                timing = NodeTiming(r=1, c=1)
            else:
                timing = NodeTiming(r=0, c=0)

        self.nodes.append(
            Node(
                id=len(self.nodes),
                type=type,
                direction=direction,
                capacity=capacity,
                loc=loc,
                timing=timing,
                metadata=metadata,
                segment=segment,
            )
        )

        return self.nodes[-1].id

    def get_segment_id_from_name(self, segment_name):
        return self.segment_name_map[segment_name]

    def get_delayless_switch_id(self):
        return self.delayless_switch

    def add_track(
            self,
            track,
            segment_id,
            capacity=1,
            timing=None,
            name=None,
            ptc=None,
            direction=NodeDirection.BI_DIR,
    ):
        """Take a Track and add node to the graph with supplimental data"""

        if track.direction == 'X':
            node_type = NodeType.CHANX
        elif track.direction == 'Y':
            node_type = NodeType.CHANY
        else:
            assert False, track

        if name is not None:
            metadata = [
                NodeMetadata(
                    name=name,
                    x_offset=0,
                    y_offset=0,
                    z_offset=0,
                    value='',
                )
            ]
        else:
            metadata = None

        self.tracks.append(
            self._create_node(
                type=node_type,
                direction=direction,
                capacity=capacity,
                loc=NodeLoc(
                    x_low=track.x_low,
                    y_low=track.y_low,
                    x_high=track.x_high,
                    y_high=track.y_high,
                    side=None,
                    ptc=ptc,
                ),
                timing=timing,
                segment=NodeSegment(segment_id=segment_id),
                metadata=metadata,
            )
        )

        return self.tracks[-1]

    def create_pin_name_from_tile_type_and_pin(
            self, tile_type, port_name, pin_idx=0
    ):
        return '{}.{}[{}]'.format(tile_type, port_name, pin_idx)

    def get_nodes_for_pin(self, loc, pin_name):
        block_type_id, pin_class_idx, pin_idx = self.pin_name_map[pin_name]
        grid_loc = self.loc_map[loc]
        assert grid_loc.block_type_id == block_type_id

        return self.loc_pin_map[(loc[0], loc[1], pin_idx)]

    def _create_edge(
            self, src_node, sink_node, switch_id, name=None, value=''
    ):
        assert src_node >= 0 and src_node < len(self.nodes), src_node
        assert sink_node >= 0 and sink_node < len(self.nodes), sink_node
        assert switch_id >= 0 and switch_id < len(self.switches), switch_id

        if name is not None:
            metadata = [
                NodeMetadata(
                    name=name, x_offset=0, y_offset=0, z_offset=0, value=value
                )
            ]
        else:
            metadata = None

        return Edge(
            src_node=src_node,
            sink_node=sink_node,
            switch_id=switch_id,
            metadata=metadata
        )

    def add_edge(self, src_node, sink_node, switch_id, name=None, value=''):
        """Add Edge to the graph

        Appends a new edge to the graph and retruns the index in the edges list
        """
        self.edges.append(
            self._create_edge(
                src_node=src_node,
                sink_node=sink_node,
                switch_id=switch_id,
                name=name,
                value=value
            )
        )

        return len(self.edges) - 1

    def add_switch(self, switch):
        """ Inner add_switch method.  Do not invoke directly.

        This method adds a switch into the graph model.  This method should
        not be invoked directly, instead invoke add_switch on the serialization
        graph object (e.g. rr_graph_xml.graph2.add_switch, etc).

        """

        switch_dict = switch._asdict()
        switch_dict['id'] = self.next_switch_id
        self.next_switch_id += 1

        switch = Switch(**switch_dict)

        assert switch.name not in self.switch_name_map
        self.switch_name_map[switch.name] = switch.id
        self.switches.append(switch)

        return switch.id

    def check_ptc(self):
        for node in self.nodes:
            assert node.loc.ptc is not None, node

    def set_track_ptc(self, track, ptc):
        node_d = self.nodes[track]._asdict()
        loc_d = self.nodes[track].loc._asdict()
        assert loc_d['ptc'] is None
        loc_d['ptc'] = ptc
        node_d['loc'] = NodeLoc(**loc_d)

        self.nodes[track] = Node(**node_d)

    def create_channels(self, pad_segment, pool=None):
        """ Pack tracks into channels and return Channels definition for tracks."""
        assert len(self.tracks) > 0

        xs = []
        ys = []

        for track in self.tracks:
            track_node = self.nodes[track]

            xs.append(track_node.loc.x_low)
            xs.append(track_node.loc.x_high)
            ys.append(track_node.loc.y_low)
            ys.append(track_node.loc.y_high)

        x_tracks = {}
        y_tracks = {}

        for track in self.tracks:
            track_node = self.nodes[track]

            if track_node.type == NodeType.CHANX:
                assert track_node.loc.y_low == track_node.loc.y_high

                x1, x2 = sorted((track_node.loc.x_low, track_node.loc.x_high))

                if track_node.loc.y_low not in x_tracks:
                    x_tracks[track_node.loc.y_low] = []

                x_tracks[track_node.loc.y_low].append((x1, x2, track))
            elif track_node.type == NodeType.CHANY:
                assert track_node.loc.x_low == track_node.loc.x_high

                y1, y2 = sorted((track_node.loc.y_low, track_node.loc.y_high))

                if track_node.loc.x_low not in y_tracks:
                    y_tracks[track_node.loc.x_low] = []

                y_tracks[track_node.loc.x_low].append((y1, y2, track))
            else:
                assert False, track_node

        x_list = []
        y_list = []

        x_channel_models = {}
        y_channel_models = {}

        if pool is not None:
            for y in x_tracks:
                x_channel_models[y] = pool.apply_async(
                    process_track, (x_tracks[y], )
                )

            for x in y_tracks:
                y_channel_models[x] = pool.apply_async(
                    process_track, (y_tracks[x], )
                )

        for y in progressbar_utils.progressbar(range(max(x_tracks) + 1)):
            if y in x_tracks:
                if pool is None:
                    x_channel_models[y] = process_track(x_tracks[y])
                else:
                    x_channel_models[y] = x_channel_models[y].get()

                x_list.append(len(x_channel_models[y].trees))
                for idx, tree in enumerate(x_channel_models[y].trees):
                    for i in tree:
                        self.set_track_ptc(track=i[2], ptc=idx)
            else:
                x_list.append(0)

        for x in progressbar_utils.progressbar(range(max(y_tracks) + 1)):
            if x in y_tracks:
                if pool is None:
                    y_channel_models[x] = process_track(y_tracks[x])
                else:
                    y_channel_models[x] = y_channel_models[x].get()

                y_list.append(len(y_channel_models[x].trees))
                for idx, tree in enumerate(y_channel_models[x].trees):
                    for i in tree:
                        self.set_track_ptc(track=i[2], ptc=idx)
            else:
                y_list.append(0)

        x_min = min(xs)
        y_min = min(ys)
        x_max = max(xs)
        y_max = max(ys)

        num_padding = 0
        for chan, channel_model in x_channel_models.items():
            for ptc, start, end in channel_model.fill_empty(max(x_min, 1),
                                                            x_max):
                num_padding += 1
                self.add_track(
                    track=Track(
                        direction='X',
                        x_low=start,
                        y_low=chan,
                        x_high=end,
                        y_high=chan,
                    ),
                    segment_id=pad_segment,
                    capacity=0,
                    timing=None,
                    ptc=ptc
                )

        for chan, channel_model in y_channel_models.items():
            for ptc, start, end in channel_model.fill_empty(max(y_min, 1),
                                                            y_max):
                num_padding += 1
                self.add_track(
                    track=Track(
                        direction='Y',
                        x_low=chan,
                        y_low=start,
                        x_high=chan,
                        y_high=end,
                    ),
                    segment_id=pad_segment,
                    capacity=0,
                    timing=None,
                    ptc=ptc
                )

        print('Number padding nodes {}'.format(num_padding))

        return Channels(
            chan_width_max=max(max(x_list), max(y_list)),
            x_min=x_min,
            y_min=y_min,
            x_max=x_max,
            y_max=y_max,
            x_list=[ChannelList(idx, info) for idx, info in enumerate(x_list)],
            y_list=[ChannelList(idx, info) for idx, info in enumerate(y_list)],
        )

    def block_type_at_loc(self, loc):
        return self.block_types[self.loc_map[loc].block_type_id].name

    def get_switch_id(self, switch_name):
        return self.switch_name_map[switch_name]

    def sort_nodes(self):
        self.nodes.sort(key=lambda node: node.id)
