#!/usr/bin/env python3
"""Build the deterministic Pivot Sans source and web release.

Pivot Sans is a scripted overlay on the official Inclusive Sans 2.004
binaries. The overlay adds:

- money-aware tabular alternates for digits and ``$ £ ¥ ₹ €``;
- encoded composite glyphs for ``≠ ≤ ≥ ≈ ‰``;
- a default-off ``ss02`` disambiguated lowercase-l family;
- corrected fork metadata and Windows clipping bounds;
- direct overlays on all official static fonts; and
- optional static pins at the axis values declared in ``spec.json``.

All upstream files are immutable inputs verified by ``upstream/SHA256SUMS``.
"""

from __future__ import annotations

import hashlib
import json
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path

from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import otTables
from fontTools.ttLib.tables._g_l_y_f import (
    Glyph,
    GlyphComponent,
    GlyphCoordinates,
)
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables.ttProgram import Program
from fontTools.subset import Options, Subsetter
from fontTools.varLib.instancer import instantiateVariableFont

try:
    from build_subsets import build_subsets
except ModuleNotFoundError:  # Support ``python -m pipeline.build`` as well.
    from pipeline.build_subsets import build_subsets

ROOT = Path(__file__).resolve().parent.parent
UPSTREAM = ROOT / "upstream"
DIST = ROOT / "dist"
SPEC = json.loads((ROOT / "pipeline" / "spec.json").read_text())

VARIABLE_SOURCES = {
    "upright": UPSTREAM / "InclusiveSans-VariableFont_wght.ttf",
    "italic": UPSTREAM / "InclusiveSans-Italic-VariableFont_wght.ttf",
}
VARIABLE_OUTPUTS = {
    "upright": "PivotSans[wght].ttf",
    "italic": "PivotSans-Italic[wght].ttf",
}
STATIC_SOURCES = [
    ("upright", 300, "InclusiveSans-Light.ttf", "PivotSans-Light.ttf"),
    ("upright", 400, "InclusiveSans-Regular.ttf", "PivotSans-Regular.ttf"),
    ("upright", 500, "InclusiveSans-Medium.ttf", "PivotSans-Medium.ttf"),
    ("upright", 600, "InclusiveSans-SemiBold.ttf", "PivotSans-SemiBold.ttf"),
    ("upright", 700, "InclusiveSans-Bold.ttf", "PivotSans-Bold.ttf"),
    ("italic", 300, "InclusiveSans-LightItalic.ttf", "PivotSans-LightItalic.ttf"),
    ("italic", 400, "InclusiveSans-Italic.ttf", "PivotSans-Italic.ttf"),
    ("italic", 500, "InclusiveSans-MediumItalic.ttf", "PivotSans-MediumItalic.ttf"),
    (
        "italic",
        600,
        "InclusiveSans-SemiBoldItalic.ttf",
        "PivotSans-SemiBoldItalic.ttf",
    ),
    ("italic", 700, "InclusiveSans-BoldItalic.ttf", "PivotSans-BoldItalic.ttf"),
]

DIGITS = SPEC["digits"]
CURRENCIES = SPEC["currencies"]
TABULAR_BASES = SPEC["tabular_bases"]
# Inclusive Sans's rvrn feature substitutes the dollar at heavier weights
# before later features run. Cover that existing glyph as a tnum source too,
# so the active heavy-dollar design joins the same money grid.
TABULAR_RVRN_SOURCES = {
    "dollar.BRACKET.varAlt01": "dollar",
}
TABULAR_SOURCES = {
    **{glyph: glyph for glyph in TABULAR_BASES},
    **TABULAR_RVRN_SOURCES,
}
TABULAR_ALTERNATES = [f"{glyph}.tf" for glyph in TABULAR_SOURCES]
MATH_CODEPOINTS = {
    glyph: int(codepoint.removeprefix("U+"), 16)
    for glyph, codepoint in SPEC["math_composites"].items()
}
MATH_GLYPHS = list(MATH_CODEPOINTS)
SS02_L_SOURCES = SPEC["ss02_l_sources"]
SS02_FOOT = "l.foot.ss02"
SS02_TARGETS = {
    source: f"{source}.ss02" for source in SS02_L_SOURCES
}
SS02_GLYPHS = [SS02_FOOT, *SS02_TARGETS.values()]
SS02_UI_NAME_ID = 300
SS02_UI_NAME = "Disambiguated lowercase l"
STATIC_PINS = SPEC["static_pins"]
SLOT = {int(weight): value for weight, value in SPEC["tabular_slot"].items()}
ADVANCES = SPEC["advances"]
MATH_ADVANCES = SPEC["math_advances"]

FAMILY = "Pivot Sans"
FORK_AUTHOR = "Beacon Bay Technologies, Inc."
PROJECT_URL = "https://pivotsans.com"
ORIGINAL_DESIGNER = "Olivia King"
ORIGINAL_DESIGNER_URL = "https://www.oliviaking.com"
VERSION = SPEC["fork_version"]
UPSTREAM_VERSION = "2.004"
RELEASE_DATE = SPEC["release_date"]
RELEASE_YEAR = int(RELEASE_DATE[:4])
RELEASE_OPENTYPE_TIMESTAMP = int(
    datetime.fromisoformat(RELEASE_DATE).replace(tzinfo=timezone.utc).timestamp()
) + 2_082_844_800
WINDOWS_ASCENT = SPEC["windows_ascent"]

