"""Pure source-derived list candidates; no game access or input authorization.

Parent rects, category/top-info bottoms and scroll offsets are physical client
pixels. Only MMML constants are multiplied by CanvasContext.scale, once. A
positive scroll offset moves content left/up. Callers supply current list order.
"""
from __future__ import annotations

from dataclasses import dataclass
import math
from types import MappingProxyType
from typing import Mapping

from .ui_layout import CanvasContext, LayoutError, Rect


@dataclass(frozen=True)
class ListCardLayout:
    rect: Rect
    visible_rect: Rect | None
    parent_rect: Rect
    columns: int
    filler_count: int
    valid: bool
    metadata: Mapping[str, object]


def _number(value, name):
    if type(value) not in (int, float) or not math.isfinite(value):
        raise LayoutError(name+" must be a finite physical-client number")
    return float(value)


def _inputs(context, index, count, is_landscape, safe_area, scroll_x, scroll_y):
    if not isinstance(context, CanvasContext) or type(is_landscape) is not bool:
        raise LayoutError("explicit canvas and orientation required")
    if (context.orientation == "landscape") != is_landscape:
        raise LayoutError("list orientation disagrees with canvas")
    if type(index) is not int or type(count) is not int or not 0 <= index < count:
        raise LayoutError("index must identify an item in the current ordered list")
    safe = context.safe_rect if safe_area is None else safe_area
    if not isinstance(safe, Rect) or not context.root_rect.contains(safe):
        raise LayoutError("safe area must be inside the physical client")
    return safe, _number(scroll_x, "scroll_x"), _number(scroll_y, "scroll_y")


def _columns(width, gap, item_width):
    result = math.floor((width + gap) / (item_width + gap))
    if result < 1:
        raise LayoutError("source list has no complete column; no minimum-one fallback")
    return result


def _result(safe, rect, parent, columns, fillers, metadata):
    visible = rect.intersection(parent)
    if visible is not None:
        visible = visible.intersection(safe)
    x, y = rect.center
    valid = visible is not None and visible.left <= x < visible.right and visible.top <= y < visible.bottom
    metadata = dict(metadata, runtime_click_approved=False,
                    valid_meaning="original center is visible; not scene/state/input authority")
    return ListCardLayout(rect, visible, parent, columns, fillers, valid, MappingProxyType(metadata))


def facility_card(context: CanvasContext, index: int, count: int, *, is_landscape: bool,
                  scroll_x=0, scroll_y=0, parent_rect: Rect | None = None,
                  safe_area: Rect | None = None) -> ListCardLayout:
    """MMML905/899/900: current facilities order, 336x490, padded last row.

    The default parent uses the source grid's candidate fill/span semantics.
    A supplied resolved parent overrides its origin/clip, not source widthSize.
    """
    safe, sx, sy = _inputs(context, index, count, is_landscape, safe_area, scroll_x, scroll_y)
    s = context.scale
    width = safe.width - 192*s if is_landscape else context.root_rect.width - 40*s
    columns = _columns(width, 8*s, 336*s)
    if parent_rect is None:
        parent_rect = Rect(safe.left + (160*s if is_landscape else 0), safe.top + 96*s,
                           safe.width - (192*s if is_landscape else 32*s),
                           safe.height - (196*s if is_landscape else 228*s))
        assumptions = ("source grid fills SafeArea; end line of grid span is exclusive",)
    else:
        assumptions = ()
    if not isinstance(parent_rect, Rect):
        raise LayoutError("resolved facility parent must be a Rect")
    row, column = divmod(index, columns)
    line_width = (336*columns + 8*(columns-1))*s
    rect = Rect(parent_rect.left + (width-line_width)/2 + column*344*s - sx,
                parent_rect.top - 16*s + row*490*s - sy, 336*s, 490*s)
    return _result(safe, rect, parent_rect, columns, (-count) % columns,
                   dict(family="facility-list", source_functions=(905, 899, 900, 14, 225),
                        candidate_assumptions=assumptions, scroll_units="physical-client-pixels"))


def recipe_card(context: CanvasContext, index: int, count: int, *, category_bottom,
                is_landscape: bool, scroll_x=0, scroll_y=0, subtract_padding=True,
                topinfo_bottom=None, parent_rect: Rect | None = None,
                safe_area: Rect | None = None) -> ListCardLayout:
    """MMML911/906: 164x232 cards, gap8/12; explicit -682/-642 width candidate.

    subtract_padding is a data-driven trial choice (default True), not a recovered
    PC/Mobile branch. Portrait uses topinfo_bottom+28, landscape category_bottom+24.
    The helper controls invisible fillers independently from actual flow columns.
    """
    safe, sx, sy = _inputs(context, index, count, is_landscape, safe_area, scroll_x, scroll_y)
    if type(subtract_padding) is not bool:
        raise LayoutError("subtract_padding must be an explicit boolean candidate")
    s = context.scale
    if parent_rect is None:
        if is_landscape:
            top = _number(category_bottom, "category_bottom") + 24*s
            parent_rect = Rect(safe.left + 618*s, top, safe.width - 650*s, safe.bottom - top)
            assumptions = ("source parent fills SafeArea; scroll left/right resolve against that parent",)
        else:
            top = _number(topinfo_bottom, "topinfo_bottom") + 28*s
            parent_rect = Rect(safe.center[0] - 360*s, top, 720*s, safe.bottom - top)
            assumptions = ("portrait width720 scroll uses centered default horizontal placement",)
    else:
        assumptions = ()
    if not isinstance(parent_rect, Rect):
        raise LayoutError("resolved recipe parent must be a Rect")
    columns = _columns(parent_rect.width - (40*s if subtract_padding else 0), 8*s, 164*s)
    helper = _columns(safe.width - 650*s, 8*s, 164*s) if is_landscape else 4
    row, column = divmod(index, columns)
    rect = Rect(parent_rect.left + 20*s + column*172*s - sx,
                parent_rect.top + row*244*s - sy, 164*s, 232*s)
    return _result(safe, rect, parent_rect, columns, (-count) % helper,
                   dict(family="recipe-list", source_functions=(911, 906, 14), helper_columns=helper,
                        padding_candidate="subtract40 (-682)" if subtract_padding else "retain-width (-642)",
                        subtract_padding=subtract_padding, scroll_units="physical-client-pixels",
                        candidate_assumptions=assumptions + ("unset flow width fills parent; default flow alignment is left",
                            "padding subtracts40 from wrap width" if subtract_padding else "padding does not reduce wrap width")))
