"""Pure math functions for viewport window calculations and navigation."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/windowing.ipynb.

# %% auto #0
__all__ = ['clamp_window_start', 'compute_window', 'navigate', 'navigate_cursor', 'find_nearest_focusable']

# %% ../../nbs/core/windowing.ipynb #8340f186
from typing import Callable, Optional, Tuple

# %% ../../nbs/core/windowing.ipynb #d381da31
def clamp_window_start(window_start: int,  # Requested first visible row
                       visible_rows: int,   # Number of visible rows
                       total_items: int,     # Total item count
                      ) -> int:              # Clamped window_start
    """Clamp window_start to valid range."""
    if total_items <= 0: return 0
    max_start = max(0, total_items - visible_rows)
    return max(0, min(window_start, max_start))

# %% ../../nbs/core/windowing.ipynb #a24dae6f
def compute_window(window_start: int,   # First visible row index (already clamped)
                   visible_rows: int,    # Number of visible rows
                   total_items: int,     # Total item count
                  ) -> Tuple[int, int]:  # (render_start, render_end) exclusive end
    """Compute the range of rows to render."""
    if total_items <= 0: return (0, 0)
    render_start = max(0, window_start)
    render_end = min(total_items, window_start + visible_rows)
    return (render_start, render_end)

# %% ../../nbs/core/windowing.ipynb #f9879d8b
from cjm_fasthtml_virtual_scrollbar.core.math import compute_scrollbar

# %% ../../nbs/core/windowing.ipynb #3737fb14
def navigate(window_start: int,   # Current first visible row
             direction: str,       # 'up', 'down', 'page_up', 'page_down', 'first', 'last'
             visible_rows: int,    # Number of visible rows
             total_items: int,     # Total item count
            ) -> int:              # New window_start (clamped)
    """Compute new window_start for a navigation action."""
    if direction == 'up':
        new_start = window_start - 1
    elif direction == 'down':
        new_start = window_start + 1
    elif direction == 'page_up':
        page_size = max(1, visible_rows - 1)
        new_start = window_start - page_size
    elif direction == 'page_down':
        page_size = max(1, visible_rows - 1)
        new_start = window_start + page_size
    elif direction == 'first':
        new_start = 0
    elif direction == 'last':
        new_start = max(0, total_items - visible_rows)
    else:
        raise ValueError(f"Unknown direction: {direction}")

    return clamp_window_start(new_start, visible_rows, total_items)

# %% ../../nbs/core/windowing.ipynb #693xsxeclrs
def navigate_cursor(
    cursor_index: int,    # Current cursor position (-1 treated as window_start)
    direction: str,       # 'up' or 'down'
    window_start: int,    # Current first visible row
    visible_rows: int,    # Number of visible rows
    total_items: int,     # Total item count
    is_skippable: Optional[Callable[[int], bool]] = None,  # Predicate: index -> skip this item?
) -> Tuple[int, int, bool]:  # (new_cursor, new_window_start, window_changed)
    """Move cursor up/down within the visible window, scrolling at edges."""
    if total_items <= 0:
        return (0, 0, False)

    # Handle uninitialized cursor
    if cursor_index < 0:
        cursor_index = window_start

    max_cursor = total_items - 1

    if direction == 'down':
        new_cursor = min(cursor_index + 1, max_cursor)
    elif direction == 'up':
        new_cursor = max(cursor_index - 1, 0)
    else:
        raise ValueError(f"navigate_cursor only handles 'up'/'down', got: {direction}")

    # Skip over skippable items in the movement direction
    if is_skippable is not None:
        if direction == 'down':
            while new_cursor <= max_cursor and is_skippable(new_cursor):
                new_cursor += 1
            if new_cursor > max_cursor:
                # No focusable item ahead — cursor stays, but window
                # can scroll down to reveal trailing skippable items
                new_ws = min(window_start + 1, max(0, total_items - visible_rows))
                return (cursor_index, new_ws, new_ws != window_start)
        else:  # up
            while new_cursor >= 0 and is_skippable(new_cursor):
                new_cursor -= 1
            if new_cursor < 0:
                # No focusable item behind — cursor stays, but window
                # can scroll up to reveal leading skippable items
                new_ws = max(0, window_start - 1)
                return (cursor_index, new_ws, new_ws != window_start)

    # Check if cursor is still within visible window
    window_end = window_start + visible_rows  # exclusive
    new_window_start = window_start

    if new_cursor >= window_end:
        new_window_start = new_cursor - visible_rows + 1
    elif new_cursor < window_start:
        new_window_start = new_cursor

    new_window_start = clamp_window_start(new_window_start, visible_rows, total_items)
    window_changed = (new_window_start != window_start)

    return (new_cursor, new_window_start, window_changed)

# %% ../../nbs/core/windowing.ipynb #51rium9lmsi
def find_nearest_focusable(
    index: int,           # Starting index to search from
    total_items: int,     # Total item count
    is_skippable: Optional[Callable[[int], bool]] = None,  # Predicate: index -> skip?
    direction: int = 1,   # Search direction: 1 (forward) or -1 (backward)
) -> int:  # Nearest focusable index, or -1 if none found
    """Find the nearest non-skippable index from a starting position."""
    if is_skippable is None or total_items <= 0:
        return max(0, min(index, total_items - 1)) if total_items > 0 else -1

    index = max(0, min(index, total_items - 1))

    # Try the requested direction first
    i = index
    while 0 <= i < total_items:
        if not is_skippable(i):
            return i
        i += direction

    # If not found, try the other direction
    i = index - direction
    while 0 <= i < total_items:
        if not is_skippable(i):
            return i
        i -= direction

    return -1  # All items are skippable
