'''
RecycleView Layouts
===================

.. versionadded:: 1.10.0

The Layouts handle the presentation of views for the
:class:`~kivy.uix.recycleview.RecycleView`.

.. warning::
    This module is highly experimental, its API may change in the future and
    the documentation is not complete at this time.
'''
from kivy.factory import Factory
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.behaviors import CompoundSelectionBehavior
from kivy.uix.recycleview.views import RecycleDataViewBehavior, \
    _view_base_cache


class LayoutChangeException(Exception):
    pass


class LayoutSelectionBehavior(CompoundSelectionBehavior):
    '''The :class:`LayoutSelectionBehavior` can be combined with
    :class:`RecycleLayoutManagerBehavior` to allow its derived classes
    selection behaviors similarly to how
    :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
    can be used to add selection behaviors to normal layout.

    :class:`RecycleLayoutManagerBehavior` manages its children
    differently than normal layouts or widgets so this class adapts
    :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
    based selection to work with :class:`RecycleLayoutManagerBehavior` as well.

    Similarly to
    :class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`,
    one can select using the keyboard or touch, which calls :meth:`select_node`
    or :meth:`deselect_node`, or one can call these methods directly. When a
    item is selected or deselected :meth:`apply_selection` is called. See
    :meth:`apply_selection`.


    '''

    key_selection = StringProperty(None, allownone=True)
    '''The key used to check whether a view of a data item can be selected
    with touch or the keyboard.

    :attr:`key_selection` is the key in data, which if present and ``True``
    will enable selection for this item from the keyboard or with a touch.
    When None, the default, not item will be selectable.

    :attr:`key_selection` is a :class:`StringProperty` and defaults to None.

    .. note::
        All data items can be selected directly using :meth:`select_node` or
        :meth:`deselect_node`, even if :attr:`key_selection` is False.
    '''

    _selectable_nodes = []
    _nodes_map = {}

    def __init__(self, **kwargs):
        self.nodes_order_reversed = False
        super(LayoutSelectionBehavior, self).__init__(**kwargs)

    def compute_sizes_from_data(self, data, flags):
        # overwrite this method so that when data changes we update
        # selectable nodes.
        key = self.key_selection
        if key is None:
            nodes = self._selectable_nodes = []
        else:
            nodes = self._selectable_nodes = [
                i for i, d in enumerate(data) if d.get(key)]

        self._nodes_map = {v: k for k, v in enumerate(nodes)}
        return super(LayoutSelectionBehavior, self).compute_sizes_from_data(
            data, flags)

    def get_selectable_nodes(self):
        # the indices of the data is used as the nodes
        return self._selectable_nodes

    def get_index_of_node(self, node, selectable_nodes):
        # the indices of the data is used as the nodes, so node
        return self._nodes_map[node]

    def goto_node(self, key, last_node, last_node_idx):
        node, idx = super(LayoutSelectionBehavior, self).goto_node(
            key, last_node, last_node_idx)
        if node is not last_node:
            self.goto_view(node)
        return node, idx

    def select_node(self, node):
        if super(LayoutSelectionBehavior, self).select_node(node):
            view = self.recycleview.view_adapter.get_visible_view(node)
            if view is not None:
                self.apply_selection(node, view, True)

    def deselect_node(self, node):
        if super(LayoutSelectionBehavior, self).deselect_node(node):
            view = self.recycleview.view_adapter.get_visible_view(node)
            if view is not None:
                self.apply_selection(node, view, False)

    def apply_selection(self, index, view, is_selected):
        '''Applies the selection to the view. This is called internally when
        a view is displayed and it needs to be shown as selected or as not
        selected.

        It is called when :meth:`select_node` or :meth:`deselect_node` is
        called or when a view needs to be refreshed. Its function is purely to
        update the view to reflect the selection state. So the function may be
        called multiple times even if the selection state may not have changed.

        If the view is a instance of
        :class:`~kivy.uix.recycleview.views.RecycleDataViewBehavior`, its
        :meth:`~kivy.uix.recycleview.views.RecycleDataViewBehavior.\
apply_selection` method will be called every time the view needs to refresh
        the selection state. Otherwise, the this method is responsible
        for applying the selection.

        :Parameters:

            `index`: int
                The index of the data item that is associated with the view.
            `view`: widget
                The widget that is the view of this data item.
            `is_selected`: bool
                Whether the item is selected.
        '''
        viewclass = view.__class__
        if viewclass not in _view_base_cache:
            _view_base_cache[viewclass] = isinstance(view,
                                                     RecycleDataViewBehavior)

        if _view_base_cache[viewclass]:
            view.apply_selection(self.recycleview, index, is_selected)

    def refresh_view_layout(self, index, layout, view, viewport):
        super(LayoutSelectionBehavior, self).refresh_view_layout(
            index, layout, view, viewport)
        self.apply_selection(index, view, index in self.selected_nodes)