ROUND_XY_TO_GRID = 0x0004
OVERLAP_COMPOUND = 0x0400
UNSCALED_COMPONENT_OFFSET = 0x1000


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def verify_upstream_manifest() -> None:
    manifest_path = UPSTREAM / "SHA256SUMS"
    if not manifest_path.is_file():
        raise FileNotFoundError(f"missing upstream manifest: {manifest_path}")

    entries = {}
    for line in manifest_path.read_text().splitlines():
        if not line.strip():
            continue
        checksum, relative = line.split(maxsplit=1)
        entries[relative.removeprefix("*")] = checksum

    required = {
        "InclusiveSans-VariableFont_wght.ttf",
        "InclusiveSans-Italic-VariableFont_wght.ttf",
        "OFL.txt",
        "README.txt",
        *(f"static/{source}" for _, _, source, _ in STATIC_SOURCES),
    }
    if set(entries) != required:
        missing = sorted(required - set(entries))
        extra = sorted(set(entries) - required)
        raise ValueError(
            f"upstream manifest file set mismatch; missing={missing}, extra={extra}"
        )

    mismatches = []
    for relative, expected in sorted(entries.items()):
        path = UPSTREAM / relative
        actual = sha256(path) if path.is_file() else "<missing>"
        if actual != expected:
            mismatches.append(f"{relative}: expected {expected}, got {actual}")
    if mismatches:
        raise ValueError("upstream checksum mismatch:\n  " + "\n  ".join(mismatches))


def stamp_fork_metadata(font: TTFont) -> None:
    """Rename upstream metadata while retaining original authorship credit."""

    name = font["name"]
    for record in name.names:
        value = record.toUnicode()
        value = value.replace("Inclusive Sans", FAMILY).replace(
            "InclusiveSans", "PivotSans"
        )
        record.string = value.encode(record.getEncoding())

    postscript_name = name.getDebugName(6)
    if not postscript_name or not postscript_name.startswith("PivotSans-"):
        raise ValueError(f"unexpected renamed PostScript name: {postscript_name!r}")

    overrides = {
        0: (
            "Copyright 2024 The Inclusive Sans Project Authors "
            "(https://github.com/LivKing/Inclusive-Sans). "
            f"Copyright {RELEASE_YEAR} {FORK_AUTHOR} "
            f"{FAMILY} {VERSION} is a modified version of Inclusive Sans "
            f"{UPSTREAM_VERSION}, released under the SIL Open Font License 1.1."
        ),
        3: f"{VERSION};PIVOT;{postscript_name}",
        5: f"Version {VERSION}",
        8: FORK_AUTHOR,
        9: (
            f"{ORIGINAL_DESIGNER} (original Inclusive Sans design); "
            f"{FORK_AUTHOR} (Pivot Sans fork)"
        ),
        10: (
            "Pivot Sans is a permanent fork of Inclusive Sans 2.004. "
            f"The Pivot Sans fork is authored and maintained by {FORK_AUTHOR} "
            "It adds money-aware tabular figures, composite math symbols, "
            "a disambiguated lowercase-l stylistic set, locale web subsets, "
            "and corrected Windows clipping bounds. "
            "See FONTLOG.txt for details."
        ),
        11: PROJECT_URL,
        12: ORIGINAL_DESIGNER_URL,
    }
    for name_id, value in overrides.items():
        name.setName(value, name_id, 3, 1, 0x409)

    variation_prefix = name.getDebugName(25)
    expected_prefix = "PivotSansItalic" if font["head"].macStyle & 0x02 else "PivotSans"
    if variation_prefix != expected_prefix:
        raise ValueError(
            f"unexpected variation PostScript prefix: {variation_prefix!r}; "
            f"expected {expected_prefix!r}"
        )

    font["head"].fontRevision = float(VERSION)
    font["head"].modified = RELEASE_OPENTYPE_TIMESTAMP
    font["OS/2"].usWinAscent = max(font["OS/2"].usWinAscent, WINDOWS_ASCENT)


def component(
    glyph_name: str,
    x: int = 0,
    y: int = 0,
    transform: tuple[tuple[float, float], tuple[float, float]] | None = None,
) -> GlyphComponent:
    result = GlyphComponent()
    result.glyphName = glyph_name
    result.flags = ROUND_XY_TO_GRID
    if transform is not None:
        # Make the Microsoft/Apple-compatible offset interpretation explicit
        # instead of relying on a rasterizer's default for scaled components.
        result.flags |= UNSCALED_COMPONENT_OFFSET
    result.x, result.y = x, y
    if transform is not None:
        result.transform = transform
    return result


def composite(
    components: list[GlyphComponent], *, overlapping: bool = False
) -> Glyph:
    if overlapping:
        if not components:
            raise ValueError("an overlapping composite requires a component")
        # The OpenType recommendation is to set this on the first component.
        components[0].flags |= OVERLAP_COMPOUND
    glyph = Glyph()
    glyph.numberOfContours = -1
    glyph.components = components
    return glyph


def simple_glyph(coordinates: list[tuple[int, int]]) -> Glyph:
    """Build one unhinted on-curve contour from deterministic coordinates."""

    if len(coordinates) < 3:
        raise ValueError("a simple contour requires at least three points")
    glyph = Glyph()
    glyph.numberOfContours = 1
    glyph.coordinates = GlyphCoordinates(coordinates)
    glyph.endPtsOfContours = [len(coordinates) - 1]
    glyph.flags = bytearray([0x01] * len(coordinates))
    glyph.program = Program()
    return glyph


