"""
RecycleLayout
=============

.. versionadded:: 1.10.0

.. warning::
    This module is highly experimental, its API may change in the future and
    the documentation is not complete at this time.
"""

from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior
from kivy.uix.layout import Layout
from kivy.properties import (
    ObjectProperty, StringProperty, ReferenceListProperty, NumericProperty
)
from kivy.factory import Factory

__all__ = ('RecycleLayout', )


class RecycleLayout(RecycleLayoutManagerBehavior, Layout):
    """
    RecycleLayout provides the default layout for RecycleViews.
    """

    default_width = NumericProperty(100, allownone=True)
    '''Default width for items

    :attr:`default_width` is a NumericProperty and default to 100
    '''
    default_height = NumericProperty(100, allownone=True)
    '''Default height for items

    :attr:`default_height` is a :class:`~kivy.properties.NumericProperty` and
    default to 100.
    '''
    default_size = ReferenceListProperty(default_width, default_height)
    '''size (width, height). Each value can be None.

    :attr:`default_size` is an :class:`~kivy.properties.ReferenceListProperty`
    to [:attr:`default_width`, :attr:`default_height`].
    '''
    default_size_hint_x = NumericProperty(None, allownone=True)
    '''Default size_hint_x for items

    :attr:`default_size_hint_x` is a :class:`~kivy.properties.NumericProperty`
    and default to None.
    '''
    default_size_hint_y = NumericProperty(None, allownone=True)
    '''Default size_hint_y for items

    :attr:`default_size_hint_y` is a :class:`~kivy.properties.NumericProperty`
    and default to None.
    '''
    default_size_hint = ReferenceListProperty(
        default_size_hint_x, default_size_hint_y
    )
    '''size (width, height). Each value can be None.

    :attr:`default_size_hint` is an
    :class:`~kivy.properties.ReferenceListProperty` to
    [:attr:`default_size_hint_x`, :attr:`default_size_hint_y`].
    '''

    key_size = StringProperty(None, allownone=True)
    '''If set, which key in the dict should be used to set the size property of
    the item.

    :attr:`key_size` is a :class:`~kivy.properties.StringProperty` and defaults
    to None.
    '''
    key_size_hint = StringProperty(None, allownone=True)
    '''If set, which key in the dict should be used to set the size_hint
    property of the item.

    :attr:`key_size_hint` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    key_size_hint_min = StringProperty(None, allownone=True)
    '''If set, which key in the dict should be used to set the size_hint_min
    property of the item.

    :attr:`key_size_hint_min` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''
    default_size_hint_x_min = NumericProperty(None, allownone=True)
    '''Default value for size_hint_x_min of items

    :attr:`default_pos_hint_x_min` is a
    :class:`~kivy.properties.NumericProperty` and defaults to None.
    '''
    default_size_hint_y_min = NumericProperty(None, allownone=True)
    '''Default value for size_hint_y_min of items

    :attr:`default_pos_hint_y_min` is a
    :class:`~kivy.properties.NumericProperty` and defaults to None.
    '''
    default_size_hint_min = ReferenceListProperty(
        default_size_hint_x_min,
        default_size_hint_y_min
    )
    '''Default value for size_hint_min of items

    :attr:`default_size_min` is a
    :class:`~kivy.properties.ReferenceListProperty` to
    [:attr:`default_size_hint_x_min`, :attr:`default_size_hint_y_min`].
    '''

    key_size_hint_max = StringProperty(None, allownone=True)
    '''If set, which key in the dict should be used to set the size_hint_max
    property of the item.

    :attr:`key_size_hint_max` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''
    default_size_hint_x_max = NumericProperty(None, allownone=True)
    '''Default value for size_hint_x_max of items

    :attr:`default_pos_hint_x_max` is a
    :class:`~kivy.properties.NumericProperty` and defaults to None.
    '''
    default_size_hint_y_max = NumericProperty(None, allownone=True)
    '''Default value for size_hint_y_max of items

    :attr:`default_pos_hint_y_max` is a
    :class:`~kivy.properties.NumericProperty` and defaults to None.
    '''
    default_size_hint_max = ReferenceListProperty(
        default_size_hint_x_max,
        default_size_hint_y_max
    )
    '''Default value for size_hint_max of items

    :attr:`default_size_max` is a
    :class:`~kivy.properties.ReferenceListProperty` to
    [:attr:`default_size_hint_x_max`, :attr:`default_size_hint_y_max`].
    '''

    default_pos_hint = ObjectProperty({})
    '''Default pos_hint value for items

    :attr:`default_pos_hint` is a :class:`~kivy.properties.DictProperty` and
    defaults to {}.
    '''
    key_pos_hint = StringProperty(None, allownone=True)
    '''If set, which key in the dict should be used to set the pos_hint of
    items.

    :attr:`key_pos_hint` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    initial_width = NumericProperty(100)
    '''Initial width for the items.

    :attr:`initial_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 100.
    '''
    initial_height = NumericProperty(100)
    '''Initial height for the items.

    :attr:`initial_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 100.
    '''
    initial_size = ReferenceListProperty(initial_width, initial_height)
    '''Initial size of items

    :attr:`initial_size` is a :class:`~kivy.properties.ReferenceListProperty`
    to [:attr:`initial_width`, :attr:`initial_height`].
    '''

    view_opts = []

    _size_needs_update = False
    _changed_views = []

    view_indices = {}

    def __init__(self, **kwargs):
        self.view_indices = {}
        self._updated_views = []
        self._trigger_layout = self._catch_layout_trigger
        super(RecycleLayout, self).__init__(**kwargs)

    def attach_recycleview(self, rv):
        super(RecycleLayout, self).attach_recycleview(rv)
        if rv:
            fbind = self.fbind
            fbind('default_size', rv.refresh_from_data)
            fbind('key_size', rv.refresh_from_data)
            fbind('default_size_hint', rv.refresh_from_data)
            fbind('key_size_hint', rv.refresh_from_data)
            fbind('default_size_hint_min', rv.refresh_from_data)
            fbind('key_size_hint_min', rv.refresh_from_data)
            fbind('default_size_hint_max', rv.refresh_from_data)
            fbind('key_size_hint_max', rv.refresh_from_data)
            fbind('default_pos_hint', rv.refresh_from_data)
            fbind('key_pos_hint', rv.refresh_from_data)

    def detach_recycleview(self):
        rv = self.recycleview
        if rv:
            funbind = self.funbind
            funbind('default_size', rv.refresh_from_data)
            funbind('key_size', rv.refresh_from_data)
            funbind('default_size_hint', rv.refresh_from_data)
            funbind('key_size_hint', rv.refresh_from_data)
            funbind('default_size_hint_min', rv.refresh_from_data)
            funbind('key_size_hint_min', rv.refresh_from_data)
            funbind('default_size_hint_max', rv.refresh_from_data)
            funbind('key_size_hint_max', rv.refresh_from_data)
            funbind('default_pos_hint', rv.refresh_from_data)
            funbind('key_pos_hint', rv.refresh_from_data)
        super(RecycleLayout, self).detach_recycleview()

    def _catch_layout_trigger(self, instance=None, value=None):
        rv = self.recycleview
        if rv is None:
            return

        idx = self.view_indices.get(instance)
        if idx is not None:
            if self._size_needs_update:
                return
            opt = self.view_opts[idx]
            if (instance.size == opt['size'] and
                    instance.size_hint == opt['size_hint'] and
                    instance.size_hint_min == opt['size_hint_min'] and
                    instance.size_hint_max == opt['size_hint_max'] and
                    instance.pos_hint == opt['pos_hint']):
                return
            self._size_needs_update = True
            rv.refresh_from_layout(view_size=True)
        else:
            rv.refresh_from_layout()

    def compute_sizes_from_data(self, data, flags):
        if [f for f in flags if not f]:
            # at least one changed data unpredictably
            self.clear_layout()
            opts = self.view_opts = [None for _ in data]
        else:
            opts = self.view_opts
            changed = False
            for flag in flags:
                for k, v in flag.items():
                    changed = True
                    if k == 'removed':
                        del opts[v]
                    elif k == 'appended':
                        opts.extend([None, ] * (v.stop - v.start))
                    elif k == 'inserted':
                        opts.insert(v, None)
                    elif k == 'modified':
                        start, stop, step = v.start, v.stop, v.step
                        r = range(start, stop) if step is None else \
                            range(start, stop, step)
                        for i in r:
                            opts[i] = None
                    else:
                        raise Exception('Unrecognized data flag {}'.format(k))

            if changed:
                self.clear_layout()

        assert len(data) == len(opts)
        ph_key = self.key_pos_hint
        ph_def = self.default_pos_hint
        sh_key = self.key_size_hint
        sh_def = self.default_size_hint
        sh_min_key = self.key_size_hint_min
        sh_min_def = self.default_size_hint_min
        sh_max_key = self.key_size_hint_max
        sh_max_def = self.default_size_hint_max
        s_key = self.key_size
        s_def = self.default_size
        viewcls_def = self.viewclass
        viewcls_key = self.key_viewclass
        iw, ih = self.initial_size

        sh = []
        for i, item in enumerate(data):
            if opts[i] is not None:
                continue

            ph = ph_def if ph_key is None else item.get(ph_key, ph_def)
            ph = item.get('pos_hint', ph)

            sh = sh_def if sh_key is None else item.get(sh_key, sh_def)
            sh = item.get('size_hint', sh)
            sh = [item.get('size_hint_x', sh[0]),
                  item.get('size_hint_y', sh[1])]

            sh_min = sh_min_def if sh_min_key is None else item.get(sh_min_key,
                                                                    sh_min_def)
            sh_min = item.get('size_hint_min', sh_min)
            sh_min = [item.get('size_hint_min_x', sh_min[0]),
                      item.get('size_hint_min_y', sh_min[1])]

            sh_max = sh_max_def if sh_max_key is None else item.get(sh_max_key,
                                                                    sh_max_def)
            sh_max = item.get('size_hint_max', sh_max)
            sh_max = [item.get('size_hint_max_x', sh_max[0]),
                      item.get('size_hint_max_y', sh_max[1])]

            s = s_def if s_key is None else item.get(s_key, s_def)
            s = item.get('size', s)
            w, h = s = item.get('width', s[0]), item.get('height', s[1])

            viewcls = None
            if viewcls_key is not None:
                viewcls = item.get(viewcls_key)
                if viewcls is not None:
                    viewcls = getattr(Factory, viewcls)
            if viewcls is None:
                viewcls = viewcls_def

            opts[i] = {
                'size': [(iw if w is None else w), (ih if h is None else h)],
                'size_hint': sh, 'size_hint_min': sh_min,
                'size_hint_max': sh_max, 'pos': None, 'pos_hint': ph,
                'viewclass': viewcls, 'width_none': w is None,
                'height_none': h is None}

    def compute_layout(self, data, flags):
        self._size_needs_update = False

        opts = self.view_opts
        changed = []
        for widget, index in self.view_indices.items():
            opt = opts[index]
            s = opt['size']
            w, h = sn = list(widget.size)
            sh = opt['size_hint']
            shnw, shnh = shn = list(widget.size_hint)
            sh_min = opt['size_hint_min']
            shn_min = list(widget.size_hint_min)
            sh_max = opt['size_hint_max']
            shn_max = list(widget.size_hint_max)
            ph = opt['pos_hint']
            phn = dict(widget.pos_hint)
            if s != sn or sh != shn or ph != phn or sh_min != shn_min or \
                    sh_max != shn_max:
                changed.append((index, widget, s, sn, sh, shn, sh_min, shn_min,
                                sh_max, shn_max, ph, phn))
                if shnw is None:
                    if shnh is None:
                        opt['size'] = sn
                    else:
                        opt['size'] = [w, s[1]]
                elif shnh is None:
                    opt['size'] = [s[0], h]
                opt['size_hint'] = shn
                opt['size_hint_min'] = shn_min
                opt['size_hint_max'] = shn_max
                opt['pos_hint'] = phn

        if [f for f in flags if not f]:  # need to redo everything
            self._changed_views = []
        else:
            self._changed_views = changed if changed else None

    def do_layout(self, *largs):
        assert False

    def set_visible_views(self, indices, data, viewport):
        view_opts = self.view_opts
        new, remaining, old = self.recycleview.view_adapter.set_visible_views(
            indices, data, view_opts)

        remove = self.remove_widget
        view_indices = self.view_indices
        for _, widget in old:
            remove(widget)
            del view_indices[widget]

        # first update the sizing info so that when we update the size
        # the widgets are not bound and won't trigger a re-layout
        refresh_view_layout = self.refresh_view_layout
        for index, widget in new:
            # make sure widget is added first so that any sizing updates
            # will be recorded
            opt = view_opts[index].copy()
            del opt['width_none']
            del opt['height_none']
            refresh_view_layout(index, opt, widget, viewport)

        # then add all the visible widgets, which binds size/size_hint
        add = self.add_widget
        for index, widget in new:
            # add to the container if it's not already done
            view_indices[widget] = index
            if widget.parent is None:
                add(widget)

        # finally, make sure if the size has changed to cause a re-layout
        changed = False
        for index, widget in new:
            opt = view_opts[index]
            if (changed or widget.size == opt['size'] and
                    widget.size_hint == opt['size_hint'] and
                    widget.size_hint_min == opt['size_hint_min'] and
                    widget.size_hint_max == opt['size_hint_max'] and
                    widget.pos_hint == opt['pos_hint']):
                continue
            changed = True

        if changed:
            # we could use LayoutChangeException here, but refresh_views in rv
            # needs to be updated to watch for it in the layout phase
            self._size_needs_update = True
            self.recycleview.refresh_from_layout(view_size=True)

    def refresh_view_layout(self, index, layout, view, viewport):
        opt = self.view_opts[index].copy()
        width_none = opt.pop('width_none')
        height_none = opt.pop('height_none')
        opt.update(layout)

        w, h = opt['size']
        shw, shh = opt['size_hint']
        if shw is None and width_none:
            w = None
        if shh is None and height_none:
            h = None
        opt['size'] = w, h
        super(RecycleLayout, self).refresh_view_layout(
            index, opt, view, viewport)

    def remove_views(self):
        super(RecycleLayout, self).remove_views()
        self.clear_widgets()
        self.view_indices = {}

    def remove_view(self, view, index):
        super(RecycleLayout, self).remove_view(view, index)
        self.remove_widget(view)
        del self.view_indices[view]

    def clear_layout(self):
        super(RecycleLayout, self).clear_layout()
        self.clear_widgets()
        self.view_indices = {}
        self._size_needs_update = False
