"""CharGrid — 2D character buffer with per-cell style annotations.

The CharGrid is the intermediate representation between layout and emission.
Both the ASCII and Micron emitters read from the same grid.
"""

from __future__ import annotations

from dataclasses import dataclass, field

from uframe.chars import BOX_CHARS
from uframe.ir import BorderWeight


@dataclass
class CellStyle:
    """Per-cell visual style for Micron emission."""
    fg: str | None = None       # 3-digit hex color
    bg: str | None = None
    bold: bool = False
    italic: bool = False
    underline: bool = False

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, CellStyle):
            return NotImplemented
        return (self.fg == other.fg and self.bg == other.bg
                and self.bold == other.bold and self.italic == other.italic
                and self.underline == other.underline)

    def __hash__(self) -> int:
        return hash((self.fg, self.bg, self.bold, self.italic, self.underline))


@dataclass
class Cell:
    """A single cell in the CharGrid."""
    char: str = " "
    style: CellStyle = field(default_factory=CellStyle)
    is_border: bool = False      # True for box-drawing characters (for merge pass)
    border_weight: BorderWeight | None = None
    border_id: int = 0           # Identifies which box this border belongs to
    link: str | None = None      # Micron link destination


class CharGrid:
    """2D buffer of cells. Origin (0,0) is top-left."""

    __slots__ = ("width", "height", "cells", "_border_counter")

    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self._border_counter = 0
        self.cells: list[list[Cell]] = [
            [Cell() for _ in range(width)]
            for _ in range(height)
        ]

    def in_bounds(self, x: int, y: int) -> bool:
        return 0 <= x < self.width and 0 <= y < self.height

    def put(self, x: int, y: int, char: str,
            style: CellStyle | None = None,
            is_border: bool = False,
            border_weight: BorderWeight | None = None,
            border_id: int = 0,
            link: str | None = None) -> None:
        """Write a single character to the grid."""
        if not self.in_bounds(x, y):
            return
        cell = self.cells[y][x]
        cell.char = char
        if style is not None:
            cell.style = style
        cell.is_border = is_border
        cell.border_weight = border_weight
        if border_id:
            cell.border_id = border_id
        if link is not None:
            cell.link = link

    def put_text(self, x: int, y: int, text: str,
                 style: CellStyle | None = None,
                 link: str | None = None) -> int:
        """Write a string horizontally starting at (x, y).

        Returns the number of characters actually written.
        """
        written = 0
        for i, ch in enumerate(text):
            px = x + i
            if not self.in_bounds(px, y):
                break
            self.put(px, y, ch, style=style, link=link)
            written += 1
        return written

    def fill_rect(self, x: int, y: int, w: int, h: int, char: str = " ",
                  style: CellStyle | None = None) -> None:
        """Fill a rectangular region with a character."""
        for row in range(y, y + h):
            for col in range(x, x + w):
                self.put(col, row, char, style=style)

    def draw_border(self, x: int, y: int, w: int, h: int,
                    weight: BorderWeight = BorderWeight.LIGHT,
                    title: str = "",
                    title_style: CellStyle | None = None,
                    border_chars: dict[str, str] | None = None,
                    title_caps: tuple[str, str] | None = None) -> None:
        """Draw a box border. Interior is not cleared.

        Args:
            x, y: top-left corner
            w, h: outer dimensions (including border)
            weight: border style
            title: optional title inset in top border
            title_style: style for the title text
        """
        if w < 2 or h < 2:
            return

        self._border_counter += 1
        bid = self._border_counter

        ch = border_chars or BOX_CHARS[weight]
        border_style = CellStyle()

        # Corners
        self.put(x, y, ch["tl"], border_style, is_border=True, border_weight=weight, border_id=bid)
        self.put(x + w - 1, y, ch["tr"], border_style, is_border=True, border_weight=weight, border_id=bid)
        self.put(x, y + h - 1, ch["bl"], border_style, is_border=True, border_weight=weight, border_id=bid)
        self.put(x + w - 1, y + h - 1, ch["br"], border_style, is_border=True, border_weight=weight, border_id=bid)

        # Top and bottom edges
        for col in range(x + 1, x + w - 1):
            self.put(col, y, ch["h"], border_style, is_border=True, border_weight=weight, border_id=bid)
            self.put(col, y + h - 1, ch["h"], border_style, is_border=True, border_weight=weight, border_id=bid)

        # Left and right edges
        for row in range(y + 1, y + h - 1):
            self.put(x, row, ch["v"], border_style, is_border=True, border_weight=weight, border_id=bid)
            self.put(x + w - 1, row, ch["v"], border_style, is_border=True, border_weight=weight, border_id=bid)

        # Title in top border
        if title and w > 4:
            lc = title_caps[0] if title_caps else " "
            rc = title_caps[1] if title_caps else " "
            title_text = f"{lc}{title}{rc}"
            max_title = w - 4
            if len(title_text) > max_title:
                title_text = title_text[:max_title]

            start_x = x + 1
            ts = title_style or CellStyle(bold=True)
            self.put_text(start_x, y, title_text, style=ts)

    def grow_height(self, new_height: int) -> None:
        """Expand the grid vertically if needed."""
        if new_height <= self.height:
            return
        for _ in range(new_height - self.height):
            self.cells.append([Cell() for _ in range(self.width)])
        self.height = new_height

    def get_line(self, row: int) -> str:
        """Get a single row as a plain string (chars only)."""
        if 0 <= row < self.height:
            return "".join(cell.char for cell in self.cells[row])
        return ""

    def to_text(self) -> str:
        """Emit the entire grid as plain text (ASCII mode)."""
        lines = []
        for row in range(self.height):
            line = self.get_line(row).rstrip()
            lines.append(line)
        # Strip trailing blank lines
        while lines and not lines[-1]:
            lines.pop()
        return "\n".join(lines)
