from __future__ import annotations

from functools import lru_cache
from typing import TYPE_CHECKING, Callable, Iterable, Sequence

import rich.repr
from rich.segment import Segment
from rich.style import Style as RichStyle
from rich.terminal_theme import TerminalTheme

from textual import log
from textual._ansi_theme import DEFAULT_TERMINAL_THEME
from textual._border import get_box, render_border_label, render_row
from textual._context import active_app
from textual._opacity import _apply_opacity
from textual._segment_tools import apply_hatch, line_pad, line_trim, make_blank
from textual.color import TRANSPARENT, Color
from textual.constants import DEBUG
from textual.content import Content
from textual.filter import LineFilter
from textual.geometry import Region, Size, Spacing
from textual.renderables.text_opacity import TextOpacity
from textual.renderables.tint import Tint
from textual.strip import Strip
from textual.style import Style

if TYPE_CHECKING:
    from typing_extensions import TypeAlias

    from textual.css.styles import StylesBase
    from textual.widget import Widget

RenderLineCallback: TypeAlias = Callable[[int], Strip]


@rich.repr.auto(angular=True)
class StylesCache:
    """Responsible for rendering CSS Styles and keeping a cache of rendered lines.

    The render method applies border, outline, and padding set in the Styles object to widget content.

    The diagram below shows content (possibly from a Rich renderable) with padding and border. The
    labels A. B. and C. indicate the code path (see comments in render_line below) chosen to render
    the indicated lines.

    ```
    ┏━━━━━━━━━━━━━━━━━━━━━━┓◀── A. border
    ┃                      ┃◀┐
    ┃                      ┃ └─ B. border + padding +
    ┃   Lorem ipsum dolor  ┃◀┐         border
    ┃   sit amet,          ┃ │
    ┃   consectetur        ┃ └─ C. border + padding +
    ┃   adipiscing elit,   ┃     content + padding +
    ┃   sed do eiusmod     ┃           border
    ┃   tempor incididunt  ┃
    ┃                      ┃
    ┃                      ┃
    ┗━━━━━━━━━━━━━━━━━━━━━━┛
    ```
    """

    def __init__(self) -> None:
        self._cache: dict[int, Strip] = {}
        self._dirty_lines: set[int] = set()
        self._width = 1
        self._simple_strip: Strip | None = None
        """A simple strip consisting of left border + background + right border, which may be reused in a render."""

    def __rich_repr__(self) -> rich.repr.Result:
        if self._dirty_lines:
            yield "dirty", self._dirty_lines
        yield "width", self._width, 1

    def set_dirty(self, *regions: Region) -> None:
        """Add a dirty regions."""
        if regions:
            for region in regions:
                self._dirty_lines.update(region.line_range)
        else:
            self.clear()

    def is_dirty(self, y: int) -> bool:
        """Check if a given line is dirty (needs to be rendered again).

        Args:
            y: Y coordinate of line.

        Returns:
            True if line requires a render, False if can be cached.
        """
        return y in self._dirty_lines

    def clear(self) -> None:
        """Clear the styles cache (will cause the content to re-render)."""

        self._cache.clear()
        self._dirty_lines.clear()

    def render_widget(self, widget: Widget, crop: Region) -> list[Strip]:
        """Render the content for a widget.

        Args:
            widget: A widget.
            region: A region of the widget to render.

        Returns:
            Rendered lines.
        """
        border_title = widget._border_title
        border_subtitle = widget._border_subtitle
        self._simple_strip = None

        base_background, background = widget.background_colors
        styles = widget.styles
        strips = self.render(
            styles,
            widget.region.size,
            base_background,
            background,
            widget.render_line,
            widget.get_line_filters(),
            (
                None
                if border_title is None
                else (
                    border_title,
                    *widget._get_title_style_information(base_background),
                )
            ),
            (
                None
                if border_subtitle is None
                else (
                    border_subtitle,
                    *widget._get_subtitle_style_information(base_background),
                )
            ),
            content_size=widget.content_region.size,
            padding=styles.padding,
            crop=crop,
            opacity=widget.opacity,
            ansi_theme=widget.app.ansi_theme,
        )

        if widget.auto_links:
            hover_style = widget.hover_style
            if (
                hover_style._link_id
                and hover_style._meta
                and "@click" in hover_style.meta
            ):
                link_style_hover = widget.link_style_hover
                if link_style_hover:
                    strips = [
                        strip.style_links(hover_style.link_id, link_style_hover)
                        for strip in strips
                    ]

        return strips

    def render(
        self,
        styles: StylesBase,
        size: Size,
        base_background: Color,
        background: Color,
        render_content_line: RenderLineCallback,
        filters: Sequence[LineFilter],
        border_title: tuple[Content, Color, Color, Style] | None,
        border_subtitle: tuple[Content, Color, Color, Style] | None,
        content_size: Size | None = None,
        padding: Spacing | None = None,
        crop: Region | None = None,
        opacity: float = 1.0,
        ansi_theme: TerminalTheme = DEFAULT_TERMINAL_THEME,
    ) -> list[Strip]:
        """Render a widget content plus CSS styles.

        Args:
            styles: CSS Styles object.
            size: Size of widget.
            base_background: Background color beneath widget.
            background: Background color of widget.
            render_content_line: Callback to render content line.
            console: The console in use by the app.
            border_title: Optional tuple of (title, color, background, style).
            border_subtitle: Optional tuple of (subtitle, color, background, style).
            content_size: Size of content or None to assume full size.
            padding: Override padding from Styles, or None to use styles.padding.
            crop: Region to crop to.
            filters: Additional post-processing for the segments.
            opacity: Widget opacity.
            ansi_theme: Theme for ANSI colors.

        Returns:
            Rendered lines.
        """
        if content_size is None:
            content_size = size
        if padding is None:
            padding = styles.padding
        if crop is None:
            crop = size.region

        width, _height = size
        if width != self._width:
            self.clear()
            self._width = width
        strips: list[Strip] = []
        add_strip = strips.append

        is_dirty = self._dirty_lines.__contains__
        render_line = self.render_line

        for y in crop.line_range:
            if is_dirty(y) or y not in self._cache:
                strip = render_line(
                    styles,
                    y,
                    size,
                    content_size,
                    padding,
                    base_background,
                    background,
                    render_content_line,
                    border_title,
                    border_subtitle,
                    opacity,
                    ansi_theme,
                )
                self._cache[y] = strip
            else:
                strip = self._cache[y]

            for filter in filters:
                strip = strip.apply_filter(filter, background)

            if DEBUG:
                if any([not (segment.control or segment.text) for segment in strip]):
                    log.warning(f"Strip contains invalid empty Segments: {strip!r}.")

            add_strip(strip)

        self._dirty_lines.difference_update(crop.line_range)

        if crop.column_span != (0, width):
            x1, x2 = crop.column_span
            strips = [strip.crop(x1, x2) for strip in strips]

        return strips

    @lru_cache(1024)
    def get_inner_outer(
        cls, base_background: Color, background: Color
    ) -> tuple[Style, Style]:
        """Get inner and outer background colors."""
        return (
            Style(background=base_background + background),
            Style(background=base_background),
        )

    def render_line(
        self,
        styles: StylesBase,
        y: int,
        size: Size,
        content_size: Size,
        padding: Spacing,
        base_background: Color,
        background: Color,
        render_content_line: Callable[[int], Strip],
        border_title: tuple[Content, Color, Color, Style] | None,
        border_subtitle: tuple[Content, Color, Color, Style] | None,
        opacity: float,
        ansi_theme: TerminalTheme,
    ) -> Strip:
        """Render a styled line.

        Args:
            styles: Styles object.
            y: The y coordinate of the line (relative to widget screen offset).
            size: Size of the widget.
            content_size: Size of the content area.
            padding: Padding.
            base_background: Background color of widget beneath this line.
            background: Background color of widget.
            render_content_line: Callback to render a line of content.
            console: The console in use by the app.
            border_title: Optional tuple of (title, color, background, style).
            border_subtitle: Optional tuple of (subtitle, color, background, style).
            opacity: Opacity of line.

        Returns:
            A line of segments.
        """

        gutter = styles.gutter
        width, height = size
        content_width, content_height = content_size

        pad_top, pad_right, pad_bottom, pad_left = padding

        (
            (border_top, border_top_color),
            (border_right, border_right_color),
            (border_bottom, border_bottom_color),
            (border_left, border_left_color),
        ) = styles.border

        (
            (outline_top, outline_top_color),
            (outline_right, outline_right_color),
            (outline_bottom, outline_bottom_color),
            (outline_left, outline_left_color),
        ) = styles.outline

        from_color = RichStyle.from_color
        inner, outer = self.get_inner_outer(base_background, background)

        def line_post(segments: Iterable[Segment]) -> Iterable[Segment]:
            """Apply effects to segments inside the border."""
            if styles.has_rule("hatch") and styles.hatch != "none":
                character, color = styles.hatch
                if character != " " and color.a > 0:
                    hatch_style = from_color(
                        (background + color).rich_color, background.rich_color
                    )
                    return apply_hatch(segments, character, hatch_style)
            return segments

        def post(segments: Iterable[Segment]) -> Iterable[Segment]:
            """Post process segments to apply opacity and tint.

            Args:
                segments: Iterable of segments.

            Returns:
                New list of segments
            """
            try:
                app = active_app.get()
                ansi_theme = app.ansi_theme
            except LookupError:
                ansi_theme = DEFAULT_TERMINAL_THEME

            if styles.tint.a:
                segments = Tint.process_segments(
                    segments, styles.tint, ansi_theme, background
                )
            if opacity != 1.0:
                segments = _apply_opacity(segments, base_background, opacity)
            return segments

        cache_simple_strip: bool = False
        line: Iterable[Segment]
        # Draw top or bottom borders (A)
        if (border_top and y == 0) or (border_bottom and y == height - 1):
            is_top = y == 0
            border_color = base_background + (
                border_top_color if is_top else border_bottom_color
            ).multiply_alpha(opacity)
            border_color_as_style = Style(foreground=border_color)
            border_edge_type = border_top if is_top else border_bottom
            has_left = border_left != ""
            has_right = border_right != ""
            border_label = border_title if is_top else border_subtitle
            if border_label is None:
                render_label = None
            else:
                label, label_color, label_background, style = border_label
                base_label_background = base_background + background
                style += Style(
                    (
                        (base_label_background + label_background)
                        if label_background.a
                        else TRANSPARENT
                    ),
                    (
                        (base_label_background + label_color)
                        if label_color.a
                        else TRANSPARENT
                    ),
                )
                render_label = (label, style)

            # Try to save time with expensive call to `render_border_label`:
            if render_label:
                label_segments = render_border_label(
                    render_label,
                    is_top,
                    border_edge_type,
                    width - 2,
                    inner,
                    outer,
                    border_color_as_style,
                    has_left,
                    has_right,
                )
            else:
                label_segments = []
            box_segments = get_box(
                border_edge_type,
                inner,
                outer,
                border_color_as_style,
            )
            label_alignment = (
                styles.border_title_align if is_top else styles.border_subtitle_align
            )
            line = render_row(
                box_segments[0 if is_top else 2],
                width,
                has_left,
                has_right,
                label_segments,
                label_alignment,  # type: ignore
            )
        # Draw padding (B)
        elif (pad_top and y < gutter.top) or (
            pad_bottom and y >= height - gutter.bottom
        ):
            if self._simple_strip is not None:
                return self._simple_strip
            cache_simple_strip = True
            background_rich_style = inner.rich_style
            left_style = Style(
                foreground=base_background + border_left_color.multiply_alpha(opacity)
            )
            left = get_box(border_left, inner, outer, left_style)[1][0]
            right_style = Style(
                foreground=base_background + border_right_color.multiply_alpha(opacity)
            )
            right = get_box(border_right, inner, outer, right_style)[1][2]
            if border_left and border_right:
                line = [left, make_blank(width - 2, background_rich_style), right]
            elif border_left:
                line = [left, make_blank(width - 1, background_rich_style)]
            elif border_right:
                line = [make_blank(width - 1, background_rich_style), right]
            else:
                line = [make_blank(width, background_rich_style)]
            line = line_post(line)
        else:
            # Content with border and padding (C)
            content_y = y - gutter.top
            if content_y < content_height:
                line = render_content_line(y - gutter.top)
                line = line.adjust_cell_length(content_width, inner.rich_style)
            else:
                line = Strip.blank(content_width, inner.rich_style)

            if (text_opacity := styles.text_opacity) != 1.0:
                line = TextOpacity.process_segments(line, text_opacity, ansi_theme)
            if pad_left or pad_right:
                line = line_post(line_pad(line, pad_left, pad_right, inner.rich_style))
            else:
                line = line_post(line)

            if border_left or border_right:
                # Add left / right border
                left_style = Style(
                    foreground=base_background
                    + border_left_color.multiply_alpha(opacity)
                )
                left = get_box(border_left, inner, outer, left_style)[1][0]
                right_style = Style(
                    foreground=base_background
                    + border_right_color.multiply_alpha(opacity)
                )
                right = get_box(border_right, inner, outer, right_style)[1][2]

                if border_left and border_right:
                    line = [left, *line, right]
                elif border_left:
                    line = [left, *line]
                else:
                    line = [*line, right]

        # Draw any outline
        if (outline_top and y == 0) or (outline_bottom and y == height - 1):
            # Top or bottom outlines
            outline_color = outline_top_color if y == 0 else outline_bottom_color
            box_segments = get_box(
                outline_top if y == 0 else outline_bottom,
                inner,
                outer,
                Style(foreground=base_background + outline_color),
            )
            line = render_row(
                box_segments[0 if y == 0 else 2],
                width,
                outline_left != "",
                outline_right != "",
                (),
            )

        elif outline_left or outline_right:
            # Lines in side outline
            left_style = Style(foreground=(base_background + outline_left_color))
            left = get_box(outline_left, inner, outer, left_style)[1][0]
            right_style = Style(foreground=(base_background + outline_right_color))
            right = get_box(outline_right, inner, outer, right_style)[1][2]
            line = line_trim(list(line), outline_left != "", outline_right != "")
            if outline_left and outline_right:
                line = [left, *line, right]
            elif outline_left:
                line = [left, *line]
            else:
                line = [*line, right]
        strip = Strip(post(line), width)
        if cache_simple_strip:
            self._simple_strip = strip
        return strip