def ss02_foot_coordinates(font: TTFont) -> list[tuple[int, int]]:
    """Derive a lowercase-l foot from native l geometry and capital-I serif."""

    glyf = font["glyf"]
    l_glyph = glyf["l"]
    if l_glyph.isComposite():
        raise ValueError("expected a simple lowercase l")
    l_coordinates, l_endpoints, _ = l_glyph.getCoordinates(glyf)
    if list(l_endpoints) != [3] or len(l_coordinates) != 4:
        raise ValueError("unexpected lowercase-l topology")

    bottom_left, top_left, top_right, bottom_right = [
        (round(x), round(y)) for x, y in l_coordinates
    ]
    top_y = top_left[1]
    if (
        bottom_left[1] != 0
        or bottom_right[1] != 0
        or top_right[1] != top_y
        or top_y <= 0
    ):
        raise ValueError("unexpected lowercase-l coordinate order")

    i_glyph = glyf["I"]
    if i_glyph.isComposite():
        raise ValueError("expected a simple capital I")
    i_coordinates, _, _ = i_glyph.getCoordinates(glyf)
    lower_serif_levels = sorted(
        {round(y) for _x, y in i_coordinates if 0 < y < top_y / 2}
    )
    if not lower_serif_levels:
        raise ValueError("could not measure the capital-I lower serif")
    foot_height = lower_serif_levels[0]

    right_shear = round(
        (top_right[0] - bottom_right[0]) * foot_height / top_y
    )
    baseline_shear = round(
        (top_left[0] - bottom_left[0]) * foot_height / top_y
    )
    overlap = 10
    start_bottom = bottom_right[0] - overlap
    start_top = bottom_right[0] + right_shear - overlap
    end_bottom = font["hmtx"]["l"][0] - 4
    end_top = end_bottom + baseline_shear
    if start_bottom >= end_bottom or start_top >= end_top:
        raise ValueError("derived lowercase-l foot has invalid geometry")

    return [
        (start_bottom, 0),
        (start_top, foot_height),
        (end_top, foot_height),
        (end_bottom, 0),
    ]


def simple_variations(
    coordinates: dict[int, list[tuple[int, int]]],
    advances: dict[int, int],
) -> list[TupleVariation]:
    """Create 300/700 gvar deltas for a simple glyph with fixed topology."""

    default = coordinates[400]
    default_advance = advances[400]
    if any(len(points) != len(default) for points in coordinates.values()):
        raise ValueError("simple variation topology mismatch")

    def deltas(weight: int):
        point_deltas = [
            (x - default_x, y - default_y)
            for (x, y), (default_x, default_y) in zip(
                coordinates[weight], default
            )
        ]
        return point_deltas + [
            (0, 0),
            (advances[weight] - default_advance, 0),
            (0, 0),
            (0, 0),
        ]

    return [
        TupleVariation({"wght": (-1.0, -1.0, 0.0)}, deltas(300)),
        TupleVariation({"wght": (0.0, 1.0, 1.0)}, deltas(700)),
    ]


def lowercase_l_offset(font: TTFont, source: str) -> tuple[int, int]:
    """Locate the native l outline inside one lowercase-l derivative."""

    if source == "l":
        return (0, 0)

    glyf = font["glyf"]
    source_glyph = glyf[source]
    if source_glyph.isComposite():
        matches = [
            part
            for part in source_glyph.components
            if part.glyphName == "l"
        ]
        if len(matches) != 1:
            raise ValueError(
                f"{source} does not contain exactly one direct l component"
            )
        part = matches[0]
        if hasattr(part, "transform"):
            raise ValueError(f"{source} has a transformed l component")
        return (round(part.x), round(part.y))

    l_coordinates, l_endpoints, _ = glyf["l"].getCoordinates(glyf)
    if list(l_endpoints) != [3]:
        raise ValueError("unexpected lowercase-l topology")
    native_l = [(round(x), round(y)) for x, y in l_coordinates]
    source_coordinates, source_endpoints, _ = source_glyph.getCoordinates(glyf)
    source_points = [(round(x), round(y)) for x, y in source_coordinates]

    matches = []
    start = 0
    for end in source_endpoints:
        contour = source_points[start : end + 1]
        start = end + 1
        if len(contour) != len(native_l):
            continue
        dx = contour[0][0] - native_l[0][0]
        dy = contour[0][1] - native_l[0][1]
        if all(
            (x + dx, y + dy) == point
            for (x, y), point in zip(native_l, contour)
        ):
            matches.append((dx, dy))

    if len(matches) != 1:
        raise ValueError(
            f"{source} does not contain exactly one translated l contour"
        )
    return matches[0]


def ss02_target_components(font: TTFont, source: str) -> list[GlyphComponent]:
    """Expand one native source recipe and append its correctly placed foot."""

    glyf = font["glyf"]
    source_glyph = glyf[source]
    if source_glyph.isComposite():
        parts = [deepcopy(part) for part in source_glyph.components]
    else:
        parts = [component(source)]

    nested = [
        part.glyphName
        for part in parts
        if glyf[part.glyphName].isComposite()
    ]
    if nested:
        raise ValueError(
            f"{source} expansion still contains composite components: "
            f"{nested}"
        )

    parts.append(component(SS02_FOOT, *lowercase_l_offset(font, source)))
    return parts


