from __future__ import annotations

#-----------------------------------------------------------------------------------------------------------------------
# OHD layout: `Assets/Shared/{resolution}/layout.json` (canonical), optional per-game override,
# and `ohd_layouts/{resolution}.default.json` fallback.
#
# digit_strip.cells[] — 13 elements typical:
#   digits 0-9 (kind=digit, full height) + dollar, comma, decimal (kind=symbol, shorter)
#-----------------------------------------------------------------------------------------------------------------------
import copy
import json
import os
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple

RESOLUTION_1920 = "1920x1080"
RESOLUTION_3840 = "3840x1080"
RESOLUTION_STRIP = "1920x360"


def is_strip_resolution(resolution: str) -> bool:
    """True for native Mini pack or overscan variants (e.g. 1920x360-96)."""
    r = (resolution or "").strip()
    return r == RESOLUTION_STRIP or r.startswith(f"{RESOLUTION_STRIP}-")


def blocking_asset_applies(resolution: str) -> bool:
    """3840x1080 / 1920x360(+overscan) OHD sets use BG + shared FR only — no per-game BL."""
    return resolution != RESOLUTION_3840 and not is_strip_resolution(resolution)

# Comma between 1000s (power 5) and 100s (power 4) → $888,888.88 style grouping.
COMMA_LEFT_POWER = 5
COMMA_RIGHT_POWER = 4


def default_assets_root() -> str:
    base = os.path.dirname(os.path.abspath(__file__))
    parent = os.path.dirname(base)
    candidates = [
        os.path.join(base, "Assets"),
        os.path.join(base, "assets"),
        os.path.join(parent, "Assets"),
        os.path.join(parent, "assets"),
    ]
    for path in candidates:
        if os.path.isdir(path):
            return path
    return candidates[0]


def format_game_index(game_id: int) -> str:
    """Canonical on-disk game folder name (01..16)."""
    return f"{int(game_id):02d}"


def default_shared_resolution_dir(resolution: str) -> str:
    """Shared digit glyphs + layout for all progressive games at this resolution."""
    return os.path.join(default_assets_root(), "Shared", resolution)


def default_shared_layout_path(resolution: str) -> str:
    return os.path.join(default_shared_resolution_dir(resolution), "layout.json")


def per_game_layout_override_path(
    game_id: int, resolution: str, assets_root: Optional[str] = None
) -> str:
    """Optional per-game layout tweaks (e.g. frame_anchor); rarely needed."""
    root = assets_root or default_assets_root()
    return os.path.join(root, format_game_index(game_id), resolution, "layout.override.json")


def save_shared_layout(layout: OhdLayout, resolution: str) -> str:
    """Write canonical `layout.json` under `Assets/Shared/{resolution}/`."""
    path = default_shared_layout_path(resolution)
    save_layout(path, layout, resolution)
    return path


@dataclass
class StripCell:
    kind: str  # "digit" | "symbol"
    x: int
    y: int
    w: int
    h: int
    digit: Optional[int] = None  # 0-9 when kind=digit
    name: Optional[str] = None  # dollar, comma, decimal when kind=symbol
    skip: bool = False  # true = duplicate/unused strip row (e.g. second "0")
    duplicate: bool = False

    def label(self) -> str:
        if self.skip:
            if self.kind == "digit" and self.digit is not None:
                return f"{self.digit}*"
            return (self.name or "?") + "*"
        if self.kind == "digit" and self.digit is not None:
            return str(self.digit)
        return self.name or "?"

    def to_json(self) -> Dict[str, Any]:
        row: Dict[str, Any] = {"kind": self.kind, "x": self.x, "y": self.y, "w": self.w, "h": self.h}
        if self.kind == "digit" and self.digit is not None:
            row["digit"] = self.digit
        if self.name:
            row["name"] = self.name
        if self.skip:
            row["skip"] = True
        if self.duplicate:
            row["duplicate"] = True
        return row


