# Copyright (c) 2018-2020, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

__all__ = ["GraphNodeDelegateRoutingError", "GraphNodeDelegateRouter", "RoutingCondition"]

from .abstract_graph_node_delegate import AbstractGraphNodeDelegate
from .abstract_graph_node_delegate import GraphConnectionDescription
from .abstract_graph_node_delegate import GraphNodeDescription
from .abstract_graph_node_delegate import GraphPortDescription
from collections import namedtuple


RoutingCondition = namedtuple("RoutingCondition", ["type", "expression", "delegate"])


class GraphNodeDelegateRoutingError(Exception):
    """
    This exception is used when it's not possible to do routing. Can only
    happen if there is no default route in the table.
    """

    pass


class GraphNodeDelegateRouter(AbstractGraphNodeDelegate):
    """
    The delegate that keeps multiple delegates and pick them depending on the
    routing conditions.

    It's possible to add the routing conditions with `add_route`, and
    conditions could be a type or a lambda expression.

    The latest added routing is stronger than previously added. Routing added
    without conditions is the default.

    We use type routing to make the specific kind of nodes unique, and also
    we can use the lambda function to make the particular state of nodes
    unique (ex. full/collapsed).

    It's possible to use type and lambda routing at the same time.

    Usage examples:

       delegate.add_route(TextureDelegate(), type="Texture2d")
       delegate.add_route(CollapsedDelegate(), expressipon=is_collapsed)
    """

    def __init__(self):
        super().__init__()
        self.__routing_table = []

    def add_route(self, delegate: AbstractGraphNodeDelegate, type=None, expression=None):
        """Add delegate to the routing tablle"""
        if not delegate:
            return

        if type is None and expression is None:
            # It's a default delegate, we don't need what we had before
            self.__routing_table.clear()

        self.__routing_table.append(RoutingCondition(type, expression, delegate))

    def __route(self, model, node):
        """Return the delegate for the given node"""
        for c in reversed(self.__routing_table):
            if (c.type is None or model[node].type == c.type) and (c.expression is None or c.expression(model, node)):
                return c.delegate

        raise GraphNodeDelegateRoutingError("Can't route.")

    def get_node_layout(self, model, node_desc: GraphNodeDescription):
        """Called to determine the node layout"""
        return self.__route(model, node_desc.node).get_node_layout(model, node_desc)

    def node_background(self, model, node_desc: GraphNodeDescription):
        """Called to create widgets of the node background"""
        return self.__route(model, node_desc.node).node_background(model, node_desc)

    def node_header_input(self, model, node_desc: GraphNodeDescription):
        """Called to create the left part of the header that will be used as input when the node is collapsed"""
        return self.__route(model, node_desc.node).node_header_input(model, node_desc)

    def node_header_output(self, model, node_desc: GraphNodeDescription):
        """Called to create the right part of the header that will be used as output when the node is collapsed"""
        return self.__route(model, node_desc.node).node_header_output(model, node_desc)

    def node_header(self, model, node_desc: GraphNodeDescription):
        """Called to create widgets of the top of the node"""
        return self.__route(model, node_desc.node).node_header(model, node_desc)

    def node_footer(self, model, node_desc: GraphNodeDescription):
        """Called to create widgets of the bottom of the node"""
        return self.__route(model, node_desc.node).node_footer(model, node_desc)

    def port_input(self, model, node_desc: GraphNodeDescription, port_desc: GraphPortDescription):
        """Called to create the left part of the port that will be used as input"""
        return self.__route(model, node_desc.node).port_input(model, node_desc, port_desc)

    def port_output(self, model, node_desc: GraphNodeDescription, port_desc: GraphPortDescription):
        """Called to create the right part of the port that will be used as output"""
        return self.__route(model, node_desc.node).port_output(model, node_desc, port_desc)

    def port(self, model, node_desc: GraphNodeDescription, port_desc: GraphPortDescription):
        """Called to create the middle part of the port"""
        return self.__route(model, node_desc.node).port(model, node_desc, port_desc)

    def connection(self, model, source: GraphConnectionDescription, target: GraphConnectionDescription):
        """Called to create the connection between ports"""
        return self.__route(model, source.node).connection(model, source, target)

    def destroy(self):
        while self.__routing_table:
            routing_condition = self.__routing_table.pop()
            routing_condition.delegate.destroy()