def append_hvar_row(hvar, delta_min: int, delta_max: int) -> int:
    variation_data = hvar.VarStore.VarData[0]
    if variation_data.VarRegionCount != 2:
        raise ValueError("expected Inclusive Sans negative/positive wght regions")
    if any(delta < -128 or delta > 127 for delta in (delta_min, delta_max)):
        # Inclusive Sans stores both region columns as signed bytes. A wider
        # derived glyph can legitimately need a larger delta; promoting the
        # existing columns to signed shorts preserves every existing item
        # value while making the appended row representable.
        variation_data.NumShorts = variation_data.VarRegionCount
    inner_index = variation_data.ItemCount
    variation_data.Item.append([delta_min, delta_max])
    variation_data.ItemCount += 1
    return inner_index  # outer index is zero


def composite_variations(
    positions: dict[int, list[tuple[int, int]]],
    advances: dict[int, int],
):
    default_positions = positions[400]
    default_advance = advances[400]

    def coordinates(weight: int):
        component_deltas = [
            (x - default_x, y - default_y)
            for (x, y), (default_x, default_y) in zip(
                positions[weight], default_positions
            )
        ]
        advance_delta = advances[weight] - default_advance
        # Component coordinates followed by origin, advance, top-sidebearing,
        # and bottom-sidebearing phantom points.
        return component_deltas + [
            (0, 0),
            (advance_delta, 0),
            (0, 0),
            (0, 0),
        ]

    return [
        TupleVariation({"wght": (-1.0, -1.0, 0.0)}, coordinates(300)),
        TupleVariation({"wght": (0.0, 1.0, 1.0)}, coordinates(700)),
    ]


def math_layout(metrics: dict[str, int]):
    equal_advance = metrics["equal"]
    slash_x = round((equal_advance - metrics["slash"] * 0.65) / 2)
    less_x = round((equal_advance - metrics["less"]) / 2)
    greater_x = round((equal_advance - metrics["greater"]) / 2)
    minus_x = round((equal_advance - metrics["minus"]) / 2)
    tilde_x = round((equal_advance - metrics["asciitilde"]) / 2)
    per_mille_advance = metrics["percent"] + round(metrics["degree"] * 0.85)
    extra_circle_x = metrics["percent"] - 10

    return {
        "notequal": {
            "advance": equal_advance,
            "overlapping": True,
            "components": [
                ("equal", 0, 0, None),
                ("slash", slash_x, 90, ((0.65, 0.0), (0.0, 0.55))),
            ],
        },
        "lessequal": {
            "advance": equal_advance,
            "components": [
                ("less", less_x, 90, None),
                ("minus", minus_x, -220, None),
            ],
        },
        "greaterequal": {
            "advance": equal_advance,
            "components": [
                ("greater", greater_x, 90, None),
                ("minus", minus_x, -220, None),
            ],
        },
        "approxequal": {
            "advance": equal_advance,
            "components": [
                ("asciitilde", tilde_x, 105, None),
                ("asciitilde", tilde_x, -90, None),
            ],
        },
        "perthousand": {
            "advance": per_mille_advance,
            "components": [
                ("percent", 0, 0, None),
                (
                    "degree",
                    extra_circle_x,
                    -430,
                    ((0.90, 0.0), (0.0, 0.98)),
                ),
            ],
        },
    }


def glyph_from_layout(layout) -> Glyph:
    return composite(
        [
            component(glyph_name, x, y, transform)
            for glyph_name, x, y, transform in layout["components"]
        ],
        overlapping=layout.get("overlapping", False),
    )


def add_unicode_mappings(font: TTFont) -> None:
    for table in font["cmap"].tables:
        if not table.isUnicode() or table.format == 14:
            continue
        for glyph_name, codepoint in MATH_CODEPOINTS.items():
            existing = table.cmap.get(codepoint)
            if existing not in {None, glyph_name}:
                raise ValueError(
                    f"U+{codepoint:04X} already maps to unexpected {existing}"
                )
            table.cmap[codepoint] = glyph_name

    best_cmap = font.getBestCmap()
    os2 = font["OS/2"]
    bmp_codepoints = [codepoint for codepoint in best_cmap if codepoint <= 0xFFFF]
    os2.usFirstCharIndex = min(bmp_codepoints)
    os2.usLastCharIndex = max(bmp_codepoints)
    os2.recalcUnicodeRanges(font)


def finish_added_glyphs(font: TTFont, glyph_names: list[str]) -> None:
    font.setGlyphOrder(font["glyf"].glyphOrder)
    for glyph_name in glyph_names:
        glyph = font["glyf"][glyph_name]
        glyph.recalcBounds(font["glyf"])
        advance, _ = font["hmtx"][glyph_name]
        font["hmtx"][glyph_name] = (advance, glyph.xMin)
    font["OS/2"].recalcAvgCharWidth(font)


