"""
Canvas - 2D surface for rendering.

The Canvas represents a full rendered surface that can be larger than the display.
The Camera then defines the visible viewport into this canvas.
"""

from dataclasses import dataclass


@dataclass
class CanvasRegion:
    """A rectangular region on the canvas."""

    x: int
    y: int
    width: int
    height: int

    def is_valid(self) -> bool:
        """Check if region has positive dimensions."""
        return self.width > 0 and self.height > 0

    def rows(self) -> set[int]:
        """Return set of row indices in this region."""
        return set(range(self.y, self.y + self.height))


class Canvas:
    """2D canvas for rendering content.

    The canvas is a 2D grid of cells that can hold text content.
    It can be larger than the visible viewport (display).

    Attributes:
        width: Total width in characters
        height: Total height in characters
    """

    def __init__(self, width: int = 80, height: int = 24):
        self.width = width
        self.height = height
        self._grid: list[list[str]] = [
            [" " for _ in range(width)] for _ in range(height)
        ]
        self._dirty_regions: list[CanvasRegion] = []  # Track dirty regions

    def clear(self) -> None:
        """Clear the entire canvas."""
        self._grid = [[" " for _ in range(self.width)] for _ in range(self.height)]
        self._dirty_regions = [CanvasRegion(0, 0, self.width, self.height)]

    def mark_dirty(self, x: int, y: int, width: int, height: int) -> None:
        """Mark a region as dirty (caller declares what they changed)."""
        self._dirty_regions.append(CanvasRegion(x, y, width, height))

    def get_dirty_regions(self) -> list[CanvasRegion]:
        """Get all dirty regions and clear the set."""
        regions = self._dirty_regions
        self._dirty_regions = []
        return regions

    def get_dirty_rows(self) -> set[int]:
        """Get union of all dirty rows."""
        rows: set[int] = set()
        for region in self._dirty_regions:
            rows.update(region.rows())
        return rows

    def is_dirty(self) -> bool:
        """Check if any region is dirty."""
        return len(self._dirty_regions) > 0

    def get_region(self, x: int, y: int, width: int, height: int) -> list[list[str]]:
        """Get a rectangular region from the canvas.

        Args:
            x: Left position
            y: Top position
            width: Region width
            height: Region height

        Returns:
            2D list of characters (height rows, width columns)
        """
        region: list[list[str]] = []
        for py in range(y, y + height):
            row: list[str] = []
            for px in range(x, x + width):
                if 0 <= py < self.height and 0 <= px < self.width:
                    row.append(self._grid[py][px])
                else:
                    row.append(" ")
            region.append(row)
        return region

    def get_region_flat(self, x: int, y: int, width: int, height: int) -> list[str]:
        """Get a rectangular region as flat list of lines.

        Args:
            x: Left position
            y: Top position
            width: Region width
            height: Region height

        Returns:
            List of strings (one per row)
        """
        region = self.get_region(x, y, width, height)
        return ["".join(row) for row in region]

    def put_region(self, x: int, y: int, content: list[list[str]]) -> None:
        """Put content into a rectangular region on the canvas.

        Args:
            x: Left position
            y: Top position
            content: 2D list of characters to place
        """
        height = len(content) if content else 0
        width = len(content[0]) if height > 0 else 0

        for py, row in enumerate(content):
            for px, char in enumerate(row):
                canvas_x = x + px
                canvas_y = y + py
                if 0 <= canvas_y < self.height and 0 <= canvas_x < self.width:
                    self._grid[canvas_y][canvas_x] = char

        if width > 0 and height > 0:
            self.mark_dirty(x, y, width, height)

    def put_text(self, x: int, y: int, text: str) -> None:
        """Put a single line of text at position.

        Args:
            x: Left position
            y: Row position
            text: Text to place
        """
        text_len = len(text)
        for i, char in enumerate(text):
            canvas_x = x + i
            if 0 <= canvas_x < self.width and 0 <= y < self.height:
                self._grid[y][canvas_x] = char

        if text_len > 0:
            self.mark_dirty(x, y, text_len, 1)

    def fill(self, x: int, y: int, width: int, height: int, char: str = " ") -> None:
        """Fill a rectangular region with a character.

        Args:
            x: Left position
            y: Top position
            width: Region width
            height: Region height
            char: Character to fill with
        """
        for py in range(y, y + height):
            for px in range(x, x + width):
                if 0 <= py < self.height and 0 <= px < self.width:
                    self._grid[py][px] = char

        if width > 0 and height > 0:
            self.mark_dirty(x, y, width, height)

    def resize(self, width: int, height: int) -> None:
        """Resize the canvas.

        Args:
            width: New width
            height: New height
        """
        if width == self.width and height == self.height:
            return

        new_grid: list[list[str]] = [[" " for _ in range(width)] for _ in range(height)]

        for py in range(min(self.height, height)):
            for px in range(min(self.width, width)):
                new_grid[py][px] = self._grid[py][px]

        self.width = width
        self.height = height
        self._grid = new_grid
