#!/usr/bin/env python3
"""Vectorize LCD TTF fonts into SVG symbol sets for the hub clock widget."""

from __future__ import annotations

import argparse
import re
from dataclasses import dataclass
from pathlib import Path

from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.ttLib import TTFont

CHARS = (
    [" "]
    + [str(d) for d in range(10)]
    + [chr(c) for c in range(ord("A"), ord("Z") + 1)]
    + [":", "+", "-", "/", "."]
)

TARGET_HEIGHT = 32.0
PADDING_X = 1.5
PADDING_Y = 1.0


@dataclass(frozen=True)
class FontSpec:
    ttf: str
    prefix: str
    outfile: str
    comment: str


FONTS: dict[str, FontSpec] = {
    "dseg14-classic": FontSpec(
        ttf="DSEG14Classic-Regular.ttf",
        prefix="d14c",
        outfile="dseg14-classic-alphabet.svg",
        comment="DSEG14 Classic (OFL) — vectorized for hub clock",
    ),
    "dseg14-modern": FontSpec(
        ttf="DSEG14Modern-Regular.ttf",
        prefix="d14m",
        outfile="dseg14-modern-alphabet.svg",
        comment="DSEG14 Modern (OFL) — vectorized for hub clock",
    ),
    "ds-digital": FontSpec(
        ttf="DS-Digital.ttf",
        prefix="dsd",
        outfile="ds-digital-alphabet.svg",
        comment="DS-Digital by Dusit Supasawat (shareware) — vectorized for hub clock",
    ),
    "digital-7-mono-italic": FontSpec(
        ttf="digital-7-mono-italic.ttf",
        prefix="d7mi",
        outfile="digital-7-mono-italic-alphabet.svg",
        comment="Digital-7 Mono Italic by Style-7 (personal use) — vectorized for hub clock",
    ),
}


def symbol_id(prefix: str, ch: str) -> str:
    if ch == " ":
        return f"{prefix}-space"
    if ch == ":":
        return f"{prefix}-colon"
    if ch == "+":
        return f"{prefix}-plus"
    if ch == "-":
        return f"{prefix}-minus"
    if ch == "/":
        return f"{prefix}-slash"
    if ch == ".":
        return f"{prefix}-dot"
    if len(ch) == 1 and "0" <= ch <= "9":
        return f"{prefix}-{ch}"
    if len(ch) == 1 and "A" <= ch <= "Z":
        return f"{prefix}-{ch}"
    return f"{prefix}-u{ord(ch):04x}"


def glyph_path(font: TTFont, ch: str) -> tuple[str, float, float, float, float] | None:
    cmap = font.getBestCmap()
    code = ord(ch)
    if code not in cmap:
        return None
    glyph_name = cmap[code]
    glyph_set = font.getGlyphSet()
    if glyph_name not in glyph_set:
        return None
    glyph = glyph_set[glyph_name]

    pen = SVGPathPen(glyph_set)
    bounds_pen = BoundsPen(glyph_set)
    glyph.draw(bounds_pen)
    if bounds_pen.bounds is None:
        return None
    x_min, y_min, x_max, y_max = bounds_pen.bounds
    glyph.draw(pen)
    path = pen.getCommands()
    if not path.strip():
        return None
    return path, x_min, y_min, x_max, y_max


def char_width(font: TTFont, ch: str, fallback: float) -> float:
    cmap = font.getBestCmap()
    code = ord(ch)
    if code not in cmap:
        return fallback
    glyph_name = cmap[code]
    if glyph_name in font["hmtx"].metrics:
        adv, _ = font["hmtx"].metrics[glyph_name]
        return float(adv)
    return fallback


def build_alphabet(spec: FontSpec, fonts_dir: Path, out_dir: Path) -> None:
    ttf_path = fonts_dir / spec.ttf
    font = TTFont(ttf_path)
    units_per_em = float(font["head"].unitsPerEm or 1000)

    lines = [
        '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" '
        'style="position:absolute;width:0;height:0;overflow:hidden">',
        f"  <!-- {spec.comment} -->",
        "  <defs>",
    ]

    for ch in CHARS:
        sid = symbol_id(spec.prefix, ch)
        if ch == " ":
            width = max(8.0, TARGET_HEIGHT * 0.35)
            lines.append(
                f'    <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}"></symbol>'
            )
            continue

        parsed = glyph_path(font, ch)
        if parsed is None and ch.isalpha():
            parsed = glyph_path(font, ch.lower())
        if parsed is None:
            width = char_width(font, "0", 20.0) * (TARGET_HEIGHT / units_per_em)
            lines.append(
                f'    <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}"></symbol>'
            )
            continue

        path, x_min, y_min, x_max, y_max = parsed
        glyph_w = max(x_max - x_min, 1.0)
        glyph_h = max(y_max - y_min, 1.0)
        scale = (TARGET_HEIGHT - PADDING_Y * 2) / glyph_h
        tx = PADDING_X - x_min * scale
        ty = TARGET_HEIGHT - PADDING_Y - y_max * scale
        width = max(glyph_w * scale + PADDING_X * 2, char_width(font, ch, glyph_w) * scale * 0.55)

        # Flip Y: font coords are bottom-up; SVG is top-down.
        transform = f"translate({tx:.3f} {ty:.3f}) scale({scale:.6f} {scale:.6f})"
        lines.append(f'    <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}">')
        lines.append(f'      <path class="lcd-active" d="{path}" transform="{transform}" />')
        lines.append("    </symbol>")

    lines.extend(["  </defs>", "</svg>", ""])
    out_path = out_dir / spec.outfile
    out_path.write_text("\n".join(lines), encoding="utf-8")
    print(f"Wrote {out_path}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--fonts-dir",
        type=Path,
        default=Path(__file__).resolve().parent / "fonts",
    )
    parser.add_argument(
        "--out-dir",
        type=Path,
        default=Path(__file__).resolve().parent,
    )
    parser.add_argument(
        "--only",
        choices=sorted(FONTS.keys()),
        action="append",
        help="Build one font (repeatable). Default: all.",
    )
    args = parser.parse_args()

    selected = args.only or sorted(FONTS.keys())
    for key in selected:
        build_alphabet(FONTS[key], args.fonts_dir, args.out_dir)


if __name__ == "__main__":
    main()