@dataclass
class DigitSlot:
    name: str
    power: int
    x: int
    y: int
    w: int
    h: int

    def to_json(self) -> Dict[str, Any]:
        return {"name": self.name, "power": self.power, "x": self.x, "y": self.y, "w": self.w, "h": self.h}


@dataclass
class SymbolSlot:
    """Screen placement for $ , . (comma may repeat with index 0, 1, …)."""
    name: str
    x: int
    y: int
    w: int
    h: int
    index: Optional[int] = None

    def to_json(self) -> Dict[str, Any]:
        row: Dict[str, Any] = {"name": self.name, "x": self.x, "y": self.y, "w": self.w, "h": self.h}
        if self.index is not None:
            row["index"] = self.index
        return row

    def label(self) -> str:
        if self.index is not None:
            return f"{self.name}:{self.index}"
        return self.name


@dataclass
class OhdLayout:
    screen_size: Tuple[int, int]
    frame_anchor: Tuple[int, int]
    digit_slots: List[DigitSlot] = field(default_factory=list)
    symbol_slots: List[SymbolSlot] = field(default_factory=list)
    strip_cells: List[StripCell] = field(default_factory=list)
    digit_order: str = "0_at_top"
    display_scale: float = 1.0
    # Optional extra horizontal spread for digit/symbol centers (defaults to display_scale).
    horizontal_scale: Optional[float] = None
    column_spacing_scale: float = 1.0
    digit_baseline_y: Optional[int] = None
    digit_top_y: Optional[int] = None
    digit_cell_w: int = 90
    digit_cell_h: int = 120
    source_path: str = ""
    # False: JSON x,y stay fixed; display_scale only enlarges glyphs (tuned 1920 layouts).
    # True: F7/F8 resize spreads slot positions from the row center (pick-coords tuning).
    scale_slot_positions: bool = False
    # Screen coords from previewJacksORBetter-FINAL.png — skip frame re-centering on load.
    use_template_screen_coords: bool = False
    # Unscaled slot coords from JSON.
    base_digit_slots: List[DigitSlot] = field(default_factory=list, repr=False)
    base_symbol_slots: List[SymbolSlot] = field(default_factory=list, repr=False)
    # 1920 aligned cells: anchor_right / anchor_bottom (lower-right of pennies column).
    aligned_cells: Optional[Dict[str, Any]] = None


def horizontal_spread_scale(layout: OhdLayout) -> float:
    """Scale factor for spreading column centers from the row midpoint."""
    if layout.horizontal_scale is not None:
        return layout.horizontal_scale
    return layout.display_scale * layout.column_spacing_scale


def _layouts_dir() -> str:
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), "ohd_layouts")


def _parse_symbol_slot(c: Dict[str, Any]) -> SymbolSlot:
    return SymbolSlot(
        name=str(c["name"]),
        x=int(c["x"]),
        y=int(c["y"]),
        w=int(c["w"]),
        h=int(c["h"]),
        index=int(c["index"]) if c.get("index") is not None else None,
    )


def _parse_strip_cell(c: Dict[str, Any]) -> StripCell:
    if "kind" in c:
        kind = str(c["kind"])
        return StripCell(
            kind=kind,
            x=int(c["x"]),
            y=int(c["y"]),
            w=int(c["w"]),
            h=int(c["h"]),
            digit=int(c["digit"]) if c.get("digit") is not None else None,
            name=str(c["name"]) if c.get("name") else None,
            skip=bool(c.get("skip", False)),
            duplicate=bool(c.get("duplicate", False)),
        )
    # Legacy: digit-only field
    return StripCell(
        kind="digit",
        x=int(c["x"]),
        y=int(c["y"]),
        w=int(c["w"]),
        h=int(c["h"]),
        digit=int(c["digit"]),
    )


