"""
Font registry and text rendering for PDF signature appearances.

Provides ``Font`` and ``FontMetrics`` dataclasses, a lazy-loading font
registry, and convenience functions (``text_width``, ``wrap_lines``,
``pdf_escape``) that delegate to the current default font.

Available fonts:
  - noto-sans: Noto Sans (Google, OFL 1.1) -- default
  - ghea-mariam: GHEA Mariam (Armenian)
  - ghea-grapalat: GHEA Grapalat (Armenian)
"""

from __future__ import annotations

import threading
from dataclasses import dataclass
from typing import TYPE_CHECKING, NamedTuple

if TYPE_CHECKING:
    from collections.abc import Callable


@dataclass(frozen=True)
class FontMetrics:
    """PDF font metrics for Type0/CIDFontType2 embedding.

    All fields needed to embed a font in a PDF and measure text.
    Generated by scripts/prepare_font.py.
    """

    name: str  # PDF BaseFont name, e.g. "NotoSans"
    units_per_em: int
    ascent: int
    descent: int
    cap_height: int
    bbox: tuple[int, int, int, int]
    stem_v: int
    italic_angle: int
    default_width: int
    cmap: dict[int, int]  # Unicode codepoint -> glyph ID
    widths: dict[int, int]  # glyph ID -> advance width
    cid_widths_str: str  # PDF /W array string
    tounicode_cmap: str  # ToUnicode CMap string
    ttf_package: str  # importlib.resources package path
    ttf_resource: str  # TTF filename within the package


@dataclass(frozen=True)
class Font:
    """A font for PDF signature text rendering.

    Attributes:
        name: PDF BaseFont name (e.g. "NotoSans").
        text_width: Callable(text, font_size) -> width in PDF points.
        pdf_escape: Callable(text) -> PDF-ready string for Tj operator.
        metrics: Full metrics for PDF embedding.
    """

    name: str
    text_width: Callable[[str, float], float]
    pdf_escape: Callable[[str], str]
    metrics: FontMetrics


# ── Font registry ────────────────────────────────────────────────

AVAILABLE_FONTS: tuple[str, ...] = ("noto-sans", "ghea-mariam", "ghea-grapalat")
DEFAULT_FONT = "noto-sans"


class _FontDef(NamedTuple):
    """Definition of a font available for PDF signature rendering."""

    metrics_module: str  # importlib module path for metrics
    ttf_package: str  # importlib.resources package for TTF file
    ttf_resource: str  # TTF filename within the package
    basefont_name: str  # PDF BaseFont name


_FONT_DEFS: dict[str, _FontDef] = {
    "noto-sans": _FontDef(
        metrics_module="revenant.core.appearance.font_data.noto_sans.metrics",
        ttf_package="revenant.core.appearance.font_data.noto_sans",
        ttf_resource="NotoSans-Subset.ttf",
        basefont_name="NotoSans",
    ),
    "ghea-mariam": _FontDef(
        metrics_module="revenant.core.appearance.font_data.ghea_mariam.metrics",
        ttf_package="revenant.core.appearance.font_data.ghea_mariam",
        ttf_resource="GHEAMariam-Subset.ttf",
        basefont_name="GHEAMariam",
    ),
    "ghea-grapalat": _FontDef(
        metrics_module="revenant.core.appearance.font_data.ghea_grapalat.metrics",
        ttf_package="revenant.core.appearance.font_data.ghea_grapalat",
        ttf_resource="GHEAGrapalat-Subset.ttf",
        basefont_name="GHEAGrapalat",
    ),
}

_font_cache: dict[str, Font] = {}
_font_cache_lock = threading.Lock()
_default_font_key: str = DEFAULT_FONT


def get_font(name: str | None = None) -> Font:
    """Get a font by key name. Lazy-loads metrics on first access.

    Thread-safe: uses a lock to prevent duplicate loading.

    Args:
        name: Font key (e.g. "noto-sans"). If None, returns the default.

    Raises:
        ValueError: If the font name is not recognized.
    """
    key = name or _default_font_key
    if key in _font_cache:
        return _font_cache[key]
    if key not in _FONT_DEFS:
        available = ", ".join(AVAILABLE_FONTS)
        raise ValueError(f"Unknown font {key!r}. Available: {available}")
    with _font_cache_lock:
        # Double-check after acquiring lock
        if key in _font_cache:
            return _font_cache[key]
        return _load_font(key)


