'''
RecycleView Data Model
======================

.. versionadded:: 1.10.0

The data model part of the RecycleView model-view-controller pattern.

It defines the models (classes) that store the data associated with a
:class:`~kivy.uix.recycleview.RecycleViewBehavior`. Each model (class)
determines how the data is stored and emits requests to the controller
(:class:`~kivy.uix.recycleview.RecycleViewBehavior`) when the data is
modified.
'''

from kivy.properties import ListProperty, ObservableDict, ObjectProperty
from kivy.event import EventDispatcher
from functools import partial

__all__ = ('RecycleDataModelBehavior', 'RecycleDataModel')


def recondition_slice_assign(val, last_len, new_len):
    if not isinstance(val, slice):
        return slice(val, val + 1)

    diff = new_len - last_len

    start, stop, step = val.start, val.stop, val.step
    if stop <= start:
        return slice(0, 0)

    if step is not None and step != 1:
        assert last_len == new_len
        if stop < 0:
            stop = max(0, last_len + stop)
        stop = min(last_len, stop)

        if start < 0:
            start = max(0, last_len + start)
        start = min(last_len, start)

        return slice(start, stop, step)

    if start < 0:
        start = last_len + start
    if stop < 0:
        stop = last_len + stop

    # whatever, too complicated don't try to compute it
    if (start < 0 or stop < 0 or start > last_len or stop > last_len or
            new_len != last_len):
        return None

    return slice(start, stop)


class RecycleDataModelBehavior(object):
    """:class:`RecycleDataModelBehavior` is the base class for the models
    that describes and provides the data for the
    :class:`~kivy.uix.recycleview.RecycleViewBehavior`.

    :Events:
        `on_data_changed`:
            Fired when the data changes. The event may dispatch
            keyword arguments specific to each implementation of the data
            model.
            When dispatched, the event and keyword arguments are forwarded to
            :meth:`~kivy.uix.recycleview.RecycleViewBehavior.\
refresh_from_data`.
    """

    __events__ = ("on_data_changed", )

    recycleview = ObjectProperty(None, allownone=True)
    '''The
    :class:`~kivy.uix.recycleview.RecycleViewBehavior` instance
    associated with this data model.
    '''

    def attach_recycleview(self, rv):
        '''Associates a
        :class:`~kivy.uix.recycleview.RecycleViewBehavior` with
        this data model.
        '''
        self.recycleview = rv
        if rv:
            self.fbind('on_data_changed', rv.refresh_from_data)

    def detach_recycleview(self):
        '''Removes the
        :class:`~kivy.uix.recycleview.RecycleViewBehavior`
        associated with this data model.
        '''
        rv = self.recycleview
        if rv:
            self.funbind('on_data_changed', rv.refresh_from_data)
        self.recycleview = None

    def on_data_changed(self, *largs, **kwargs):
        pass


class RecycleDataModel(RecycleDataModelBehavior, EventDispatcher):
    '''An implementation of :class:`RecycleDataModelBehavior` that keeps the
    data in a indexable list. See :attr:`data`.

    When data changes this class currently dispatches `on_data_changed`  with
    one of the following additional keyword arguments.

    `none`: no keyword argument
        With no additional argument it means a generic data change.
    `removed`: a slice or integer
        The value is a slice or integer indicating the indices removed.
    `appended`: a slice
        The slice in :attr:`data` indicating the first and last new items
        (i.e. the slice pointing to the new items added at the end).
    `inserted`: a integer
        The index in :attr:`data` where a new data item was inserted.
    `modified`: a slice
        The slice with the indices where the data has been modified.
        This currently does not allow changing of size etc.
    '''

    data = ListProperty([])
    '''Stores the model's data using a list.

    The data for a item at index `i` can also be accessed with
    :class:`RecycleDataModel` `[i]`.
    '''

    _last_len = 0

    def __init__(self, **kwargs):
        self.fbind('data', self._on_data_callback)
        super(RecycleDataModel, self).__init__(**kwargs)

    def __getitem__(self, index):
        return self.data[index]

    @property
    def observable_dict(self):
        '''A dictionary instance, which when modified will trigger a `data` and
        consequently an `on_data_changed` dispatch.
        '''
        return partial(ObservableDict, self.__class__.data, self)

    def attach_recycleview(self, rv):
        super(RecycleDataModel, self).attach_recycleview(rv)
        if rv:
            self.fbind('data', rv._dispatch_prop_on_source, 'data')

    def detach_recycleview(self):
        rv = self.recycleview
        if rv:
            self.funbind('data', rv._dispatch_prop_on_source, 'data')
        super(RecycleDataModel, self).detach_recycleview()

    def _on_data_callback(self, instance, value):
        last_len = self._last_len
        new_len = self._last_len = len(self.data)
        op, val = value.last_op

        if op == '__setitem__':
            val = recondition_slice_assign(val, last_len, new_len)
            if val is not None:
                self.dispatch('on_data_changed', modified=val)
            else:
                self.dispatch('on_data_changed')
        elif op == '__delitem__':
            self.dispatch('on_data_changed', removed=val)
        elif op == '__setslice__':
            val = recondition_slice_assign(slice(*val), last_len, new_len)
            if val is not None:
                self.dispatch('on_data_changed', modified=val)
            else:
                self.dispatch('on_data_changed')
        elif op == '__delslice__':
            self.dispatch('on_data_changed', removed=slice(*val))
        elif op == '__iadd__' or op == '__imul__':
            self.dispatch('on_data_changed', appended=slice(last_len, new_len))
        elif op == 'append':
            self.dispatch('on_data_changed', appended=slice(last_len, new_len))
        elif op == 'insert':
            self.dispatch('on_data_changed', inserted=val)
        elif op == 'pop':
            if val:
                self.dispatch('on_data_changed', removed=val[0])
            else:
                self.dispatch('on_data_changed', removed=last_len - 1)
        elif op == 'extend':
            self.dispatch('on_data_changed', appended=slice(last_len, new_len))
        else:
            self.dispatch('on_data_changed')
