#!/usr/bin/python
from ..base.commands import PropertyChangedCmd
from ..base.model import NodeModel
from ..constants import (NODE_PROP,
                                   NODE_LAYOUT_DIRECTION,
                                   NODE_LAYOUT_VERTICAL,
                                   NODE_LAYOUT_HORIZONTAL)


class _ClassProperty(object):

    def __init__(self, f):
        self.f = f

    def __get__(self, instance, owner):
        return self.f(owner)


class NodeObject(object):
    """
    The ``NodeGraphQt.NodeObject`` class is the main base class that all
    nodes inherit from.

    **Inherited by:**
        :class:`NodeGraphQt.BaseNode`,
        :class:`NodeGraphQt.BackdropNode`

    Args:
        qgraphics_views (dict): Dictionary with the node layout type as the key
            and a custom graphics item subclassed from the ``AbstractNodeItem``
            as the value.

            .. code-block:: python

                # snippet taken from the NodeGraphQt.BaseNode class.

                class BaseNode(NodeObject):

                    def __init__(self, qgraphics_views=None):
                        qgraphics_views = qgraphics_views or {
                            NodeGraphQt.constants.NODE_LAYOUT_HORIZONTAL: NodeItem,
                            NodeGraphQt.constants.NODE_LAYOUT_VERTICAL: NodeItemVertical
                        }
                        super(BaseNode, self).__init__(qgraphics_views)

    """

    # Unique node identifier domain. `eg.` ``"com.chantacticvfx"``
    __identifier__ = 'nodeGraphQt.nodes'

    # Base node name.
    NODE_NAME = None

    def __init__(self, qgraphics_views=None):
        self._graph = None
        self._model = NodeModel()
        self._model.type_ = self.type_
        self._model.name = self.NODE_NAME

        _NodeItem = None
        if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL:
            _NodeItem = qgraphics_views.get(NODE_LAYOUT_VERTICAL)
        elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL:
            _NodeItem = qgraphics_views.get(NODE_LAYOUT_HORIZONTAL)

        if _NodeItem is None:
            raise ValueError(
                'qgraphics item for the {} node layout can\'t be None!'.format({
                    NODE_LAYOUT_VERTICAL: 'vertical',
                    NODE_LAYOUT_HORIZONTAL: 'horizontal'
                }[NODE_LAYOUT_DIRECTION]))

        self._view = _NodeItem()
        self._view.type_ = self.type_
        self._view.name = self.model.name
        self._view.id = self._model.id

    def __repr__(self):
        return '<{}("{}") object at {}>'.format(
            self.__class__.__name__, self.NODE_NAME, hex(id(self)))

    @_ClassProperty
    def type_(cls):
        """
        Node type identifier followed by the class name.
        `eg.` ``"com.chantacticvfx.NodeObject"``

        Returns:
            str: node type.
        """
        return cls.__identifier__ + '.' + cls.__name__

    @property
    def id(self):
        """
        The node unique id.

        Returns:
            str: unique id string.
        """
        return self.model.id

    @property
    def graph(self):
        """
        The parent node graph.

        Returns:
            NodeGraphQt.NodeGraph: node graph.
        """
        return self._graph

    @property
    def view(self):
        """
        Returns the :class:`QtWidgets.QGraphicsItem` used in the scene.

        Returns:
            NodeGraphQt.qgraphics.node_abstract.AbstractNodeItem: node item.
        """
        return self._view

    def set_view(self, item):
        """
        Set a new ``QGraphicsItem`` item to be used as the view.
        (the provided qgraphics item must be subclassed from the
        ``AbstractNodeItem`` object.)

        Args:
            item (NodeGraphQt.qgraphics.node_abstract.AbstractNodeItem): node item.
        """
        if self._view:
            old_view = self._view
            scene = self._view.scene()
            scene.removeItem(old_view)
            self._view = item
            scene.addItem(self._view)
        else:
            self._view = item
        self.NODE_NAME = self._view.name

        # update the view.
        self.update()

    @property
    def model(self):
        """
        Return the node model.

        Returns:
            NodeGraphQt.base.model.NodeModel: node model object.
        """
        return self._model

    def set_model(self, model):
        """
        Set a new model to the node model.
        (Setting a new node model will also update the views qgraphics item.)

        Args:
            model (NodeGraphQt.base.model.NodeModel): node model object.
        """
        self._model = model
        self._model.type_ = self.type_
        self._model.id = self.view.id

        # update the view.
        self.update()

    def update_model(self):
        """
        Update the node model from view.
        """
        for name, val in self.view.properties.items():
            if name in self.model.properties.keys():
                setattr(self.model, name, val)
            if name in self.model.custom_properties.keys():
                self.model.custom_properties[name] = val

    def update(self):
        """
        Update the node view from model.
        """
        settings = self.model.to_dict[self.model.id]
        settings['id'] = self.model.id
        if settings.get('custom'):
            settings['widgets'] = settings.pop('custom')

        self.view.from_dict(settings)

    def serialize(self):
        """
        Serialize node model to a dictionary.

        example:

        .. highlight:: python
        .. code-block:: python

            {'0x106cf75a8': {
                'name': 'foo node',
                'color': (48, 58, 69, 255),
                'border_color': (85, 100, 100, 255),
                'text_color': (255, 255, 255, 180),
                'type': 'com.chantasticvfx.MyNode',
                'selected': False,
                'disabled': False,
                'visible': True,
                'inputs': {
                    <port_name>: {<node_id>: [<port_name>, <port_name>]}
                },
                'outputs': {
                    <port_name>: {<node_id>: [<port_name>, <port_name>]}
                },
                'input_ports': [<port_name>, <port_name>],
                'output_ports': [<port_name>, <port_name>],
                'width': 0.0,
                'height: 0.0,
                'pos': (0.0, 0.0),
                'custom': {},
                }
            }

        Returns:
            dict: serialized node
        """
        return self.model.to_dict

    def name(self):
        """
        Name of the node.

        Returns:
            str: name of the node.
        """
        return self.model.name

    def set_name(self, name=''):
        """
        Set the name of the node.

        Args:
            name (str): name for the node.
        """
        self.set_property('name', name)

    def color(self):
        """
        Returns the node color in (red, green, blue) value.

        Returns:
            tuple: ``(r, g, b)`` from ``0-255`` range.
        """
        r, g, b, a = self.model.color
        return r, g, b

    def set_color(self, r=0, g=0, b=0):
        """
        Sets the color of the node in (red, green, blue) value.

        Args:
            r (int): red value ``0-255`` range.
            g (int): green value ``0-255`` range.
            b (int): blue value ``0-255`` range.
        """
        self.set_property('color', (r, g, b, 255))

    def disabled(self):
        """
        Returns whether the node is enabled or disabled.

        Returns:
            bool: True if the node is disabled.
        """
        return self.model.disabled

    def set_disabled(self, mode=False):
        """
        Set the node state to either disabled or enabled.

        Args:
            mode(bool): True to disable node.
        """
        self.set_property('disabled', mode)

    def selected(self):
        """
        Returns the selected state of the node.

        Returns:
            bool: True if the node is selected.
        """
        self.model.selected = self.view.isSelected()
        return self.model.selected

    def set_selected(self, selected=True):
        """
        Set the node to be selected or not selected.

        Args:
            selected (bool): True to select the node.
        """
        self.set_property('selected', selected)

    def create_property(self, name, value, items=None, range=None,
                        widget_type=NODE_PROP, tab=None):
        """
        Creates a custom property to the node.

        See Also:
            Custom node properties bin widget
            :class:`NodeGraphQt.PropertiesBinWidget`

        Hint:
            Here are some constants variables used to define the node
            widget type in the ``PropertiesBinWidget``.

            - :attr:`NodeGraphQt.constants.NODE_PROP`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QLABEL`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QLINEEDIT`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QTEXTEDIT`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QCOMBO`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QCHECKBOX`
            - :attr:`NodeGraphQt.constants.NODE_PROP_QSPINBOX`
            - :attr:`NodeGraphQt.constants.NODE_PROP_COLORPICKER`
            - :attr:`NodeGraphQt.constants.NODE_PROP_FILE`
            - :attr:`NodeGraphQt.constants.NODE_PROP_VECTOR2`
            - :attr:`NodeGraphQt.constants.NODE_PROP_VECTOR3`
            - :attr:`NodeGraphQt.constants.NODE_PROP_VECTOR4`
            - :attr:`NodeGraphQt.constants.NODE_PROP_FLOAT`
            - :attr:`NodeGraphQt.constants.NODE_PROP_INT`
            - :attr:`NodeGraphQt.constants.NODE_PROP_BUTTON`

        Args:
            name (str): name of the property.
            value (object): data.
            items (list[str]): items used by widget type ``NODE_PROP_QCOMBO``
            range (tuple)): ``(min, max)`` values used by ``NODE_PROP_SLIDER``
            widget_type (int): widget flag to display in the
                :class:`NodeGraphQt.PropertiesBinWidget`
            tab (str): name of the widget tab to display in the properties bin.
        """
        self.model.add_property(name, value, items, range, widget_type, tab)

    def properties(self):
        """
        Returns all the node properties.

        Returns:
            dict: a dictionary of node properties.
        """
        props = self.model.to_dict[self.id].copy()
        props['id'] = self.id
        return props

    def get_property(self, name):
        """
        Return the node custom property.

        Args:
            name (str): name of the property.

        Returns:
            object: property data.
        """
        if self.graph and name == 'selected':
            self.model.set_property(name, self.view.selected)

        return self.model.get_property(name)

    def set_property(self, name, value, push_undo=True):
        """
        Set the value on the node custom property.

        Args:
            name (str): name of the property.
            value (object): property data (python built in types).
            push_undo (bool): register the command to the undo stack. (default: True)
        """

        # prevent signals from causing a infinite loop.
        if self.get_property(name) == value:
            return

        if self.graph and name == 'name':
            value = self.graph.get_unique_name(value)
            self.NODE_NAME = value

        if self.graph:
            if push_undo:
                undo_stack = self.graph.undo_stack()
                undo_stack.push(PropertyChangedCmd(self, name, value))
            else:
                PropertyChangedCmd(self, name, value).redo()
        else:
            if hasattr(self.view, name):
                setattr(self.view, name, value)
            self.model.set_property(name, value)

    def has_property(self, name):
        """
        Check if node custom property exists.

        Args:
            name (str): name of the node.

        Returns:
            bool: true if property name exists in the Node.
        """
        return name in self.model.custom_properties.keys()

    def set_x_pos(self, x):
        """
        Set the node horizontal X position in the node graph.

        Args:
            x (float or int): node X position.
        """
        y = self.pos()[1]
        self.set_pos(float(x), y)

    def set_y_pos(self, y):
        """
        Set the node horizontal Y position in the node graph.

        Args:
            y (float or int): node Y position.
        """

        x = self.pos()[0]
        self.set_pos(x, float(y))

    def set_pos(self, x, y):
        """
        Set the node X and Y position in the node graph.

        Args:
            x (float or int): node X position.
            y (float or int): node Y position.
        """
        self.set_property('pos', [float(x), float(y)])

    def x_pos(self):
        """
        Get the node X position in the node graph.

        Returns:
            float: x position.
        """
        return self.model.pos[0]

    def y_pos(self):
        """
        Get the node Y position in the node graph.

        Returns:
            float: y position.
        """
        return self.model.pos[1]

    def pos(self):
        """
        Get the node XY position in the node graph.

        Returns:
            list[float, float]: x, y position.
        """
        if self.view.xy_pos and self.view.xy_pos != self.model.pos:
            self.model.pos = self.view.xy_pos

        return self.model.pos
