"""Font-independent typesetting primitives, shared by build_logo.py and build_og.py.

Everything here turns text into SVG `<path>` data via fontTools, so nothing that
ships depends on a webfont being present at render time. Both the logo and the
Open Graph cards are rasterized by rsvg-convert, which has no access to the
site's woff2 subsets — outlining is what makes them agree.
"""
from fontTools.ttLib import TTFont
from fontTools.varLib.instancer import instantiateVariableFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.transformPen import TransformPen


def load(path, axes=None):
    """Load a font, pinning a variable one to `axes`. Static faces pass axes=None."""
    f = TTFont(path)
    if axes and "fvar" in f:
        instantiateVariableFont(f, axes, inplace=True)
    return {"gs": f.getGlyphSet(), "cmap": f.getBestCmap(),
            "upm": f["head"].unitsPerEm, "cap": f["OS/2"].sCapHeight,
            "name": path.split("/")[-1]}


def require_glyphs(font, text):
    """Fail loudly on a character the font cannot draw.

    A missing glyph does not raise on its own — `cmap[ord(ch)]` KeyErrors here,
    but the *browser* would silently fall back to another family and only look
    slightly wrong. Display faces routinely lack the currency sign the site
    quotes prices in — that shipped once before anyone noticed. Call this before
    laying out any string that came from site content rather than from this file.
    """
    missing = sorted({ch for ch in text if ord(ch) not in font["cmap"]})
    if missing:
        raise SystemExit(
            f"{font['name']} cannot draw: "
            + ", ".join(f"{ch!r} (U+{ord(ch):04X})" for ch in missing)
        )


def layout(font, text, em, tracking=0.0, x0=0.0, baseline=0.0):
    """Per-glyph SVG paths baked into px space (y-down), plus the end pen x."""
    s = em / font["upm"]
    penx = x0
    items = []
    for ch in text:
        gn = font["cmap"][ord(ch)]
        pen = SVGPathPen(font["gs"])
        font["gs"][gn].draw(TransformPen(pen, (s, 0, 0, -s, penx, baseline)))
        items.append(pen.getCommands())
        penx += (font["gs"][gn].width + tracking) * s
    return items, penx


def bounds(font, text, em, tracking=0.0, x0=0.0, baseline=0.0):
    s = em / font["upm"]
    bp = BoundsPen(font["gs"])
    penx = x0
    for ch in text:
        gn = font["cmap"][ord(ch)]
        font["gs"][gn].draw(TransformPen(bp, (s, 0, 0, -s, penx, baseline)))
        penx += (font["gs"][gn].width + tracking) * s
    return bp.bounds


def advance(font, text, em, tracking=0.0):
    """Pen advance for a string — the width to wrap against.

    Deliberately not `bounds()`: bounds is the inked extent, which excludes the
    trailing sidebearing and would let a line creep past the margin.
    """
    s = em / font["upm"]
    return sum((font["gs"][font["cmap"][ord(ch)]].width + tracking) * s for ch in text)


# Thai marks that hang above or below their consonant and must never begin a
# line, and the leading vowels that are written *in front of* the consonant they
# are pronounced after and so must never end one.
_THAI_MARKS = {0x0E31, *range(0x0E34, 0x0E3B), *range(0x0E47, 0x0E4F)}
_THAI_LEADING = set(range(0x0E40, 0x0E45))
_THAI_BLOCK = range(0x0E00, 0x0E80)


def clusters(word):
    """Split a word into the smallest units a line may break between.

    Thai writes no spaces between words, so a word-only wrap treats a whole Thai
    sentence as a single token and never breaks it at all — the failure is not an
    error but one line running off the edge of the card. Proper Thai line
    breaking needs a dictionary; without one, breaking between orthographic
    clusters is the usable approximation. It never splits a consonant from the
    vowel or tone mark sitting on it, and never strands a leading vowel from the
    consonant it precedes. Latin words come back whole.
    """
    if not any(ord(c) in _THAI_BLOCK for c in word):
        return [word]
    out = []
    for ch in word:
        if out and (ord(ch) in _THAI_MARKS or ord(out[-1][-1]) in _THAI_LEADING):
            out[-1] += ch
        else:
            out.append(ch)
    return out


def _tokens(text):
    """(chunk, glue) pairs. `glue` is the space a word break restores, if any."""
    out = []
    for wi, word in enumerate(text.split()):
        for ci, chunk in enumerate(clusters(word)):
            out.append((chunk, " " if (wi and ci == 0) else ""))
    return out


def wrap(font, text, em, max_w, tracking=0.0):
    """Greedy wrap, breaking between words or — in Thai — between clusters.

    Identical to a plain word wrap for space-delimited text: the tokens are the
    words and the glue is the space. Truncation has to follow the same rule —
    ellipsizing by character will cut a consonant off the mark that belongs to
    it and leave the mark floating on its own.
    """
    lines, cur = [], ""
    for chunk, glue in _tokens(text):
        trial = cur + glue + chunk
        if cur and advance(font, trial, em, tracking) > max_w:
            lines.append(cur)
            cur = chunk
        else:
            cur = trial
    if cur:
        lines.append(cur)
    return lines


def rrect(x, y, w, h, r):
    r = min(r, w / 2, h / 2)
    return (f"M{x + r:.2f},{y:.2f} H{x + w - r:.2f} A{r:.2f},{r:.2f} 0 0 1 {x + w:.2f},{y + r:.2f} "
            f"V{y + h - r:.2f} A{r:.2f},{r:.2f} 0 0 1 {x + w - r:.2f},{y + h:.2f} "
            f"H{x + r:.2f} A{r:.2f},{r:.2f} 0 0 1 {x:.2f},{y + h - r:.2f} "
            f"V{y + r:.2f} A{r:.2f},{r:.2f} 0 0 1 {x + r:.2f},{y:.2f} Z")


def svg(vb, body, label):
    x, y, w, h = vb
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x:.2f} {y:.2f} {w:.2f} {h:.2f}" '
            f'role="img" aria-label="{label}" fill="none">\n{body}</svg>\n')
