"""
RecycleGridLayout
=================

.. 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.

The RecycleGridLayout is designed to provide a
:class:`~kivy.uix.gridlayout.GridLayout` type layout when used with the
:class:`~kivy.uix.recycleview.RecycleView` widget. Please refer to the
:mod:`~kivy.uix.recycleview` module documentation for more information.
"""

import itertools
chain_from_iterable = itertools.chain.from_iterable
from kivy.uix.recyclelayout import RecycleLayout
from kivy.uix.gridlayout import GridLayout, GridLayoutException, nmax, nmin
from collections import defaultdict
from math import ceil

__all__ = ('RecycleGridLayout', )


class RecycleGridLayout(RecycleLayout, GridLayout):

    _cols_pos = None
    _rows_pos = None

    def __init__(self, **kwargs):
        super(RecycleGridLayout, self).__init__(**kwargs)
        self.funbind('children', self._trigger_layout)

    def on_children(self, instance, value):
        pass

    def _fill_rows_cols_sizes(self):
        cols, rows = self._cols, self._rows
        cols_sh, rows_sh = self._cols_sh, self._rows_sh
        cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min
        cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
        self._cols_count = cols_count = [defaultdict(int) for _ in cols]
        self._rows_count = rows_count = [defaultdict(int) for _ in rows]

        # calculate minimum size for each columns and rows
        idx_iter = self._create_idx_iter(len(cols), len(rows))
        has_bound_y = has_bound_x = False
        for opt, (col, row) in zip(self.view_opts, idx_iter):
            (shw, shh), (w, h) = opt['size_hint'], opt['size']
            shw_min, shh_min = opt['size_hint_min']
            shw_max, shh_max = opt['size_hint_max']

            if shw is None:
                cols_count[col][w] += 1
            if shh is None:
                rows_count[row][h] += 1

            # compute minimum size / maximum stretch needed
            if shw is None:
                cols[col] = nmax(cols[col], w)
            else:
                cols_sh[col] = nmax(cols_sh[col], shw)
                if shw_min is not None:
                    has_bound_x = True
                    cols_sh_min[col] = nmax(cols_sh_min[col], shw_min)
                if shw_max is not None:
                    has_bound_x = True
                    cols_sh_max[col] = nmin(cols_sh_max[col], shw_max)

            if shh is None:
                rows[row] = nmax(rows[row], h)
            else:
                rows_sh[row] = nmax(rows_sh[row], shh)
                if shh_min is not None:
                    has_bound_y = True
                    rows_sh_min[row] = nmax(rows_sh_min[row], shh_min)
                if shh_max is not None:
                    has_bound_y = True
                    rows_sh_max[row] = nmin(rows_sh_max[row], shh_max)
        self._has_hint_bound_x = has_bound_x
        self._has_hint_bound_y = has_bound_y

    def _update_rows_cols_sizes(self, changed):
        cols_count, rows_count = self._cols_count, self._rows_count
        cols, rows = self._cols, self._rows
        remove_view = self.remove_view
        n_cols = len(cols)
        n_rows = len(rows)
        orientation = self.orientation

        # this can be further improved to reduce re-comp, but whatever...
        for index, widget, (w, h), (wn, hn), sh, shn, sh_min, shn_min, \
                sh_max, shn_max, _, _ in changed:
            if sh != shn or sh_min != shn_min or sh_max != shn_max:
                return True
            elif (sh[0] is not None and w != wn and
                  (h == hn or sh[1] is not None) or
                  sh[1] is not None and h != hn and
                  (w == wn or sh[0] is not None)):
                remove_view(widget, index)
            else:  # size hint is None, so check if it can be resized inplace
                col, row = self._calculate_idx_from_a_view_idx(
                    n_cols, n_rows, index)
                if w != wn:
                    col_w = cols[col]
                    cols_count[col][w] -= 1
                    cols_count[col][wn] += 1
                    was_last_w = cols_count[col][w] <= 0
                    if was_last_w and col_w == w or wn > col_w:
                        return True
                    if was_last_w:
                        del cols_count[col][w]

                if h != hn:
                    row_h = rows[row]
                    rows_count[row][h] -= 1
                    rows_count[row][hn] += 1
                    was_last_h = rows_count[row][h] <= 0
                    if was_last_h and row_h == h or hn > row_h:
                        return True
                    if was_last_h:
                        del rows_count[row][h]

        return False

    def compute_layout(self, data, flags):
        super(RecycleGridLayout, self).compute_layout(data, flags)

        n = len(data)
        smax = self.get_max_widgets()
        if smax and n > smax:
            raise GridLayoutException(
                'Too many children ({}) in GridLayout. Increase rows/cols!'.
                format(n))

        changed = self._changed_views
        if (changed is None or
                changed and not self._update_rows_cols_sizes(changed)):
            return

        self.clear_layout()
        if not self._init_rows_cols_sizes(n):
            self._cols_pos = None
            l, t, r, b = self.padding
            self.minimum_size = l + r, t + b
            return
        self._fill_rows_cols_sizes()
        self._update_minimum_size()
        self._finalize_rows_cols_sizes()

        view_opts = self.view_opts
        for widget, x, y, w, h in self._iterate_layout(n):
            opt = view_opts[n - widget - 1]
            shw, shh = opt['size_hint']
            opt['pos'] = x, y
            wo, ho = opt['size']
            # layout won't/shouldn't change previous size if size_hint is None
            # which is what w/h being None means.
            opt['size'] = [(wo if shw is None else w),
                           (ho if shh is None else h)]

        spacing_x, spacing_y = self.spacing
        cols, rows = self._cols, self._rows

        cols_pos = self._cols_pos = [None, ] * len(cols)
        rows_pos = self._rows_pos = [None, ] * len(rows)

        cols_pos[0] = self.x
        last = cols_pos[0] + self.padding[0] + cols[0] + spacing_x / 2.
        for i, val in enumerate(cols[1:], 1):
            cols_pos[i] = last
            last += val + spacing_x

        last = rows_pos[-1] = \
            self.y + self.height - self.padding[1] - rows[0] - spacing_y / 2.
        n = len(rows)
        for i, val in enumerate(rows[1:], 1):
            last -= spacing_y + val
            rows_pos[n - 1 - i] = last

    def get_view_index_at(self, pos):
        if self._cols_pos is None:
            return 0

        x, y = pos
        col_pos = self._cols_pos
        row_pos = self._rows_pos
        cols, rows = self._cols, self._rows
        if not col_pos or not row_pos:
            return 0

        if x >= col_pos[-1]:
            ix = len(cols) - 1
        else:
            ix = 0
            for val in col_pos[1:]:
                if x < val:
                    break
                ix += 1

        if y >= row_pos[-1]:
            iy = len(rows) - 1
        else:
            iy = 0
            for val in row_pos[1:]:
                if y < val:
                    break
                iy += 1

        if not self._fills_from_left_to_right:
            ix = len(cols) - ix - 1
        if self._fills_from_top_to_bottom:
            iy = len(rows) - iy - 1
        return (iy * len(cols) + ix) if self._fills_row_first else \
            (ix * len(rows) + iy)

    def compute_visible_views(self, data, viewport):
        if self._cols_pos is None:
            return []
        x, y, w, h = viewport
        right = x + w
        top = y + h
        at_idx = self.get_view_index_at
        tl, tr, bl, br = sorted((
            at_idx((x, y)),
            at_idx((right, y)),
            at_idx((x, top)),
            at_idx((right, top)),
        ))

        n = len(data)
        if len({tl, tr, bl, br}) < 4:
            # visible area is one row/column
            return range(min(n, tl), min(n, br + 1))
        indices = []
        stride = len(self._cols) if self._fills_row_first else len(self._rows)
        if stride:
            x_slice = br - bl + 1
            indices = chain_from_iterable(
                range(min(s, n), min(n, s + x_slice))
                for s in range(tl, bl + 1, stride))
        return indices

    def _calculate_idx_from_a_view_idx(self, n_cols, n_rows, view_idx):
        '''returns a tuple of (column-index, row-index) from a view-index'''
        if self._fills_row_first:
            row_idx, col_idx = divmod(view_idx, n_cols)
        else:
            col_idx, row_idx = divmod(view_idx, n_rows)
        if not self._fills_from_left_to_right:
            col_idx = n_cols - col_idx - 1
        if not self._fills_from_top_to_bottom:
            row_idx = n_rows - row_idx - 1
        return (col_idx, row_idx, )

    def goto_view(self, index):
        """Scroll the view to make the specified index visible.

        Args:
            index (int): The index in the data list to scroll to.
        """
        if not self.view_opts or not self.parent:
            return

        # Calculate grid dimensions
        num_items = len(self.parent.data)
        cols = self.cols
        rows = self.rows

        # Limit index to valid range and handle negative indices
        index = max(-num_items, min(index, num_items - 1))
        if index < 0:
            index = num_items + index

        # If cols/rows not set, calculate them
        if cols is None:
            cols = ceil(num_items / rows)
        if rows is None:
            rows = ceil(num_items / cols)

        # Calculate row and column of target index
        if self._fills_row_first:
            row_idx = index // cols
            col_idx = index % cols
        else:
            col_idx = index // rows
            row_idx = index % rows

        # Calculate total dimensions
        spacing_x, spacing_y = self.spacing
        padding_left, padding_top, padding_right, padding_bottom = self.padding
        x_padding = padding_left + padding_right
        y_padding = padding_top + padding_bottom
        viewport_width = self.parent.width
        viewport_height = self.parent.height

        # Calculate total width and height of the grid
        total_width = sum(self._cols) + (cols - 1) * spacing_x + x_padding
        total_height = sum(self._rows) + (rows - 1) * spacing_y + y_padding

        if self._fills_from_left_to_right:
            x_pos = (
                padding_left + sum(self._cols[:col_idx]) + col_idx * spacing_x
            )
            target_scroll_x = max(
                0, min(1, x_pos / (total_width - viewport_width))
            )
        else:
            # right to left
            col_idx = cols - col_idx - 1
            x_pos = (
                padding_left
                + sum(self._cols[col_idx + 1 :])
                + (cols - col_idx - 1) * spacing_x
            )
            # position at left of viewport
            x_pos = x_pos - viewport_width + self._cols[col_idx]
            target_scroll_x = max(
                0, min(1, 1 - (x_pos / (total_width - viewport_width)))
            )

        if self._fills_from_top_to_bottom:
            y_pos = (
                padding_top + sum(self._rows[:row_idx]) + row_idx * spacing_y
            )
            target_scroll_y = max(
                0, min(1, 1 - (y_pos / (total_height - viewport_height)))
            )
        else:
            # Calculate base position from bottom
            row_idx = rows - row_idx - 1
            y_pos = (
                padding_top
                + sum(self._rows[row_idx + 1 :])
                + (rows - row_idx - 1) * spacing_y
            )
            # position at top of viewport
            y_pos = y_pos - viewport_height + self._rows[row_idx]
            target_scroll_y = max(
                0, min(1, y_pos / (total_height - viewport_height))
            )

        # Adjust scroll position to center big widgets
        widget_width = self._cols[col_idx]
        widget_height = self._rows[row_idx]
        if widget_width > viewport_width:
            # center wide widgets
            target_scroll_x = max(
                0,
                min(
                    1,
                    target_scroll_x
                    + (widget_width - viewport_width)
                    / (2 * (total_width - viewport_width)),
                ),
            )
        if widget_height > viewport_height:
            # center tall widgets
            target_scroll_y = max(
                0,
                min(
                    1,
                    target_scroll_y
                    - (widget_height - viewport_height)
                    / (2 * (total_height - viewport_height)),
                ),
            )

        # Apply scroll positions
        self.parent.scroll_x = target_scroll_x
        self.parent.scroll_y = target_scroll_y