def _load_font(key: str) -> Font:
    """Import a metrics module and create a Font instance."""
    import importlib

    font_def = _FONT_DEFS[key]
    try:
        mod = importlib.import_module(font_def.metrics_module)
    except ModuleNotFoundError:
        raise ValueError(
            f"Font {key!r} is not available. Missing metrics module: {font_def.metrics_module}"
        ) from None

    metrics = FontMetrics(
        name=font_def.basefont_name,
        units_per_em=mod.UNITS_PER_EM,
        ascent=mod.ASCENT,
        descent=mod.DESCENT,
        cap_height=mod.CAP_HEIGHT,
        bbox=mod.BBOX,
        stem_v=mod.STEM_V,
        italic_angle=mod.ITALIC_ANGLE,
        default_width=mod.DEFAULT_WIDTH,
        cmap=mod.CMAP,
        widths=mod.WIDTHS,
        cid_widths_str=mod.CID_WIDTHS_STR,
        tounicode_cmap=mod.TOUNICODE_CMAP,
        ttf_package=font_def.ttf_package,
        ttf_resource=font_def.ttf_resource,
    )

    font = Font(
        name=font_def.basefont_name,
        text_width=_make_text_width(metrics),
        pdf_escape=_make_pdf_escape(metrics),
        metrics=metrics,
    )
    _font_cache[key] = font
    return font


def _make_text_width(m: FontMetrics) -> Callable[[str, float], float]:
    """Create a text_width closure for a specific font's metrics."""
    cmap = m.cmap
    widths = m.widths
    default_w = m.default_width
    upm = m.units_per_em

    def _tw(text: str, font_size: float) -> float:
        total = 0
        for ch in text:
            gid = cmap.get(ord(ch), 0)
            total += widths.get(gid, default_w)
        return total * font_size / upm

    return _tw


def _make_pdf_escape(m: FontMetrics) -> Callable[[str], str]:
    """Create a pdf_escape closure for a specific font's cmap.

    Characters not in the font's cmap are replaced with '?' to avoid
    invisible .notdef glyphs in the rendered PDF.
    """
    cmap = m.cmap
    question_gid = cmap.get(ord("?"), 0)

    def _pe(text: str) -> str:
        parts: list[str] = []
        for ch in text:
            gid = cmap.get(ord(ch))
            if gid is None:
                gid = question_gid
            parts.append(f"{gid:04X}")
        return f"<{''.join(parts)}>"

    return _pe


# ── Default font ─────────────────────────────────────────────────


def get_default_font() -> Font:
    """Return the current default font."""
    return get_font(_default_font_key)


# ── Convenience functions (delegate to default font) ─────────────


def text_width(text: str, font_size: float) -> float:
    """Measure text width in PDF points using the default font."""
    return get_default_font().text_width(text, font_size)


def pdf_escape(text: str) -> str:
    """Format text for PDF Tj operator using the default font."""
    return get_default_font().pdf_escape(text)


def wrap_lines(
    text: str,
    font_size: float,
    max_width: float,
    measure: Callable[[str, float], float] | None = None,
) -> list[str]:
    """Word-wrap text using a text_width measurement function.

    Args:
        text: Text to wrap.
        font_size: Font size in PDF points.
        max_width: Maximum line width in PDF points.
        measure: text_width(text, font_size) -> width callable.
            If None, uses the default font's text_width.
    """
    if measure is None:
        measure = get_default_font().text_width
    words = text.split()
    lines: list[str] = []
    current = ""
    for word in words:
        candidate = f"{current} {word}".strip() if current else word
        if measure(candidate, font_size) <= max_width:
            current = candidate
        else:
            if current:
                lines.append(current)
            current = word
    if current:
        lines.append(current)
    return lines


def encode_text_hex(text: str, cmap: dict[int, int] | None = None) -> str:
    """Encode text as hex glyph IDs for Type0/Identity-H font.

    Each character is mapped to its glyph ID via the cmap table,
    then encoded as a 4-hex-digit big-endian value.
    Unknown characters are replaced with '?' glyph.
    """
    if cmap is None:
        cmap = get_default_font().metrics.cmap
    question_gid = cmap.get(ord("?"), 0)
    parts: list[str] = []
    for ch in text:
        gid = cmap.get(ord(ch))
        if gid is None:
            gid = question_gid
        parts.append(f"{gid:04X}")
    return "".join(parts)