def add_variable_ss02_glyphs(font: TTFont) -> list[str]:
    """Add the shared variable foot and nine source-preserving alternates."""

    order = set(font.getGlyphOrder())
    collisions = sorted(set(SS02_GLYPHS) & order)
    if collisions:
        raise ValueError(f"upstream already contains ss02 glyphs: {collisions}")
    missing = sorted(set(SS02_L_SOURCES) - order)
    if missing:
        raise ValueError(f"missing lowercase-l source glyphs: {missing}")

    instances = {
        weight: instantiateVariableFont(
            font,
            {"wght": weight},
            inplace=False,
            optimize=False,
        )
        for weight in (300, 400, 700)
    }
    foot_coordinates = {
        weight: ss02_foot_coordinates(instance)
        for weight, instance in instances.items()
    }
    foot_advances = {
        weight: instance["hmtx"]["l"][0] for weight, instance in instances.items()
    }
    source_components = {
        source: {
            weight: ss02_target_components(instance, source)
            for weight, instance in instances.items()
        }
        for source in SS02_L_SOURCES
    }

    glyf, hmtx, gvar = font["glyf"], font["hmtx"], font["gvar"]
    hvar = font["HVAR"].table
    glyf[SS02_FOOT] = simple_glyph(foot_coordinates[400])
    hmtx[SS02_FOOT] = hmtx["l"]
    hvar.AdvWidthMap.mapping[SS02_FOOT] = hvar.AdvWidthMap.mapping["l"]
    gvar.variations[SS02_FOOT] = simple_variations(
        foot_coordinates, foot_advances
    )

    for source, target in SS02_TARGETS.items():
        glyf[target] = composite(
            source_components[source][400],
            overlapping=True,
        )
        hmtx[target] = hmtx[source]
        hvar.AdvWidthMap.mapping[target] = hvar.AdvWidthMap.mapping[source]
        source_advances = {
            weight: instance["hmtx"][source][0]
            for weight, instance in instances.items()
        }
        gvar.variations[target] = composite_variations(
            {
                weight: [
                    (round(part.x), round(part.y))
                    for part in source_components[source][weight]
                ]
                for weight in (300, 400, 700)
            },
            source_advances,
        )

    return SS02_GLYPHS


def add_static_ss02_glyphs(font: TTFont) -> list[str]:
    """Add source-preserving lowercase-l alternates to one official static."""

    order = set(font.getGlyphOrder())
    collisions = sorted(set(SS02_GLYPHS) & order)
    if collisions:
        raise ValueError(f"upstream already contains ss02 glyphs: {collisions}")
    missing = sorted(set(SS02_L_SOURCES) - order)
    if missing:
        raise ValueError(f"missing lowercase-l source glyphs: {missing}")

    glyf, hmtx = font["glyf"], font["hmtx"]
    glyf[SS02_FOOT] = simple_glyph(ss02_foot_coordinates(font))
    hmtx[SS02_FOOT] = hmtx["l"]
    for source, target in SS02_TARGETS.items():
        glyf[target] = composite(
            ss02_target_components(font, source),
            overlapping=True,
        )
        hmtx[target] = hmtx[source]
    return SS02_GLYPHS


def clone_ss02_layout_metadata(font: TTFont) -> None:
    """Give each alternate its source's GDEF, kerning, and mark anchors."""

    gdef = font["GDEF"].table
    class_defs = gdef.GlyphClassDef.classDefs
    for source, target in SS02_TARGETS.items():
        if target in class_defs:
            raise ValueError(f"GDEF already classifies {target}")
        try:
            class_defs[target] = class_defs[source]
        except KeyError as exc:
            raise ValueError(f"GDEF does not classify {source}") from exc

    pair_first_clones = 0
    pair_second_clones = 0
    mark_base_subtables = 0
    for lookup in font["GPOS"].table.LookupList.Lookup:
        if lookup.LookupType == 2:
            for subtable in lookup.SubTable:
                if subtable.Format != 1:
                    continue

                source_pair_sets = dict(
                    zip(subtable.Coverage.glyphs, subtable.PairSet)
                )
                for source, target in SS02_TARGETS.items():
                    if source not in source_pair_sets:
                        continue
                    if target in subtable.Coverage.glyphs:
                        raise ValueError(f"PairPos already covers {target}")
                    subtable.Coverage.glyphs.append(target)
                    subtable.PairSet.append(deepcopy(source_pair_sets[source]))
                    pair_first_clones += 1
                subtable.PairSetCount = len(subtable.PairSet)

                for pair_set in subtable.PairSet:
                    source_records = {
                        record.SecondGlyph: record
                        for record in pair_set.PairValueRecord
                    }
                    for source, target in SS02_TARGETS.items():
                        if source not in source_records:
                            continue
                        if target in source_records:
                            raise ValueError(
                                f"PairPos already has second glyph {target}"
                            )
                        record = deepcopy(source_records[source])
                        record.SecondGlyph = target
                        pair_set.PairValueRecord.append(record)
                        pair_second_clones += 1
                    pair_set.PairValueCount = len(pair_set.PairValueRecord)

        elif lookup.LookupType == 4:
            for subtable in lookup.SubTable:
                base_records = dict(
                    zip(
                        subtable.BaseCoverage.glyphs,
                        subtable.BaseArray.BaseRecord,
                    )
                )
                covered_sources = set(base_records) & set(SS02_TARGETS)
                if not covered_sources:
                    continue
                if covered_sources != set(SS02_TARGETS):
                    raise ValueError(
                        "MarkBase coverage is incomplete for lowercase-l "
                        f"sources: {sorted(covered_sources)}"
                    )
                for source, target in SS02_TARGETS.items():
                    if target in subtable.BaseCoverage.glyphs:
                        raise ValueError(f"MarkBase already covers {target}")
                    subtable.BaseCoverage.glyphs.append(target)
                    subtable.BaseArray.BaseRecord.append(
                        deepcopy(base_records[source])
                    )
                subtable.BaseArray.BaseCount = len(
                    subtable.BaseArray.BaseRecord
                )
                mark_base_subtables += 1

    if pair_first_clones == 0 or pair_second_clones == 0:
        raise ValueError(
            "could not clone lowercase-l PairPos behavior on both sides"
        )
    if mark_base_subtables != 2:
        raise ValueError(
            "expected exactly two lowercase-l MarkBase subtables, found "
            f"{mark_base_subtables}"
        )


