from __future__ import annotations

import os
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

import pygame

from ..core.engine import leftmost_significant_power
from .layout import OdometerLayout, layout_from_dict, load_layout_file

COMMA_LEFT_POWER = 5
TokenKind = str

_FILENAME_DIGIT = re.compile(r"^(\d)_(\d+)x(\d+)\.png$", re.I)
_FILENAME_DOLLAR_SUFFIX = re.compile(r"^(\d+)x(\d+)_\$\.png$", re.I)
_FILENAME_DOLLAR_PREFIX = re.compile(r"^\$_(\d+)x(\d+)\.png$", re.I)
_FILENAME_PUNCT = re.compile(r"^(comma|decimal)_(\d+)x(\d+)\.png$", re.I)


@dataclass(frozen=True)
class CellSpec:
    kind: TokenKind
    w: int
    h: int
    surface: pygame.Surface
    digit: Optional[int] = None


@dataclass(frozen=True)
class AlignedOdometerLayout:
    anchor_right: int
    anchor_bottom: int
    glyph_dir: str


@dataclass(frozen=True)
class AlignedCellLibrary:
    dollar: CellSpec
    digits: Dict[int, CellSpec]
    comma: CellSpec
    decimal: CellSpec
    layout: AlignedOdometerLayout

    def spec_for(self, kind: TokenKind, digit: Optional[int] = None) -> CellSpec:
        if kind == "dollar":
            return self.dollar
        if kind == "comma":
            return self.comma
        if kind == "decimal":
            return self.decimal
        if kind == "digit" and digit is not None:
            return self.digits[digit]
        raise KeyError(f"unknown token {kind!r} digit={digit!r}")


@dataclass(frozen=True)
class OdometerTheme:
    aligned: AlignedCellLibrary
    layout: OdometerLayout = OdometerLayout()
    background: Optional[pygame.Surface] = None
    blocking: Optional[pygame.Surface] = None
    frame: Optional[pygame.Surface] = None
    glyph_dir: str = ""
    background_path: str = ""
    blocking_path: str = ""
    frame_path: str = ""

    @classmethod
    def from_dir(
        cls,
        path: str,
        *,
        layout_path: Optional[str] = None,
        background_path: Optional[str] = None,
        frame_path: Optional[str] = None,
    ) -> "OdometerTheme":
        glyph_dir = os.path.abspath(path)
        resolved_layout_path = layout_path or os.path.join(glyph_dir, "layout.json")
        layout = (
            load_layout_file(resolved_layout_path)
            if os.path.isfile(resolved_layout_path)
            else OdometerLayout()
        )
        library = load_aligned_cell_library(glyph_dir, layout=layout)
        resolved_frame_path = frame_path or _guess_frame_path(glyph_dir)
        background = _load_surface(background_path) if background_path else None
        frame = _load_surface(resolved_frame_path) if resolved_frame_path else None
        return cls(
            aligned=library,
            layout=layout,
            background=background,
            frame=frame,
            glyph_dir=glyph_dir,
            background_path=os.path.abspath(background_path) if background_path else "",
            frame_path=os.path.abspath(resolved_frame_path) if resolved_frame_path else "",
        )

    @classmethod
    def from_config(
        cls, data: Dict[str, Any], *, base_dir: Optional[str] = None
    ) -> "OdometerTheme":
        assets = data.get("assets", {})
        layout_data = data.get("layout", {})
        base = os.path.abspath(base_dir) if base_dir else os.getcwd()
        glyph_dir = _resolve_path(
            assets.get("aligned_dir")
            or assets.get("glyph_dir")
            or assets.get("directory")
            or assets.get("path"),
            base,
        )
        if not glyph_dir:
            raise ValueError("OdometerTheme.from_config requires assets.aligned_dir")
        layout = (
            layout_from_dict(layout_data, source_path="")
            if isinstance(layout_data, dict)
            else OdometerLayout()
        )
        library = load_aligned_cell_library(glyph_dir, layout=layout)
        background_path = _resolve_path(assets.get("background"), base)
        frame_path = _resolve_path(assets.get("frame"), base)
        return cls(
            aligned=library,
            layout=layout,
            background=_load_surface(background_path) if background_path else None,
            frame=_load_surface(frame_path) if frame_path else None,
            glyph_dir=glyph_dir,
            background_path=background_path or "",
            frame_path=frame_path or "",
        )

    @property
    def frame_size(self) -> Tuple[int, int]:
        if self.frame is not None:
            return self.frame.get_size()
        return (0, 0)

    @property
    def default_rect(self) -> pygame.Rect:
        if self.frame is not None:
            return pygame.Rect(self.layout.frame_anchor, self.frame.get_size())
        if self.layout.screen_size != (0, 0):
            return pygame.Rect((0, 0), self.layout.screen_size)
        return pygame.Rect((0, 0), (0, 0))

    @property
    def anchor_bottom_offset(self) -> int:
        _, anchor_bottom = aligned_anchor_from_layout(self.layout, self.aligned)
        return anchor_bottom - int(self.layout.frame_anchor[1])


