"""Snap-to-grid placement math for widgets (see models.Widget) -- pure,
no I/O, no ORM.

The grid is defined relative to the panel's long/short axis, not
landscape/portrait specifically, so it stays valid across
image_pipeline.logical_render_size(orientation)'s genuine width/height
swap for portrait (not just a rotation applied at the very end) --
landscape orientations are GRID_LONG columns x GRID_SHORT rows, portrait
orientations are GRID_SHORT columns x GRID_LONG rows, same cell size
either way. Changing a frame's orientation therefore invalidates any
existing widget layout (an 8x5 arrangement isn't valid on a 5x8 grid) --
callers are expected to reset to one full-panel widget on an orientation
change, not try to remap coordinates.
"""

from __future__ import annotations

GRID_LONG = 8
GRID_SHORT = 5

# Per-widget-type minimum grid footprint (cols, rows) -- enforced both in
# the placement UI and server-side (routers/api_widgets.py). A calendar
# widget crammed into 1x1 would be illegible regardless of size-tier
# scaling (see calendar_render.py); whiteboard needs enough room to be
# worth looking at; photos can go as small as a single cell.
MIN_FOOTPRINT: dict[str, tuple[int, int]] = {
    "photos": (1, 1),
    "calendar": (3, 2),
    "whiteboard": (2, 2),
}

Rect = tuple[int, int, int, int]  # (x, y, w, h)


def grid_dims(orientation: str) -> tuple[int, int]:
    """(cols, rows) for this orientation."""
    if orientation in ("portrait", "portrait_flipped"):
        return GRID_SHORT, GRID_LONG
    return GRID_LONG, GRID_SHORT


def full_panel_rect(orientation: str) -> Rect:
    """The single full-panel widget rect for this orientation -- what a
    frame gets reset to whenever its layout can't carry over (initial
    migration backfill, an orientation change)."""
    cols, rows = grid_dims(orientation)
    return (0, 0, cols, rows)


def in_bounds(orientation: str, rect: Rect) -> bool:
    cols, rows = grid_dims(orientation)
    x, y, w, h = rect
    return x >= 0 and y >= 0 and w > 0 and h > 0 and x + w <= cols and y + h <= rows


def meets_minimum(widget_type: str, rect: Rect) -> bool:
    min_w, min_h = MIN_FOOTPRINT.get(widget_type, (1, 1))
    _, _, w, h = rect
    return w >= min_w and h >= min_h


def overlaps(a: Rect, b: Rect) -> bool:
    ax, ay, aw, ah = a
    bx, by, bw, bh = b
    return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah


def cell_to_pixels(orientation: str, panel_w: int, panel_h: int, rect: Rect) -> tuple[int, int, int, int]:
    """Grid rect -> pixel rect in logical (pre-rotation) canvas space --
    against image_pipeline.logical_render_size(orientation)'s own
    (panel_w, panel_h), the same space every renderer already composes
    in before the final orientation transpose."""
    cols, rows = grid_dims(orientation)
    cell_w = panel_w / cols
    cell_h = panel_h / rows
    x, y, w, h = rect
    px, py = round(x * cell_w), round(y * cell_h)
    # Snap the far edge to the next cell boundary rather than compounding
    # per-cell rounding error across w/h -- keeps adjacent widgets'
    # shared edge pixel-exact instead of leaving a stray gap/overlap.
    px2, py2 = round((x + w) * cell_w), round((y + h) * cell_h)
    return (px, py, px2 - px, py2 - py)