def add_variable_overlay(font: TTFont, style: str) -> None:
    order = font.getGlyphOrder()
    glyf, hmtx, gvar = font["glyf"], font["hmtx"], font["gvar"]
    hvar = font["HVAR"].table

    slot_deltas = (SLOT[300] - SLOT[400], SLOT[700] - SLOT[400])
    tabular_varidx = append_hvar_row(hvar, *slot_deltas)

    for source, metric_base in TABULAR_SOURCES.items():
        alternate = f"{source}.tf"
        if alternate in order:
            raise ValueError(f"upstream already contains {alternate}")
        offsets = {
            weight: round(
                (
                    SLOT[weight]
                    - ADVANCES[style][str(weight)][metric_base]
                )
                / 2
            )
            for weight in (300, 400, 700)
        }
        if source != metric_base:
            if hmtx[source][0] != hmtx[metric_base][0]:
                raise ValueError(
                    f"{source} and {metric_base} do not share default advances"
                )
            if (
                hvar.AdvWidthMap.mapping[source]
                != hvar.AdvWidthMap.mapping[metric_base]
            ):
                raise ValueError(
                    f"{source} and {metric_base} do not share HVAR advances"
                )
        glyf[alternate] = composite([component(source, offsets[400], 0)])
        hmtx[alternate] = (SLOT[400], 0)
        hvar.AdvWidthMap.mapping[alternate] = tabular_varidx
        gvar.variations[alternate] = composite_variations(
            {
                weight: [(offsets[weight], 0)]
                for weight in (300, 400, 700)
            },
            {weight: SLOT[weight] for weight in (300, 400, 700)},
        )

    layouts = {
        weight: math_layout(MATH_ADVANCES[style][str(weight)])
        for weight in (300, 400, 700)
    }
    per_mille_advances = {
        weight: layouts[weight]["perthousand"]["advance"]
        for weight in (300, 400, 700)
    }
    per_mille_varidx = append_hvar_row(
        hvar,
        per_mille_advances[300] - per_mille_advances[400],
        per_mille_advances[700] - per_mille_advances[400],
    )
    equal_varidx = hvar.AdvWidthMap.mapping["equal"]

    for glyph_name in MATH_GLYPHS:
        if glyph_name in order:
            raise ValueError(f"upstream already contains {glyph_name}")
        default_layout = layouts[400][glyph_name]
        glyf[glyph_name] = glyph_from_layout(default_layout)
        hmtx[glyph_name] = (default_layout["advance"], 0)
        hvar.AdvWidthMap.mapping[glyph_name] = (
            per_mille_varidx if glyph_name == "perthousand" else equal_varidx
        )
        gvar.variations[glyph_name] = composite_variations(
            {
                weight: [
                    (x, y)
                    for _, x, y, _ in layouts[weight][glyph_name]["components"]
                ]
                for weight in (300, 400, 700)
            },
            {
                weight: layouts[weight][glyph_name]["advance"]
                for weight in (300, 400, 700)
            },
        )

    added_ss02 = add_variable_ss02_glyphs(font)
    add_unicode_mappings(font)
    finish_added_glyphs(
        font, TABULAR_ALTERNATES + MATH_GLYPHS + added_ss02
    )
    clone_ss02_layout_metadata(font)


def add_static_overlay(font: TTFont, style: str, weight: int) -> None:
    order = font.getGlyphOrder()
    glyf, hmtx = font["glyf"], font["hmtx"]
    slot = SLOT[weight]
    added_tabular = []

    for source, metric_base in TABULAR_SOURCES.items():
        if source not in order:
            continue
        alternate = f"{source}.tf"
        if alternate in order:
            raise ValueError(f"upstream already contains {alternate}")
        advance = hmtx[source][0]
        measured = ADVANCES[style][str(weight)][metric_base]
        if advance != measured:
            raise ValueError(
                f"{source} at {weight}: static advance {advance} != "
                f"measured variable advance {measured}"
            )
        glyf[alternate] = composite(
            [component(source, round((slot - advance) / 2), 0)]
        )
        hmtx[alternate] = (slot, 0)
        added_tabular.append(alternate)

    metrics = {
        glyph_name: hmtx[glyph_name][0]
        for glyph_name in (
            "equal",
            "slash",
            "less",
            "greater",
            "minus",
            "asciitilde",
            "percent",
            "degree",
        )
    }
    layouts = math_layout(metrics)
    for glyph_name in MATH_GLYPHS:
        if glyph_name in order:
            raise ValueError(f"upstream already contains {glyph_name}")
        glyf[glyph_name] = glyph_from_layout(layouts[glyph_name])
        hmtx[glyph_name] = (layouts[glyph_name]["advance"], 0)

    added_ss02 = add_static_ss02_glyphs(font)
    add_unicode_mappings(font)
    finish_added_glyphs(font, added_tabular + MATH_GLYPHS + added_ss02)
    clone_ss02_layout_metadata(font)