def _resolve_path(path: Optional[str], base_dir: str) -> Optional[str]:
    if not path:
        return None
    if os.path.isabs(path):
        return path
    return os.path.abspath(os.path.join(base_dir, path))


def _guess_frame_path(folder: str) -> Optional[str]:
    for name in sorted(os.listdir(folder)):
        low = name.lower()
        if low in {"fr.png", "shared-fr.png"} or low.endswith("-fr.png"):
            return os.path.join(folder, name)
    return None


def _load_png(path: str) -> pygame.Surface:
    return pygame.image.load(path)


def _load_surface(path: Optional[str]) -> Optional[pygame.Surface]:
    if not path:
        return None
    if not os.path.isfile(path):
        return None
    return pygame.image.load(path)


def load_aligned_cell_library(
    glyph_dir: str, *, layout: Optional[OdometerLayout] = None
) -> AlignedCellLibrary:
    if not os.path.isdir(glyph_dir):
        raise FileNotFoundError(f"Missing glyph directory: {glyph_dir}")

    digit_specs: Dict[int, Tuple[int, int, str]] = {}
    dollar_path: Optional[str] = None
    dollar_size: Optional[Tuple[int, int]] = None
    punct: Dict[str, Tuple[int, int, str]] = {}

    for name in os.listdir(glyph_dir):
        low = name.lower()
        if not low.endswith(".png"):
            continue
        path = os.path.join(glyph_dir, name)
        match = _FILENAME_DIGIT.match(name)
        if match:
            digit_specs[int(match.group(1))] = (
                int(match.group(2)),
                int(match.group(3)),
                path,
            )
            continue
        match = _FILENAME_DOLLAR_SUFFIX.match(name) or _FILENAME_DOLLAR_PREFIX.match(
            name
        )
        if match:
            dollar_path = path
            dollar_size = (int(match.group(1)), int(match.group(2)))
            continue
        match = _FILENAME_PUNCT.match(name)
        if match:
            punct[str(match.group(1)).lower()] = (
                int(match.group(2)),
                int(match.group(3)),
                path,
            )

    if len(digit_specs) != 10 or dollar_path is None or "comma" not in punct or "decimal" not in punct:
        raise FileNotFoundError(f"Incomplete aligned glyph set in {glyph_dir}")

    digits = {
        digit: CellSpec("digit", w, h, _load_png(path), digit=digit)
        for digit, (w, h, path) in digit_specs.items()
    }
    dollar_w, dollar_h = dollar_size or (0, 0)
    comma_w, comma_h, comma_path = punct["comma"]
    decimal_w, decimal_h, decimal_path = punct["decimal"]
    anchor_right, anchor_bottom = aligned_anchor_from_layout(layout, None)

    return AlignedCellLibrary(
        dollar=CellSpec("dollar", dollar_w, dollar_h, _load_png(dollar_path)),
        digits=digits,
        comma=CellSpec("comma", comma_w, comma_h, _load_png(comma_path)),
        decimal=CellSpec("decimal", decimal_w, decimal_h, _load_png(decimal_path)),
        layout=AlignedOdometerLayout(anchor_right, anchor_bottom, glyph_dir),
    )


def build_rtl_tokens(
    digits: List[int], leftmost: int, *, show_comma: bool
) -> List[Tuple[TokenKind, Optional[int]]]:
    tokens: List[Tuple[TokenKind, Optional[int]]] = []
    for power in range(0, leftmost + 1):
        tokens.append(("digit", digits[power]))
        if power == 1 and leftmost >= 2:
            tokens.append(("decimal", None))
        if power == 4 and show_comma:
            tokens.append(("comma", None))
    tokens.append(("dollar", None))
    return tokens


def total_string_width(
    library: AlignedCellLibrary, tokens: List[Tuple[TokenKind, Optional[int]]]
) -> int:
    return sum(library.spec_for(kind, digit).w for kind, digit in tokens)


def aligned_anchor_from_layout(
    layout: Optional[OdometerLayout], library: Optional[AlignedCellLibrary]
) -> Tuple[int, int]:
    if layout is not None:
        anchor_right = layout.aligned_anchor_right
        anchor_bottom = layout.aligned_anchor_bottom
        if anchor_right is not None or anchor_bottom is not None:
            return int(anchor_right or 0), int(anchor_bottom or 0)
    if library is not None:
        return library.layout.anchor_right, library.layout.anchor_bottom
    return (0, 0)


def visible_tokens_for_digits(
    digits: List[int],
) -> List[Tuple[TokenKind, Optional[int]]]:
    leftmost = leftmost_significant_power(digits)
    return build_rtl_tokens(
        digits,
        leftmost,
        show_comma=leftmost >= COMMA_LEFT_POWER,
    )
