from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol

from openpyxl.worksheet.worksheet import Worksheet

from .theme import Theme


@dataclass(frozen=True)
class Anchor:
    row: int
    col: int


@dataclass(frozen=True)
class Box:
    r1: int
    c1: int
    r2: int
    c2: int

    def __post_init__(self) -> None:
        if self.r1 <= 0 or self.c1 <= 0:
            raise ValueError("Box must start on positive row/column indices")
        if self.r2 < self.r1 or self.c2 < self.c1:
            raise ValueError("Box bounds are invalid")

    @property
    def width(self) -> int:
        return self.c2 - self.c1 + 1

    @property
    def height(self) -> int:
        return self.r2 - self.r1 + 1

    def intersects(self, other: "Box") -> bool:
        no_overlap = self.r2 < other.r1 or self.r1 > other.r2 or self.c2 < other.c1 or self.c1 > other.c2
        return not no_overlap


class RenderableComponent(Protocol):
    def measure(
        self,
        anchor: Anchor,
        span_rows: int | None = None,
        span_cols: int | None = None,
    ) -> Box:
        ...

    def draw(self, ws: Worksheet, box: Box, theme: Theme) -> None:
        ...


class SheetCanvas:
    def __init__(
        self,
        ws: Worksheet,
        theme: Theme,
        detect_collisions: bool = False,
    ) -> None:
        self.ws = ws
        self.theme = theme
        self.detect_collisions = detect_collisions
        self._occupied: list[Box] = []

        # Sober default line-graph style per requirements.
        self.ws.sheet_view.showGridLines = False

    def put(
        self,
        component: RenderableComponent,
        row: int,
        col: int,
        span_rows: int | None = None,
        span_cols: int | None = None,
    ) -> Box:
        anchor = Anchor(row=row, col=col)
        box = component.measure(anchor=anchor, span_rows=span_rows, span_cols=span_cols)

        if self.detect_collisions:
            for existing in self._occupied:
                if box.intersects(existing):
                    raise ValueError(f"Component collision detected: new={box} existing={existing}")

        component.draw(self.ws, box, self.theme)
        self._occupied.append(box)
        return box

    @property
    def occupied(self) -> tuple[Box, ...]:
        return tuple(self._occupied)
