from __future__ import annotations

import os
import re
from typing import Optional

import pygame

from ..core.engine import OdometerConfig
from ..render.widget import OdometerWidget
from ..theme.aligned import OdometerTheme, load_aligned_cell_library
from ..theme.layout import OdometerLayout, load_layout_data, load_layout_file

try:
    from ... import ohd_layout as legacy_layout
except ImportError:  # pragma: no cover - package imported as top-level `reusable_odometer`
    try:
        import ohd_layout as legacy_layout  # type: ignore[no-redef]
    except ImportError:
        try:
            from DyzineFrame import ohd_layout as legacy_layout  # type: ignore[no-redef]
        except ImportError:  # pragma: no cover - allows later extraction into its own repo
            legacy_layout = None


def default_assets_root() -> str:
    if legacy_layout is None:
        raise RuntimeError("Current-project asset discovery is unavailable outside this repo")
    return legacy_layout.default_assets_root()


def format_game_index(game_id: int) -> str:
    if legacy_layout is None:
        return f"{int(game_id):02d}"
    return legacy_layout.format_game_index(game_id)


def default_shared_resolution_dir(
    resolution: str, assets_root: Optional[str] = None
) -> str:
    root = assets_root or default_assets_root()
    return os.path.join(root, "Shared", resolution)


def current_layout_path(
    resolution: str, game_id: int, assets_root: Optional[str] = None
) -> Optional[str]:
    root = assets_root or default_assets_root()
    game_token = format_game_index(game_id)
    candidates = [
        os.path.join(root, game_token, resolution, "layout.override.json"),
        os.path.join(root, "Shared", resolution, "layout.json"),
    ]
    if legacy_layout is not None:
        candidates.append(
            os.path.join(
                os.path.dirname(os.path.abspath(legacy_layout.__file__)),
                "ohd_layouts",
                f"{resolution}.default.json",
            )
        )
    for path in candidates:
        if path and os.path.isfile(path):
            return path
    return None


def _per_game_layout_path(
    resolution: str, game_id: int, assets_root: Optional[str] = None
) -> str:
    root = assets_root or default_assets_root()
    return os.path.join(root, format_game_index(game_id), resolution, "layout.json")


def load_current_layout(
    resolution: str, game_id: int, assets_root: Optional[str] = None
) -> OdometerLayout:
    path = current_layout_path(resolution, game_id, assets_root)
    if path:
        return load_layout_file(path)
    return OdometerLayout()


def load_current_config(
    resolution: str, game_id: int, assets_root: Optional[str] = None
) -> OdometerConfig:
    root = assets_root or default_assets_root()
    merged = {}
    for path in (
        os.path.join(root, "Shared", resolution, "layout.json"),
        _per_game_layout_path(resolution, game_id, root),
        os.path.join(root, format_game_index(game_id), resolution, "layout.override.json"),
    ):
        if not os.path.isfile(path):
            continue
        data = load_layout_data(path)
        block = data.get("odometer")
        if isinstance(block, dict):
            merged.update(block)
    return OdometerConfig.from_mapping(merged)


def load_current_theme(
    game_id: int,
    resolution: str,
    assets_root: Optional[str] = None,
    *,
    include_background: bool = False,
    include_frame: bool = True,
) -> OdometerTheme:
    root = assets_root or default_assets_root()
    game_token = format_game_index(game_id)
    folder = os.path.join(root, game_token, resolution)
    shared_dir = default_shared_resolution_dir(resolution, root)
    layout = load_current_layout(resolution, game_id, root)

    background_path = (
        _pick_file(
            folder,
            [rf"^{game_token}-BG(?:\.[A-Za-z0-9]+)?$", r"theme", r"32x9.*theme"],
        )
        if include_background
        else None
    )
    frame_path = None
    blocking_path = None
    if include_frame:
        frame_path = _pick_file(
            shared_dir,
            [
                r"^FR(?:\.[A-Za-z0-9]+)?$",
                r"^Shared-FR(?:\.[A-Za-z0-9]+)?$",
                r"^[0-9]{2}-FR(?:\.[A-Za-z0-9]+)?$",
                r"frame_01",
                r"32x9_frame",
            ],
        )
        if frame_path is None:
            frame_path = _pick_file(
                folder,
                [rf"^{game_token}-FR(?:\.[A-Za-z0-9]+)?$", r"frame_01", r"32x9_frame"],
            )
        if (
            legacy_layout is None
            or legacy_layout.blocking_asset_applies(resolution)
        ):
            blocking_path = _pick_file(
                folder, [rf"^{game_token}-BL(?:\.[A-Za-z0-9]+)?$"]
            )

    return OdometerTheme(
        aligned=load_aligned_cell_library(shared_dir, layout=layout),
        layout=layout,
        background=_load_surface(background_path) if background_path else None,
        blocking=_load_surface(blocking_path) if blocking_path else None,
        frame=_load_surface(frame_path) if frame_path else None,
        glyph_dir=shared_dir,
        background_path=background_path or "",
        blocking_path=blocking_path or "",
        frame_path=frame_path or "",
    )


def create_current_widget(
    game_id: int,
    resolution: str,
    assets_root: Optional[str] = None,
    *,
    include_background: bool = False,
    include_frame: bool = True,
    initial_target_pennies: Optional[int] = None,
) -> OdometerWidget:
    config = load_current_config(resolution, game_id, assets_root)
    theme = load_current_theme(
        game_id,
        resolution,
        assets_root,
        include_background=include_background,
        include_frame=include_frame,
    )
    start = (
        config.initial_display_pennies
        if initial_target_pennies is None
        else max(0, int(initial_target_pennies))
    )
    return OdometerWidget(theme, config=config, initial_target_pennies=start)


def optimize_theme_surfaces(theme: OdometerTheme) -> None:
    """Convert loaded surfaces after `pygame.display.set_mode()` for faster blits."""
    if pygame.display.get_surface() is None:
        return
    if theme.background is not None:
        object.__setattr__(theme, "background", _optimize_surface(theme.background))
    if theme.blocking is not None:
        object.__setattr__(theme, "blocking", _optimize_surface(theme.blocking))
    if theme.frame is not None:
        object.__setattr__(theme, "frame", _optimize_surface(theme.frame))
    object.__setattr__(
        theme.aligned.dollar,
        "surface",
        _optimize_surface(theme.aligned.dollar.surface),
    )
    object.__setattr__(
        theme.aligned.comma,
        "surface",
        _optimize_surface(theme.aligned.comma.surface),
    )
    object.__setattr__(
        theme.aligned.decimal,
        "surface",
        _optimize_surface(theme.aligned.decimal.surface),
    )
    for spec in theme.aligned.digits.values():
        object.__setattr__(spec, "surface", _optimize_surface(spec.surface))


def _pick_file(
    folder: str, patterns: list[str], exclude: Optional[list[str]] = None
) -> Optional[str]:
    if not os.path.isdir(folder):
        return None
    exclude = exclude or ["preview", "sample", ".psd"]
    names = sorted(os.listdir(folder))
    for pattern in patterns:
        rx = re.compile(pattern, re.IGNORECASE)
        for name in names:
            low = name.lower()
            if not rx.search(name):
                continue
            if any(token in low for token in exclude):
                continue
            if low.endswith((".png", ".jpg", ".jpeg", ".webp")):
                return os.path.join(folder, name)
    return None


def _load_surface(path: str) -> pygame.Surface:
    return pygame.image.load(path)


def _optimize_surface(surf: pygame.Surface) -> pygame.Surface:
    if surf.get_flags() & pygame.SRCALPHA:
        return surf.convert_alpha()
    return surf.convert()
