# 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__ = ["GraphModel"]

from enum import Enum
from enum import IntFlag
from enum import auto
from typing import Any
from typing import Optional
from typing import Union


class GraphModel:
    """
    The base class for the Graph model.

    The model is the central component of the graph widget. It is the
    application's dynamic data structure, independent of the user interface,
    and it directly manages the data. It follows closely model–view pattern.
    It defines the standard interface to be able to interoperate with the
    components of the model-view architecture. It is not supposed to be
    instantiated directly. Instead, the user should subclass it to create a
    new model.

    The model manages two kinds of data elements. Node and port are the
    atomic data elements of the model. Both node and port can have any number
    of sub-ports and any number of input and output connections.

    There is no specific Python type for the elements of the model. Since
    Python has dynamic types, the model can return any object as a node or a
    port. When the widget needs to get a property of the node, it provides
    the given node back to the model.

    Example:

    .. code:: python

       class UsdShadeModel(GraphModel):
           @property
           def nodes(self, prim=None):
               # Return Usd.Prim in a list
               return [stage.GetPrimAtPath(selection)]

           @property
           def name(self, item=None):
               # item here is Usd.Prim because UsdShadeModel.nodes returns
               # Usd.Prim
               return item.GetPath().name

       # Accessing nodes and properties example
       model = UsdShadeModel()

       # UsdShadeModel decides the type of nodes. It's a list with Usd.Prim
       nodes = model.nodes

       for node in nodes:
           # The node is accessed through evaluation of self[key]. It will
           # return the proxy object that redirects its properties back to
           # model. So the following line will call UsdShadeModel.name(node).
           name = model[node].name
           print(f"The model has node {name}")
    """

    class _ItemProxy:
        """
        The proxy that allows accessing the nodes and ports of the model
        through evaluation of self[key]. This proxy object redirects its
        properties to the model that has properties like this:

        class Model:
           @property
           def name(self, item):
               pass

           @name.setter
           def name(self, value, item):
               pass

        """

        def __init__(self, model, item):
            """Save model and item to be able to redirect the properties"""
            # Since we already have __setattr__ reimplemented, the following
            # code is the way to bypass it
            super().__setattr__("_model", model)
            super().__setattr__("_item", item)

        def __getattr__(self, attr):
            """
            Called when the default attribute access fails with an
            AttributeError.
            """
            model_property = getattr(type(self._model), attr)
            return model_property.fget(self._model, self._item)

        def __setattr__(self, attr, value):
            """
            Called when an attribute assignment is attempted. This is called
            instead of the normal mechanism (i.e. store the value in the
            instance dictionary).
            """
            model_property = getattr(type(self._model), attr)
            model_property.fset(self._model, value, self._item)
            # TODO: Maybe it's better to automatically call model._item_changed here?
            # No, it's not better because in some cases node is changing and
            # the widget doesn't change. For example when the user changes the
            # position.

    class _Event(set):
        """
        A list of callable objects. Calling an instance of this will cause a
        call to each item in the list in ascending order by index.
        """

        def __call__(self, *args, **kwargs):
            """Called when the instance is “called” as a function"""
            # Call all the saved functions
            for f in list(self):
                f(*args, **kwargs)

        def __repr__(self):
            """
            Called by the repr() built-in function to compute the “official”
            string representation of an object.
            """
            return f"Event({set.__repr__(self)})"

    class ExpansionState(Enum):
        OPEN = 0
        MINIMIZED = 1
        CLOSED = 2

    class PreviewState(IntFlag):
        NONE = 0
        OPEN = auto()
        CACHED = auto()
        LARGE = auto()

    class _EventSubscription:
        """
        Event subscription.

        _Event has callback while this object exists.
        """

        def __init__(self, event, fn):
            """
            Save the function, the event, and add the function to the event.
            """
            self._fn = fn
            self._event = event
            event.add(self._fn)

        def __del__(self):
            """Called by GC."""
            self._event.remove(self._fn)

    DISPLAY_NAME = None

    def __init__(self):
        super().__init__()
        # TODO: begin_edit/end_edit
        self.__on_item_changed = self._Event()
        self.__on_selection_changed = self._Event()
        self.__on_node_changed = self._Event()

    def __getitem__(self, item):
        """Called to implement evaluation of self[key]"""
        # Return a proxy that redirects its properties back to the model.
        return self._ItemProxy(self, item)

    def _item_changed(self, item=None):
        """Call the event object that has the list of functions"""
        self.__on_item_changed(item)

    def _rebuild_node(self, item=None, full=False):
        """Call the event object that has the list of functions"""
        self.__on_node_changed(item, full=full)

    def subscribe_item_changed(self, fn):
        """
        Return the object that will automatically unsubscribe when destroyed.
        """
        return self._EventSubscription(self.__on_item_changed, fn)

    def _selection_changed(self):
        """Call the event object that has the list of functions"""
        self.__on_selection_changed()

    def subscribe_selection_changed(self, fn):
        """
        Return the object that will automatically unsubscribe when destroyed.
        """
        return self._EventSubscription(self.__on_selection_changed, fn)

    def subscribe_node_changed(self, fn):
        """
        Return the object that will automatically unsubscribe when destroyed.
        """
        return self._EventSubscription(self.__on_node_changed, fn)

    @staticmethod
    def has_nodes(obj):
        """Returns true if the model can currently build the graph network using the provided object"""
        pass

    # The list of properties of node and port.

    @property
    def name(self, item) -> str:
        """The name of the item how it should be displayed in the view"""
        pass

    @name.setter
    def name(self, value: str, item=None):
        """Request to rename item"""
        pass

    @property
    def type(self, item):
        pass

    @property
    def inputs(self, item):
        pass

    @inputs.setter
    def inputs(self, value, item=None):
        pass

    @property
    def outputs(self, item):
        pass

    @outputs.setter
    def outputs(self, value, item=None):
        pass

    @property
    def nodes(self, item=None):
        pass

    @property
    def ports(self, item=None):
        pass

    @ports.setter
    def ports(self, value, item=None):
        pass

    @property
    def expansion_state(self, item=None) -> ExpansionState:
        return self.ExpansionState.OPEN

    @expansion_state.setter
    def expansion_state(self, value: ExpansionState, item=None):
        pass

    def can_connect(self, source, target):
        """Return if it's possible to connect source to target"""
        return True

    @property
    def position(self, item=None):
        """Returns the position of the node"""
        pass

    @position.setter
    def position(self, value, item=None):
        """The node position setter"""
        pass

    def position_begin_edit(self, item):
        """Called when the user started dragging the node"""
        pass

    def position_end_edit(self, item):
        """Called when the user finished dragging the node and released the mouse"""
        pass

    @property
    def size(self, item):
        """The node size. Is used for nodes like Backdrop."""
        pass

    @size.setter
    def size(self, value, item=None):
        """The node position setter"""
        pass

    def size_begin_edit(self, item):
        """Called when the user started resizing the node"""
        pass

    def size_end_edit(self, item):
        """Called when the user finished resizing the node and released the mouse"""
        pass

    @property
    def description(self, item):
        """The text label that is displayed on the backdrop in the node graph."""
        pass

    @description.setter
    def description(self, value, item=None):
        """The node description setter"""
        pass

    @property
    def display_color(self, item):
        """The node color."""
        pass

    @display_color.setter
    def display_color(self, value, item=None):
        """The node color setter"""
        pass

    @property
    def stacking_order(self, item):
        """
        This value is a hint when an application cares about the visibility
        of a node and whether each node overlaps another.
        """
        return 0

    @property
    def selection(self):
        pass

    @selection.setter
    def selection(self, value: list):
        pass

    def special_select_widget(self, node, node_widget):
        # Returning None means node_widget will get used
        return None

    def destroy(self):
        pass

    @property
    def icon(self, item) -> Optional[Union[str, Any]]:
        """Return icon of the image"""
        pass

    @icon.setter
    def icon(self, value: Optional[Union[str, Any]], item=None):
        """Set the icon of the image"""
        pass

    @property
    def preview(self, item) -> Optional[Union[str, Any]]:
        """Return the preview of the image"""
        pass

    @preview.setter
    def preview(self, value: Optional[Union[str, Any]], item=None):
        """Set the preview of the image"""
        pass

    @property
    def preview_state(self, item) -> PreviewState:
        """Return the state of the preview of the node"""
        return GraphModel.PreviewState.NONE

    @preview_state.setter
    def preview_state(self, value: PreviewState, item=None):
        """Set the state of the preview of the node"""
        pass
