"""Viewport component for pytui - Elm-style scrollable text view.

A frozen dataclass + pure functions approach matching pytui's architecture.
Based on patterns from charmbracelet/bubbles viewport.

Usage:
    # In your Model
    @dataclass(frozen=True)
    class Model:
        viewport: Viewport = Viewport()

    # In update()
    case KeyPress(key="j"):
        new_vp = viewport_scroll_down(model.viewport, 1, visible_height)
        return replace(model, viewport=new_vp), Cmd.none()

    # In view()
    visible = viewport_visible_lines(model.viewport, visible_height)
    for line in visible:
        lines.append(line)
"""

from __future__ import annotations

from dataclasses import dataclass, replace


def _clamp(value: int, min_val: int, max_val: int) -> int:
    """Clamp value to [min_val, max_val]."""
    if min_val > max_val:
        min_val, max_val = max_val, min_val
    return max(min_val, min(value, max_val))


@dataclass(frozen=True)
class Viewport:
    """Immutable viewport state.

    Attributes:
        lines: Content as tuple of strings (one per line)
        y_offset: Vertical scroll position (0 = top)
        auto_follow: If True, scroll to bottom when new content added
    """

    lines: tuple[str, ...] = ()
    y_offset: int = 0
    auto_follow: bool = True


# ── Query functions ──────────────────────────────────────────────────────────


def viewport_max_offset(vp: Viewport, visible_height: int) -> int:
    """Maximum valid y_offset for this viewport."""
    return max(0, len(vp.lines) - visible_height)


def viewport_at_top(vp: Viewport) -> bool:
    """Check if viewport is scrolled to top."""
    return vp.y_offset <= 0


def viewport_at_bottom(vp: Viewport, visible_height: int) -> bool:
    """Check if viewport is scrolled to bottom."""
    return vp.y_offset >= viewport_max_offset(vp, visible_height)


def viewport_scroll_percent(vp: Viewport, visible_height: int) -> float:
    """Get scroll position as percentage (0.0 to 1.0)."""
    max_off = viewport_max_offset(vp, visible_height)
    if max_off == 0:
        return 1.0
    return vp.y_offset / max_off


def viewport_visible_lines(vp: Viewport, visible_height: int) -> list[str]:
    """Get the lines currently visible in the viewport."""
    start = _clamp(vp.y_offset, 0, len(vp.lines))
    end = _clamp(start + visible_height, start, len(vp.lines))
    return list(vp.lines[start:end])


# ── Scroll operations ────────────────────────────────────────────────────────


def viewport_scroll_down(vp: Viewport, n: int, visible_height: int) -> Viewport:
    """Scroll down by n lines. Returns unchanged viewport if already at bottom."""
    if viewport_at_bottom(vp, visible_height) or n <= 0:
        return vp
    max_off = viewport_max_offset(vp, visible_height)
    new_offset = _clamp(vp.y_offset + n, 0, max_off)
    # Re-enable auto_follow if we hit bottom
    auto_follow = new_offset >= max_off
    return replace(vp, y_offset=new_offset, auto_follow=auto_follow)


def viewport_scroll_up(vp: Viewport, n: int) -> Viewport:
    """Scroll up by n lines. Returns unchanged viewport if already at top."""
    if viewport_at_top(vp) or n <= 0:
        return vp
    new_offset = max(0, vp.y_offset - n)
    # Disable auto_follow when scrolling up
    return replace(vp, y_offset=new_offset, auto_follow=False)


def viewport_page_down(vp: Viewport, visible_height: int) -> Viewport:
    """Scroll down by one page."""
    return viewport_scroll_down(vp, visible_height, visible_height)


def viewport_page_up(vp: Viewport, visible_height: int) -> Viewport:
    """Scroll up by one page."""
    return viewport_scroll_up(vp, visible_height)


def viewport_half_page_down(vp: Viewport, visible_height: int) -> Viewport:
    """Scroll down by half a page (Ctrl+D style)."""
    return viewport_scroll_down(vp, visible_height // 2, visible_height)


def viewport_half_page_up(vp: Viewport, visible_height: int) -> Viewport:
    """Scroll up by half a page (Ctrl+U style)."""
    return viewport_scroll_up(vp, visible_height // 2)


def viewport_goto_top(vp: Viewport) -> Viewport:
    """Jump to top of content."""
    if viewport_at_top(vp):
        return vp
    return replace(vp, y_offset=0, auto_follow=False)


def viewport_goto_bottom(vp: Viewport, visible_height: int) -> Viewport:
    """Jump to bottom of content, re-enable auto_follow."""
    max_off = viewport_max_offset(vp, visible_height)
    return replace(vp, y_offset=max_off, auto_follow=True)


# ── Content operations ───────────────────────────────────────────────────────


def viewport_set_content(vp: Viewport, lines: tuple[str, ...], visible_height: int) -> Viewport:
    """Replace all content. Adjusts scroll if past new content end."""
    new_vp = replace(vp, lines=lines)
    max_off = viewport_max_offset(new_vp, visible_height)

    # If scrolled past content, jump to bottom
    if new_vp.y_offset > max_off:
        return replace(new_vp, y_offset=max_off)

    # If auto_follow enabled, stay at bottom
    if new_vp.auto_follow:
        return replace(new_vp, y_offset=max_off)

    return new_vp


def viewport_append_line(vp: Viewport, line: str, visible_height: int) -> Viewport:
    """Append a single line. Auto-scrolls if auto_follow enabled."""
    new_lines = vp.lines + (line,)
    new_vp = replace(vp, lines=new_lines)

    if vp.auto_follow:
        max_off = viewport_max_offset(new_vp, visible_height)
        return replace(new_vp, y_offset=max_off)

    return new_vp


def viewport_append_lines(vp: Viewport, lines: tuple[str, ...], visible_height: int) -> Viewport:
    """Append multiple lines. Auto-scrolls if auto_follow enabled."""
    if not lines:
        return vp
    new_lines = vp.lines + lines
    new_vp = replace(vp, lines=new_lines)

    if vp.auto_follow:
        max_off = viewport_max_offset(new_vp, visible_height)
        return replace(new_vp, y_offset=max_off)

    return new_vp


def viewport_clear(vp: Viewport) -> Viewport:
    """Clear all content and reset scroll."""
    return Viewport(lines=(), y_offset=0, auto_follow=True)
