"""
`ScrollView` is a base class for [Line API](/guide/widgets#line-api) widgets.
"""

from __future__ import annotations

from rich.console import RenderableType

from textual._animator import EasingFunction
from textual._types import AnimationLevel, CallbackType
from textual.containers import ScrollableContainer
from textual.geometry import Region, Size


class ScrollView(ScrollableContainer):
    """
    A base class for a Widget that handles its own scrolling (i.e. doesn't rely
    on the compositor to render children).

    !!! note

        This is the typically wrong class for making something scrollable. If you want to make something scroll, set its
        `overflow` style to auto or scroll. Or use one of the pre-defined scrolling containers such as [VerticalScroll][textual.containers.VerticalScroll].
    """

    ALLOW_MAXIMIZE = True

    DEFAULT_CSS = """
    ScrollView {
        overflow-y: auto;
        overflow-x: auto;
    }
    """

    @property
    def is_scrollable(self) -> bool:
        """Always scrollable."""
        return True

    @property
    def is_container(self) -> bool:
        """Since a ScrollView should be a line-api widget, it won't have children,
        and therefore isn't a container."""
        return False

    def watch_scroll_x(self, old_value: float, new_value: float) -> None:
        if self.show_horizontal_scrollbar:
            self.horizontal_scrollbar.position = new_value
        if round(old_value) != round(new_value):
            self.refresh(self.size.region)

    def watch_scroll_y(self, old_value: float, new_value: float) -> None:
        if self.show_vertical_scrollbar:
            self.vertical_scrollbar.position = new_value
        if round(old_value) != round(new_value):
            self.refresh(self.size.region)

    def on_mount(self):
        self._refresh_scrollbars()

    def get_content_width(self, container: Size, viewport: Size) -> int:
        """Gets the width of the content area.

        Args:
            container: Size of the container (immediate parent) widget.
            viewport: Size of the viewport.

        Returns:
            The optimal width of the content.
        """
        return self.virtual_size.width

    def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
        """Gets the height (number of lines) in the content area.

        Args:
            container: Size of the container (immediate parent) widget.
            viewport: Size of the viewport.
            width: Width of renderable.

        Returns:
            The height of the content.
        """
        return self.virtual_size.height

    def _size_updated(
        self, size: Size, virtual_size: Size, container_size: Size, layout: bool = True
    ) -> bool:
        """Called when size is updated.

        Args:
            size: New size.
            virtual_size: New virtual size.
            container_size: New container size.
            layout: Perform layout if required.

        Returns:
            True if a resize event should be sent, otherwise False.
        """
        if size_changed := self._size != size:
            self._set_dirty()
        if (
            size_changed
            or virtual_size != self.virtual_size
            or container_size != self.container_size
        ):
            self._scrollbar_changes.clear()
            self._size = size
            virtual_size = self.virtual_size
            self._container_size = size - self.styles.gutter.totals
            self._scroll_update(virtual_size)

        return size_changed or self._container_size != container_size

    def render(self) -> RenderableType:
        """Render the scrollable region (if `render_lines` is not implemented).

        Returns:
            Renderable object.
        """
        from rich.panel import Panel

        return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}")

    # Custom scroll to which doesn't require call_after_refresh
    def scroll_to(
        self,
        x: float | None = None,
        y: float | None = None,
        *,
        animate: bool = True,
        speed: float | None = None,
        duration: float | None = None,
        easing: EasingFunction | str | None = None,
        force: bool = False,
        on_complete: CallbackType | None = None,
        level: AnimationLevel = "basic",
        immediate: bool = False,
    ) -> None:
        """Scroll to a given (absolute) coordinate, optionally animating.

        Args:
            x: X coordinate (column) to scroll to, or `None` for no change.
            y: Y coordinate (row) to scroll to, or `None` for no change.
            animate: Animate to new scroll position.
            speed: Speed of scroll if `animate` is `True`; or `None` to use `duration`.
            duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
            easing: An easing method for the scrolling animation.
            force: Force scrolling even when prohibited by overflow styling.
            on_complete: A callable to invoke when the animation is finished.
            level: Minimum level required for the animation to take place (inclusive).
            immediate: If `False` the scroll will be deferred until after a screen refresh,
                set to `True` to scroll immediately.
        """

        self._scroll_to(
            x,
            y,
            animate=animate,
            speed=speed,
            duration=duration,
            easing=easing,
            force=force,
            on_complete=on_complete,
            level=level,
        )

    def refresh_line(self, y: int) -> None:
        """Refresh a single line.

        Args:
            y: Coordinate of line.
        """
        self.refresh(
            Region(
                0,
                y - self.scroll_offset.y,
                max(self.virtual_size.width, self.size.width),
                1,
            )
        )

    def refresh_lines(self, y_start: int, line_count: int = 1) -> None:
        """Refresh one or more lines.

        Args:
            y_start: First line to refresh.
            line_count: Total number of lines to refresh.
        """
        refresh_region = Region(
            0,
            y_start - self.scroll_offset.y,
            max(self.virtual_size.width, self.size.width),
            line_count,
        )
        self.refresh(refresh_region)