def default_strip_cells(strip_w: int, strip_h: int) -> List[StripCell]:
    """13 rows: digits 0-9 then dollar, comma, decimal (heights are estimates — drag to fix)."""
    cells: List[StripCell] = []
    digit_h = max(200, (strip_h - 420) // 10)
    for d in range(10):
        cells.append(StripCell("digit", 0, d * digit_h, strip_w, digit_h, digit=d))
    y = 10 * digit_h
    sym_h = max(80, (strip_h - y) // 3)
    for name in ("dollar", "comma", "decimal"):
        cells.append(StripCell("symbol", 0, y, strip_w, sym_h, name=name))
        y += sym_h
    return cells


def load_layout(
    resolution: str,
    game_id: int,
    frame_size: Optional[Tuple[int, int]] = None,
    assets_root: Optional[str] = None,
    strip_size: Optional[Tuple[int, int]] = None,
) -> OhdLayout:
    root = assets_root or default_assets_root()
    data = None
    used_path = ""
    for path in _layout_paths(resolution, game_id, root):
        if os.path.isfile(path):
            with open(path, encoding="utf-8") as f:
                data = json.load(f)
            used_path = path
            break
    if data is None:
        layout = _fallback_layout(resolution, frame_size or (1872, 526))
        if strip_size:
            layout.strip_cells = default_strip_cells(strip_size[0], strip_size[1])
        return layout

    screen = tuple(data.get("screen", [1920, 1080]))
    frame_anchor = tuple(data.get("frame_anchor", [0, 0]))
    strip = data.get("digit_strip", {})
    digit_order = strip.get("digit_order", "0_at_top")
    raw_cells = strip.get("cells", [])
    if raw_cells:
        strip_cells = [_parse_strip_cell(c) for c in raw_cells]
    elif strip_size:
        strip_cells = default_strip_cells(strip_size[0], strip_size[1])
    else:
        strip_cells = []

    slots = [
        DigitSlot(
            str(s.get("name", f"col{s['power']}")),
            int(s["power"]),
            int(s["x"]),
            int(s["y"]),
            int(s["w"]),
            int(s["h"]),
        )
        for s in data.get("digit_slots", [])
    ]
    slots.sort(key=lambda s: s.power)
    symbol_slots = [_parse_symbol_slot(s) for s in data.get("symbol_slots", [])]
    odometer = data.get("odometer", {})
    display_scale = float(data.get("display_scale", odometer.get("scale", 1.0)))
    hs = data.get("horizontal_scale")
    horizontal_scale = float(hs) if hs is not None else None
    scale_slot_positions = bool(data.get("scale_slot_positions", False))
    use_template_screen_coords = bool(data.get("use_template_screen_coords", False))
    column_spacing_scale = float(data.get("column_spacing_scale", 1.0))
    db = data.get("digit_baseline_y")
    digit_baseline_y = int(db) if db is not None else None
    dt = data.get("digit_top_y")
    digit_top_y = int(dt) if dt is not None else None
    aligned_cells = data.get("aligned_cells")
    if aligned_cells is not None and not isinstance(aligned_cells, dict):
        aligned_cells = None
    odometer_cfg = data.get("odometer")
    if odometer_cfg is not None and isinstance(odometer_cfg, dict):
        from odometer_engine import configure_odometer

        configure_odometer(odometer_cfg)
        from odometer_engine import format_odometer_config_summary, format_odometer_lag_map

        print(f"Odometer: {format_odometer_config_summary()}", flush=True)
        print(format_odometer_lag_map(), flush=True)
    cell = data.get("digit_cell", {})
    if cell:
        cell_w, cell_h = int(cell.get("w", 90)), int(cell.get("h", 120))
    elif slots:
        cell_w, cell_h = slots[0].w, slots[0].h
    else:
        cell_w, cell_h = 90, 120
    layout = OhdLayout(
        screen_size=(int(screen[0]), int(screen[1])),
        frame_anchor=(int(frame_anchor[0]), int(frame_anchor[1])),
        digit_slots=copy.deepcopy(slots),
        symbol_slots=copy.deepcopy(symbol_slots),
        strip_cells=strip_cells,
        digit_order=digit_order,
        display_scale=display_scale,
        horizontal_scale=horizontal_scale,
        column_spacing_scale=column_spacing_scale,
        digit_baseline_y=digit_baseline_y,
        digit_top_y=digit_top_y,
        scale_slot_positions=scale_slot_positions,
        use_template_screen_coords=use_template_screen_coords,
        digit_cell_w=cell_w,
        digit_cell_h=cell_h,
        source_path=used_path,
        base_digit_slots=copy.deepcopy(slots),
        base_symbol_slots=copy.deepcopy(symbol_slots),
        aligned_cells=copy.deepcopy(aligned_cells) if aligned_cells else None,
    )
    normalize_base_digit_sizes(layout)
    ensure_single_comma_slot(layout)
    rebuild_display_slots(layout)
    if frame_size and not layout.use_template_screen_coords:
        center_digit_row_in_frame(layout, frame_size[0])
    return layout


def layout_to_json_dict(layout: OhdLayout, resolution: str) -> Dict[str, Any]:
    digit_src = layout.base_digit_slots if layout.base_digit_slots else layout.digit_slots
    symbol_src = layout.base_symbol_slots if layout.base_symbol_slots else layout.symbol_slots
    doc: Dict[str, Any] = {
        "resolution": resolution,
        "screen": list(layout.screen_size),
        "frame_anchor": list(layout.frame_anchor),
        "digit_strip": {
            "orientation": "vertical",
            "digit_order": layout.digit_order,
            "cells": [c.to_json() for c in layout.strip_cells],
        },
        "digit_slots": [s.to_json() for s in digit_src],
        "symbol_slots": [s.to_json() for s in symbol_src],
    }
    if abs(layout.display_scale - 1.0) > 1e-6:
        doc["display_scale"] = round(layout.display_scale, 4)
    if layout.horizontal_scale is not None and abs(layout.horizontal_scale - layout.display_scale) > 1e-6:
        doc["horizontal_scale"] = round(layout.horizontal_scale, 4)
    if abs(layout.column_spacing_scale - 1.0) > 1e-6:
        doc["column_spacing_scale"] = round(layout.column_spacing_scale, 4)
    if layout.digit_baseline_y is not None:
        doc["digit_baseline_y"] = layout.digit_baseline_y
    if layout.digit_top_y is not None:
        doc["digit_top_y"] = layout.digit_top_y
    if layout.scale_slot_positions:
        doc["scale_slot_positions"] = True
    if layout.use_template_screen_coords:
        doc["use_template_screen_coords"] = True
    doc["digit_cell"] = {"w": layout.digit_cell_w, "h": layout.digit_cell_h}
    if layout.aligned_cells:
        doc["aligned_cells"] = copy.deepcopy(layout.aligned_cells)
    return doc


def save_layout(path: str, layout: OhdLayout, resolution: str) -> None:
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(layout_to_json_dict(layout, resolution), f, indent=2)
        f.write("\n")


def _layout_paths(resolution: str, game_id: int, assets_root: str) -> List[str]:
    paths: List[str] = [default_shared_layout_path(resolution)]
    override = os.path.join(
        assets_root, format_game_index(game_id), resolution, "layout.override.json"
    )
    if os.path.isfile(override):
        paths.insert(0, override)
    paths.append(os.path.join(_layouts_dir(), f"{resolution}.default.json"))
    return paths


def _fallback_layout(resolution: str, frame_size: Tuple[int, int]) -> OhdLayout:
    fw, fh = frame_size
    if resolution == RESOLUTION_3840:
        sw, sh = 3840, 1080
        fx, fy = (sw - fw) // 2, sh - fh - 20
    elif is_strip_resolution(resolution):
        sw, sh = 1920, 360
        fx, fy = 648, 0
    else:
        sw, sh = 1920, 1080
        fx, fy = (sw - fw) // 2, sh - fh - 30
    return OhdLayout(
        screen_size=(sw, sh),
        frame_anchor=(fx, fy),
        digit_slots=[],
        strip_cells=[],
        source_path="(computed fallback)",
    )


def digit_row_center(layout: OhdLayout) -> Tuple[float, float]:
    """Center of digit columns only (scale anchor — symbols follow digit gaps)."""
    slots = layout.base_digit_slots or layout.digit_slots
    if not slots:
        return 960.0, 780.0
    centers_x = [s.x + s.w / 2.0 for s in slots]
    centers_y = [s.y + s.h / 2.0 for s in slots]
    return sum(centers_x) / len(centers_x), sum(centers_y) / len(centers_y)


def display_row_center(layout: OhdLayout) -> Tuple[float, float]:
    """Backward-compatible alias for digit row center."""
    return digit_row_center(layout)


def _scale_rect_from_anchor(
    x: int, y: int, w: int, h: int, scale: float, anchor: Tuple[float, float]
) -> Tuple[int, int, int, int]:
    if abs(scale - 1.0) < 1e-6:
        return x, y, w, h
    cx, cy = anchor
    return (
        int(round(cx + (x - cx) * scale)),
        int(round(cy + (y - cy) * scale)),
        max(1, int(round(w * scale))),
        max(1, int(round(h * scale))),
    )


def apply_display_scale_to_slots(
    slots: List[Any],
    scale: float,
    anchor: Tuple[float, float],
) -> None:
    if abs(scale - 1.0) < 1e-6:
        return
    for slot in slots:
        slot.x, slot.y, slot.w, slot.h = _scale_rect_from_anchor(
            slot.x, slot.y, slot.w, slot.h, scale, anchor
        )


# Inset from frame right edge when anchoring the digit row (screen pixels at 1.0× layout).
FRAME_ROW_PAD_RIGHT = 100


def center_digit_row_in_frame(layout: OhdLayout, frame_width: int) -> None:
    """Shift base digit/symbol slots so the row is centered in the frame."""
    slots = layout.base_digit_slots
    if not slots or frame_width <= 0:
        return
    left = min(s.x for s in slots)
    right = max(s.x + s.w for s in slots)
    row_cx = (left + right) / 2.0
    frame_left = layout.frame_anchor[0]
    frame_right = frame_left + frame_width
    desired_cx = (frame_left + frame_right) / 2.0
    shift = int(round(desired_cx - row_cx))
    if shift == 0:
        return
    for s in slots:
        s.x += shift
    for s in layout.base_symbol_slots:
        s.x += shift
    rebuild_display_slots(layout)


def _spread_column_center(
    bcx: float, row_cx: float, spread: float
) -> float:
    """Scale horizontal distance from row center (column_step at 1.0 × spread)."""
    return row_cx + (bcx - row_cx) * spread


def rebuild_digit_slots_scaled(layout: OhdLayout) -> None:
    """
    Build on-screen digit_slots from base + display_scale.
    Template layouts: fixed FINAL x,y; display_scale only affects glyph drawing.
    Pick-coords tuning: scale_slot_positions spreads slot rects from row center.
    """
    base = layout.base_digit_slots
    if not base:
        layout.digit_slots = []
        return
    cell_w = layout.digit_cell_w
    cell_h = layout.digit_cell_h
    if layout.use_template_screen_coords or not layout.scale_slot_positions:
        layout.digit_slots = [
            DigitSlot(b.name, b.power, b.x, b.y, cell_w, cell_h)
            for b in sorted(base, key=lambda s: s.power)
        ]
        return
    scale = layout.display_scale
    cell_w = max(1, int(round(layout.digit_cell_w * scale)))
    cell_h = max(1, int(round(layout.digit_cell_h * scale)))
    hx = horizontal_spread_scale(layout)
    cx, _ = digit_row_center(layout)
    by_power = {s.power: s for s in base}
    scaled: List[DigitSlot] = []
    for power in sorted(by_power.keys()):
        b = by_power[power]
        bcx = b.x + b.w / 2.0
        scx = cx + (bcx - cx) * hx
        scaled.append(
            DigitSlot(
                b.name,
                power,
                int(round(scx - cell_w / 2.0)),
                int(round(b.y)),
                cell_w,
                cell_h,
            )
        )
    layout.digit_slots = scaled


def rebuild_symbol_slots_scaled(layout: OhdLayout) -> None:
    """Template: fixed symbol slots. Pick-coords: scale symbol slot rects with digits."""
    base_syms = layout.base_symbol_slots
    if not base_syms:
        layout.symbol_slots = []
        return
    if layout.use_template_screen_coords or not layout.scale_slot_positions:
        layout.symbol_slots = copy.deepcopy(base_syms)
        return
    scale = layout.display_scale
    scaled: List[SymbolSlot] = []
    for sym in base_syms:
        sw = max(1, int(round(sym.w * scale)))
        sh = max(1, int(round(sym.h * scale)))
        scaled.append(
            SymbolSlot(sym.name, sym.x, sym.y, sw, sh, index=sym.index)
        )
    layout.symbol_slots = scaled


def sync_screen_rect_to_base(
    layout: OhdLayout, sx: int, sy: int, sw: int, sh: int, base: Any
) -> None:
    """Write on-screen slot rect into base (1.0) layout coordinates."""
    scale = layout.display_scale
    cx, cy = display_row_center(layout)
    if abs(scale - 1.0) < 1e-6:
        base.x, base.y, base.w, base.h = sx, sy, sw, sh
        return
    base.x = int(round(cx + (sx - cx) / scale))
    base.y = int(round(cy + (sy - cy) / scale))
    base.w = max(1, int(round(sw / scale)))
    base.h = max(1, int(round(sh / scale)))


def _gap_center_x(left: DigitSlot, right: DigitSlot, sym_w: int) -> int:
    gap_left = left.x + left.w
    gap_right = right.x
    return gap_left + max(0, (gap_right - gap_left - sym_w) // 2)


def _auto_symbol_xy(
    layout: OhdLayout, name: str, index: Optional[int], sym_w: int
) -> Optional[Tuple[int, int]]:
    """Default screen x,y from scaled digit columns (before user offset)."""
    row = layout.digit_slots[0] if layout.digit_slots else None
    if row is None:
        return None
    y = row.y
    if name == "dollar":
        s7 = slot_for_power(layout, 7)
        if s7 is None:
            return None
        return s7.x - sym_w - 4, y
    if name == "decimal":
        dollars = slot_for_power(layout, 2)
        dimes = slot_for_power(layout, 1)
        if dollars and dimes:
            return _gap_center_x(dollars, dimes, sym_w), y
        return None
    if name == "comma":
        left = slot_for_power(layout, COMMA_LEFT_POWER)
        right = slot_for_power(layout, COMMA_RIGHT_POWER)
        if left and right:
            return _gap_center_x(left, right, sym_w), y
    return None


def _symbol_offsets_at_base(layout: OhdLayout) -> Dict[Tuple[str, Optional[int]], Tuple[int, int]]:
    """Per-symbol delta from auto gap position at display_scale 1.0."""
    tmp = OhdLayout(
        screen_size=layout.screen_size,
        frame_anchor=layout.frame_anchor,
        digit_slots=copy.deepcopy(layout.base_digit_slots),
        symbol_slots=[],
    )
    offsets: Dict[Tuple[str, Optional[int]], Tuple[int, int]] = {}
    for sym in layout.base_symbol_slots:
        auto = _auto_symbol_xy(tmp, sym.name, sym.index, sym.w)
        if auto is None:
            offsets[(sym.name, sym.index)] = (0, 0)
        else:
            offsets[(sym.name, sym.index)] = (sym.x - auto[0], sym.y - auto[1])
    return offsets


def _snap_symbols_to_digits(layout: OhdLayout, offsets: Dict[Tuple[str, Optional[int]], Tuple[int, int]]) -> None:
    scale = layout.display_scale
    for sym in layout.symbol_slots:
        sym.w = max(1, int(round(
            next((b.w for b in layout.base_symbol_slots if b.name == sym.name and b.index == sym.index), sym.w)
            * scale
        )))
        sym.h = max(1, int(round(
            next((b.h for b in layout.base_symbol_slots if b.name == sym.name and b.index == sym.index), sym.h)
            * scale
        )))
        auto = _auto_symbol_xy(layout, sym.name, sym.index, sym.w)
        off = offsets.get((sym.name, sym.index), (0, 0))
        if auto is None:
            continue
        sym.x = auto[0] + int(round(off[0] * scale))
        sym.y = auto[1] + int(round(off[1] * scale))


def ensure_single_comma_slot(layout: OhdLayout) -> None:
    """Keep one comma between 1000s and 100s columns (base coordinates)."""
    if layout.use_template_screen_coords:
        return
    others = [s for s in layout.base_symbol_slots if s.name != "comma"]
    tmp = OhdLayout(
        screen_size=layout.screen_size,
        frame_anchor=layout.frame_anchor,
        digit_slots=copy.deepcopy(layout.base_digit_slots),
        symbol_slots=[],
    )
    gap = _gap_slot(tmp, "comma", COMMA_LEFT_POWER, COMMA_RIGHT_POWER, index=0)
    layout.base_symbol_slots = others + ([gap] if gap else [])


def symbol_slots_for_render(layout: OhdLayout) -> List[SymbolSlot]:
    """At most one comma in the draw list."""
    out: List[SymbolSlot] = []
    comma_used = False
    for slot in layout.symbol_slots:
        if slot.name == "comma":
            if comma_used:
                continue
            comma_used = True
        out.append(slot)
    return out


def normalize_base_digit_sizes(layout: OhdLayout) -> None:
    """All digit columns share one cell size at display_scale 1.0."""
    for slot in layout.base_digit_slots:
        slot.w = layout.digit_cell_w
        slot.h = layout.digit_cell_h


def sync_digit_cell_from_screen(layout: OhdLayout, screen_w: int, screen_h: int) -> None:
    """Update shared digit cell size from on-screen dimensions (inverse display_scale)."""
    scale = max(layout.display_scale, 1e-6)
    layout.digit_cell_w = max(1, int(round(screen_w / scale)))
    layout.digit_cell_h = max(1, int(round(screen_h / scale)))
    normalize_base_digit_sizes(layout)


def rebuild_display_slots(layout: OhdLayout, snap_symbols: bool = True) -> None:
    """Rebuild digit_slots / symbol_slots from base coords and display_scale."""
    if not layout.base_digit_slots:
        layout.base_digit_slots = copy.deepcopy(layout.digit_slots)
    if not layout.base_symbol_slots:
        layout.base_symbol_slots = copy.deepcopy(layout.symbol_slots)
    normalize_base_digit_sizes(layout)
    rebuild_digit_slots_scaled(layout)
    if layout.use_template_screen_coords or not layout.scale_slot_positions:
        rebuild_symbol_slots_scaled(layout)
        return
    scale = layout.display_scale
    if snap_symbols and layout.base_symbol_slots:
        offsets = _symbol_offsets_at_base(layout)
        layout.symbol_slots = copy.deepcopy(layout.base_symbol_slots)
        _snap_symbols_to_digits(layout, offsets)
    else:
        layout.symbol_slots = copy.deepcopy(layout.base_symbol_slots)
        apply_display_scale_to_slots(layout.symbol_slots, scale, digit_row_center(layout))


def set_display_scale(
    layout: OhdLayout,
    scale: float,
    snap_symbols: bool = True,
    *,
    scale_slot_positions: Optional[bool] = None,
) -> None:
    layout.display_scale = max(0.25, min(3.0, float(scale)))
    if scale_slot_positions is not None:
        layout.scale_slot_positions = scale_slot_positions
    rebuild_display_slots(layout, snap_symbols=snap_symbols)


def slot_for_power(layout: OhdLayout, power: int) -> Optional[DigitSlot]:
    for slot in layout.digit_slots:
        if slot.power == power:
            return slot
    return None


def _gap_slot(
    layout: OhdLayout,
    name: str,
    left_power: int,
    right_power: int,
    index: Optional[int],
    default_w: int = 28,
) -> Optional[SymbolSlot]:
    left = slot_for_power(layout, left_power)
    right = slot_for_power(layout, right_power)
    if left is None or right is None:
        return None
    gap_left = left.x + left.w
    gap_right = right.x
    gap_w = max(1, gap_right - gap_left)
    w = min(default_w, gap_w) if gap_w < default_w else default_w
    x = gap_left + max(0, (gap_w - w) // 2)
    return SymbolSlot(name, x, left.y, w, left.h, index=index)


def default_symbol_slots(layout: OhdLayout) -> List[SymbolSlot]:
    """Infer symbol_slots: $ , between 1000s|100s , . only."""
    if not layout.digit_slots:
        return []
    s7 = slot_for_power(layout, 7)
    slots: List[SymbolSlot] = []
    if s7:
        slots.append(SymbolSlot("dollar", max(0, s7.x - 88), s7.y, 82, s7.h))
    gap = _gap_slot(layout, "comma", COMMA_LEFT_POWER, COMMA_RIGHT_POWER, index=0)
    if gap:
        slots.append(gap)
    dec = _gap_slot(layout, "decimal", 2, 1, index=None, default_w=24)
    if dec:
        slots.append(dec)
    return slots


def active_strip_cells(layout: OhdLayout) -> List[StripCell]:
    return [c for c in layout.strip_cells if not c.skip]


def digit_strip_cells(layout: OhdLayout) -> List[StripCell]:
    return [c for c in layout.strip_cells if c.kind == "digit" and not c.skip]


def symbol_strip_cells(layout: OhdLayout) -> List[StripCell]:
    return [c for c in layout.strip_cells if c.kind == "symbol" and not c.skip]


def infer_uniform_digit_size(layout: OhdLayout) -> Tuple[int, int, int]:
    """
    From measured digit cells (non-skip), infer uniform w, h, and median h (digits 1-9).
    Returns (width, height_for_1_to_9, height_digit_0).
    """
    digits = [c for c in digit_strip_cells(layout) if c.digit is not None]
    if not digits:
        return 196, 296, 312
    widths = [c.w for c in digits]
    heights = [c.h for c in digits]
    d0 = next((c.h for c in digits if c.digit == 0), heights[0])
    h19 = [c.h for c in digits if c.digit != 0]
    median_h = sorted(h19)[len(h19) // 2] if h19 else heights[0]
    width = max(set(widths), key=widths.count)
    return width, median_h, d0


def normalize_digit_cell_heights(layout: OhdLayout, uniform_all: bool = False) -> None:
    """Set uniform w/h on active digits; y positions unchanged (keeps your vertical alignment)."""
    w, h19, h0 = infer_uniform_digit_size(layout)
    for c in layout.strip_cells:
        if c.kind == "digit" and not c.skip and c.digit is not None:
            c.w = w
            if uniform_all:
                c.h = h19
            elif c.digit == 0:
                c.h = h0
            else:
                c.h = h19