def add_single_substitution_feature(
    font: TTFont,
    feature_tag: str,
    requested_mapping: dict[str, str],
    *,
    ui_name: str | None = None,
) -> None:
    """Append one sorted, globally available single-substitution feature."""

    gsub = font["GSUB"].table
    existing_tags = [
        record.FeatureTag for record in gsub.FeatureList.FeatureRecord
    ]
    if feature_tag in existing_tags:
        raise ValueError(f"font already contains a {feature_tag} feature")
    if existing_tags != sorted(existing_tags) or (
        existing_tags and existing_tags[-1] > feature_tag
    ):
        raise ValueError(
            f"unexpected GSUB ordering before {feature_tag}: {existing_tags}"
        )

    substitution = otTables.SingleSubst()
    glyphs = set(font.getGlyphOrder())
    substitution.mapping = {
        source: target
        for source, target in requested_mapping.items()
        if source in glyphs and target in glyphs
    }
    if not substitution.mapping:
        raise ValueError(f"{feature_tag} has no available substitutions")

    lookup = otTables.Lookup()
    lookup.LookupType = 1
    lookup.LookupFlag = 0
    lookup.SubTable = [substitution]
    lookup.SubTableCount = 1
    gsub.LookupList.Lookup.append(lookup)
    gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup)
    lookup_index = len(gsub.LookupList.Lookup) - 1

    feature = otTables.Feature()
    if ui_name is None:
        feature.FeatureParams = None
    else:
        if feature_tag != "ss02":
            raise ValueError("UI names are only supported for ss02")
        if any(record.nameID == SS02_UI_NAME_ID for record in font["name"].names):
            raise ValueError(f"name ID {SS02_UI_NAME_ID} is already occupied")
        font["name"].setName(
            ui_name,
            SS02_UI_NAME_ID,
            3,
            1,
            0x409,
        )
        params = otTables.FeatureParamsStylisticSet()
        params.Version = 0
        params.UINameID = SS02_UI_NAME_ID
        feature.FeatureParams = params
    feature.LookupListIndex = [lookup_index]
    feature.LookupCount = 1
    record = otTables.FeatureRecord()
    record.FeatureTag = feature_tag
    record.Feature = feature
    gsub.FeatureList.FeatureRecord.append(record)
    gsub.FeatureList.FeatureCount = len(gsub.FeatureList.FeatureRecord)
    feature_index = len(gsub.FeatureList.FeatureRecord) - 1

    script_tags = {record.ScriptTag for record in gsub.ScriptList.ScriptRecord}
    if script_tags != {"DFLT", "latn"}:
        raise ValueError(f"unexpected upstream GSUB scripts: {sorted(script_tags)}")

    for script_record in gsub.ScriptList.ScriptRecord:
        language_systems = [script_record.Script.DefaultLangSys]
        language_systems.extend(
            record.LangSys for record in script_record.Script.LangSysRecord
        )
        for language_system in language_systems:
            if language_system is None:
                continue
            language_system.FeatureIndex.append(feature_index)
            language_system.FeatureCount = len(language_system.FeatureIndex)


def add_ss02_feature(font: TTFont) -> None:
    add_single_substitution_feature(
        font,
        "ss02",
        SS02_TARGETS,
        ui_name=SS02_UI_NAME,
    )


def add_tnum_feature(font: TTFont) -> None:
    add_single_substitution_feature(
        font,
        "tnum",
        {source: f"{source}.tf" for source in TABULAR_SOURCES},
    )


def save_woff2(ttf_path: Path) -> Path:
    font = TTFont(ttf_path, recalcTimestamp=False)
    font.flavor = "woff2"
    output = ttf_path.with_suffix(".woff2")
    font.save(output)
    return output


def name_static_pin(font: TTFont, style: str, weight: int) -> str:
    italic = style == "italic"
    legacy_family = f"{FAMILY} {weight}"
    legacy_subfamily = "Italic" if italic else "Regular"
    preferred_subfamily = f"Weight {weight}" + (" Italic" if italic else "")
    full_name = legacy_family + (" Italic" if italic else "")
    suffix = f"Wght{weight}" + ("Italic" if italic else "")
    postscript_name = f"PivotSans-{suffix}"
    name = font["name"]
    overrides = {
        1: legacy_family,
        2: legacy_subfamily,
        3: f"{VERSION};PIVOT;{postscript_name}",
        4: full_name,
        5: f"Version {VERSION}",
        6: postscript_name,
        16: FAMILY,
        17: preferred_subfamily,
    }
    for name_id, value in overrides.items():
        name.setName(value, name_id, 3, 1, 0x409)

    stat = font["STAT"].table
    weight_axis_index = next(
        (
            index
            for index, axis in enumerate(stat.DesignAxisRecord.Axis)
            if axis.AxisTag == "wght"
        ),
        None,
    )
    if weight_axis_index is None:
        raise ValueError("static pin STAT table has no wght design axis")
    if any(
        getattr(axis_value, "AxisIndex", None) == weight_axis_index
        for axis_value in stat.AxisValueArray.AxisValue
    ):
        raise ValueError("static pin unexpectedly retains a wght STAT value")
    weight_name_id = name.addName(
        f"Weight {weight}",
        platforms=((3, 1, 0x409),),
        minNameID=256,
    )
    axis_value = otTables.AxisValue()
    axis_value.Format = 1
    axis_value.AxisIndex = weight_axis_index
    axis_value.Flags = 0
    axis_value.ValueNameID = weight_name_id
    axis_value.Value = float(weight)
    stat.AxisValueArray.AxisValue.append(axis_value)
    stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)

    os2 = font["OS/2"]
    os2.usWeightClass = weight
    os2.fsSelection &= ~((1 << 0) | (1 << 5) | (1 << 6))
    os2.fsSelection |= 1 << 0 if italic else 1 << 6
    font["head"].macStyle &= ~0x03
    if italic:
        font["head"].macStyle |= 0x02
    font["head"].modified = RELEASE_OPENTYPE_TIMESTAMP
    return suffix