class RecycleLayoutManagerBehavior(object):
    """A RecycleLayoutManagerBehavior is responsible for positioning views into
    the :attr:`RecycleView.data` within a :class:`RecycleView`. It adds new
    views into the data when it becomes visible to the user, and removes them
    when they leave the visible area.
    """

    viewclass = ObjectProperty(None)
    '''See :attr:`RecyclerView.viewclass`.
    '''
    key_viewclass = StringProperty(None)
    '''See :attr:`RecyclerView.key_viewclass`.
    '''

    recycleview = ObjectProperty(None, allownone=True)

    asked_sizes = None

    def attach_recycleview(self, rv):
        self.recycleview = rv
        if rv:
            fbind = self.fbind
            # can be made more selective update than refresh_from_data which
            # causes a full update. But this likely affects most of the data.
            fbind('viewclass', rv.refresh_from_data)
            fbind('key_viewclass', rv.refresh_from_data)
            fbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
            fbind('key_viewclass', rv._dispatch_prop_on_source,
                  'key_viewclass')

    def detach_recycleview(self):
        self.clear_layout()
        rv = self.recycleview
        if rv:
            funbind = self.funbind
            funbind('viewclass', rv.refresh_from_data)
            funbind('key_viewclass', rv.refresh_from_data)
            funbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
            funbind('key_viewclass', rv._dispatch_prop_on_source,
                  'key_viewclass')
        self.recycleview = None

    def compute_sizes_from_data(self, data, flags):
        pass

    def compute_layout(self, data, flags):
        pass

    def compute_visible_views(self, data, viewport):
        '''`viewport` is in coordinates of the layout manager.
        '''
        pass

    def set_visible_views(self, indices, data, viewport):
        '''`viewport` is in coordinates of the layout manager.
        '''
        pass

    def refresh_view_layout(self, index, layout, view, viewport):
        '''`See :meth:`~kivy.uix.recycleview.views.RecycleDataAdapter.\
refresh_view_layout`.
        '''
        self.recycleview.view_adapter.refresh_view_layout(
            index, layout, view, viewport)

    def get_view_index_at(self, pos):
        """Return the view `index` on which position, `pos`, falls.

        `pos` is in coordinates of the layout manager.
        """
        pass

    def remove_views(self):
        rv = self.recycleview
        if rv:
            adapter = rv.view_adapter
            if adapter:
                adapter.make_views_dirty()

    def remove_view(self, view, index):
        rv = self.recycleview
        if rv:
            adapter = rv.view_adapter
            if adapter:
                adapter.make_view_dirty(view, index)

    def clear_layout(self):
        rv = self.recycleview
        if rv:
            adapter = rv.view_adapter
            if adapter:
                adapter.invalidate()

    def goto_view(self, index):
        """Moves the views so that the view corresponding to `index` is
        visible.  A negative index is allowed.
        """
        # implement this method in the subclass, RecycleBoxLayout
        # or RecycleGridLayout
        pass

    def on_viewclass(self, instance, value):
        # resolve the real class if it was a string.
        if isinstance(value, str):
            self.viewclass = getattr(Factory, value)