def prune_static_instance(font: TTFont) -> None:
    """Remove helpers that cannot be reached at a pinned axis position."""

    options = Options()
    options.layout_features = ["*"]
    options.glyph_names = True
    options.symbol_cmap = True
    options.legacy_cmap = True
    options.notdef_glyph = True
    options.notdef_outline = True
    options.recommended_glyphs = True
    options.name_IDs = ["*"]
    options.name_legacy = True
    options.name_languages = ["*"]
    options.recalc_timestamp = False
    options.canonical_order = True
    options.prune_unicode_ranges = True
    options.prune_codepage_ranges = True
    options.recalc_average_width = True
    options.recalc_max_context = True

    codepoints = set(font.getBestCmap())
    subsetter = Subsetter(options=options)
    subsetter.populate(unicodes=codepoints)
    subsetter.subset(font)
    if set(font.getBestCmap()) != codepoints:
        raise ValueError("static pin pruning changed Unicode coverage")
    font["hhea"].recalc(font)


def build_static_pins(variable_paths: dict[str, Path]) -> list[Path]:
    (DIST / "pins").mkdir(parents=True, exist_ok=True)
    outputs = []
    for style, variable_path in variable_paths.items():
        for weight in STATIC_PINS:
            variable = TTFont(variable_path, recalcTimestamp=False)
            instance = instantiateVariableFont(
                variable,
                {"wght": weight},
                inplace=False,
                updateFontNames=False,
            )
            prune_static_instance(instance)
            suffix = name_static_pin(instance, style, weight)
            output = DIST / "pins" / f"PivotSans-{suffix}.ttf"
            instance.save(output)
            outputs.append(output)
            print(f"built {output.relative_to(ROOT)}")
    return outputs


def clear_generated_fonts() -> None:
    for directory in (DIST, DIST / "static", DIST / "pins", DIST / "subsets"):
        directory.mkdir(parents=True, exist_ok=True)
        for pattern in ("*.ttf", "*.woff2"):
            for path in directory.glob(pattern):
                path.unlink()


def write_release_checksums(paths: list[Path]) -> None:
    assets = [
        DIST / "pivot-sans.css",
        DIST / "pivot-sans-subsets.css",
        DIST / "specimen.html",
        DIST / "qa-visual.png",
    ]
    missing = [path for path in assets if not path.is_file()]
    if missing:
        raise FileNotFoundError(
            "missing release assets: "
            + ", ".join(str(path.relative_to(ROOT)) for path in missing)
        )
    checksum_paths = sorted(set(paths + assets))
    lines = [
        f"{sha256(path)}  {path.relative_to(DIST).as_posix()}"
        for path in checksum_paths
    ]
    (DIST / "SHA256SUMS").write_text("\n".join(lines) + "\n")


def build() -> None:
    verify_upstream_manifest()
    clear_generated_fonts()
    built_ttf = []
    variable_paths = {}

    for style, source in VARIABLE_SOURCES.items():
        font = TTFont(source, recalcTimestamp=False)
        add_variable_overlay(font, style)
        add_ss02_feature(font)
        add_tnum_feature(font)
        stamp_fork_metadata(font)
        output = DIST / VARIABLE_OUTPUTS[style]
        font.save(output)
        variable_paths[style] = output
        built_ttf.append(output)
        print(f"built {output.relative_to(ROOT)} ({len(font.getGlyphOrder())} glyphs)")

    for style, weight, source_name, output_name in STATIC_SOURCES:
        source = UPSTREAM / "static" / source_name
        font = TTFont(source, recalcTimestamp=False)
        add_static_overlay(font, style, weight)
        add_ss02_feature(font)
        add_tnum_feature(font)
        stamp_fork_metadata(font)
        output = DIST / "static" / output_name
        font.save(output)
        built_ttf.append(output)
        print(f"built {output.relative_to(ROOT)} ({len(font.getGlyphOrder())} glyphs)")

    built_ttf.extend(build_static_pins(variable_paths))
    built_woff2 = [save_woff2(path) for path in built_ttf]
    subset_woff2, subset_css = build_subsets(variable_paths, DIST)
    write_release_checksums(
        built_ttf + built_woff2 + subset_woff2 + [subset_css]
    )
    print(
        f"\n{len(built_ttf)} TTF + {len(built_woff2)} full/pinned WOFF2 + "
        f"{len(subset_woff2)} subset WOFF2 and SHA256SUMS -> {DIST}"
    )


if __name__ == "__main__":
    build()
