"""Private native entry: sources, geometry, and features in order."""

import hashlib
import json
import os
import sys
from contextlib import ExitStack, contextmanager
from pathlib import Path

# FontForge script runners do not uniformly add the
# script's own directory.
sys.path.insert(0, str(Path(__file__).resolve().parent))  # noqa: E402

import constructions
import declarations
import donors
import glyphs as sources
import native_math as features
import outlines
import substitution
from config import CFF_HINT_VALUES, identity
from diagnostics import write_contract
from windows_metrics import capture


@contextmanager
def stage(label):
    try:
        yield
    except (Exception, SystemExit) as exc:
        raise ValueError("native " + label + ": " + str(exc)) from exc


def read_snapshot(path, edition, coverage, condition):
    with Path(path).open(encoding="utf-8") as stream:
        snapshot = json.load(stream)
    run = snapshot["build_identity"]
    if (
        run["edition"] != edition
        or run["donor_coverage"] != coverage
        or run["upm"] != 1000
        or snapshot["large_variants"] != condition
    ):
        raise ValueError(
            (
                "native inputs differ from the requested "
                "edition/coverage/condition"
            )
        )
    return snapshot


def write_payloads(
    font,
    work,
    edition,
    snapshot,
    imported,
    constants,
    min_overlap,
    requested,
    numeric_requested,
    variant_sources,
):
    """Bind payloads to the serialized native font."""
    min_overlap = int(
        getattr(font.math, "MinConnectorOverlap", min_overlap) or 0
    )
    math_expected = write_contract(
        work / "math-constants.json",
        {
            "upm": font.em,
            "constants": dict(constants),
            "MinConnectorOverlap": min_overlap,
        },
    )
    inventory_expected = write_contract(
        work / "large-variant-build.json",
        {
            "policy": "source",
            "edition": edition,
            "input_hashes": snapshot["inputs"],
            "numeric_requests": numeric_requested,
            "requests": [
                dict(tag=tag, slot=slot) for tag, slot in sorted(requested)
            ],
            "variant_copies": [
                dict(glyph=glyph, tag=tag, slot=slot)
                for glyph, (tag, slot) in sorted(variant_sources.items())
            ],
            "imports": [
                {"tag": tag, "slot": slot, "glyph": name}
                for (tag, slot), name in sorted(imported.items())
                if name in font
            ],
        },
    )
    return {
        "math-constants.json": math_expected,
        "large-variant-build.json": inventory_expected,
    }


def save_native(font, work, basename, snapshot, payloads):
    path = work / (basename + ".otf")
    for name, value in CFF_HINT_VALUES.items():
        font.private[name] = value
    font.generate(str(path), flags=("opentype",))
    write_contract(
        work / "native-result.json",
        {
            "schema": "mtp2otf7-native-result/2",
            "identity": snapshot["build_identity"],
            "native_otf_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
            "payloads": payloads,
        },
    )


def generate(
    root,
    work,
    snapshot_path,
    edition,
    regular,
    bold,
    coverage,
    policy,
    *,
    purpose="font",
    metrics_path=None,
):
    if purpose not in ("font", "windows") or policy != (
        "source" if purpose == "font" else "legacy"
    ):
        raise ValueError("invalid FontForge purpose/condition")
    if purpose == "windows" and metrics_path is None:
        raise ValueError("compatibility metrics output absent")
    coverage = donors.public_mode(coverage)
    root, work = Path(root), Path(work)
    pl_dir = work / "pl"
    pl_dir.mkdir(parents=True, exist_ok=True)
    with stage("input snapshot"):
        snapshot = read_snapshot(snapshot_path, edition, coverage, policy)
        rules = declarations.policy_from_data(snapshot)
        limits = constructions.VariantLimits(policy)
        spec = donors.load_spec() if coverage == "minimal" else None
    with ExitStack() as owners:
        with stage("source faces"):
            faces = owners.enter_context(
                outlines.source_faces(root / "mtpro2", edition, snapshot)
            )
        with sources.native_font() as font:
            family, basename, _ = identity(edition)
            font.encoding = "UnicodeFull"
            font.em = 1000
            font.familyname = family
            font.fontname = basename
            font.fullname = family
            font.weight = "Regular"
            font.copyright = (
                "Copyright (C) Publish or Perish, Inc. 2005.  "
                "Outlines from the MathTime(TM) Professional II Type1 fonts. "
                "Unofficial OpenType MATH conversion for the "
                "licensee's own use; "
                "redistribution of this font file is not permitted."
            )
            glyphs = sources.GlyphSources(font, faces, rules, snapshot["plan"])
            with stage("core source glyphs"):
                operators = sources.import_core(glyphs, limits)
            with stage("Roman donor glyphs"):
                dotless_i, dotless_j = sources.import_roman(
                    glyphs, regular, bold
                )
            with stage("source alphabets and aliases"):
                alphabet = sources.import_bold_and_aliases(
                    glyphs, dotless_i, dotless_j
                )
            with stage("large variants"):
                variants = constructions.build_variants(
                    glyphs, limits, operators, snapshot["geometry"]
                )
            with stage("accents and dots"):
                accents = constructions.build_accents_and_dots(glyphs)
            with stage("arrows and braces"):
                constructions.build_arrows_and_braces(
                    glyphs, limits, variants, accents
                )
            with stage("initial MATH"):
                constants, min_overlap = features.install_initial_math(
                    glyphs, accents
                )
            with stage("variant and connector geometry"):
                constructions.install_variant_geometry(
                    glyphs, limits, variants
                )
            with stage("optical source glyphs"):
                optical = sources.import_optical_forms(glyphs, accents)
            with stage("source prime geometry"):
                prime_accents = sources.build_primes(glyphs, optical)
            with stage("optical routes"):
                substitution.install_optical_routes(glyphs, optical, alphabet)
            with stage("remaining Roman donor"):
                sources.import_remaining_roman(
                    glyphs, regular, edition, coverage, spec
                )
            with stage("private codepoints"):
                features.assign_private_codepoints(glyphs)
            with stage("native metrics"):
                bounds = features.assign_metrics(
                    glyphs, cap_height=constants["FlattenedAccentBaseHeight"]
                )
            # Exactly the same native glyph set/scan;
            # no source-only payloads.
            if purpose == "windows":
                with stage("compatibility result"):
                    result = capture(font, snapshot, bounds)
                    result["numeric_requests"] = {
                        tag: sorted(face[1].fontdimen.used)
                        for tag, face in faces.items()
                    }
                    result["source_requests"] = [
                        dict(tag=tag, slot=slot)
                        for tag, slot in sorted(glyphs.source_requests)
                    ]
                    write_contract(metrics_path, result)
                return
            with stage("source spacing and GDEF"):
                features.finish_source_features(
                    glyphs,
                    limits,
                    accents,
                    variants,
                    prime_accents=prime_accents,
                )
            with stage("source payloads"):
                payloads = write_payloads(
                    font,
                    work,
                    edition,
                    snapshot,
                    glyphs.imported,
                    constants,
                    min_overlap,
                    glyphs.source_requests,
                    {
                        tag: sorted(face[1].fontdimen.used)
                        for tag, face in faces.items()
                    },
                    glyphs.variant_sources,
                )
            with stage("native range and donor checks"):
                features.check_native_ranges(glyphs)
                sources.check_minimal_supply(glyphs, spec, alphabet)
            with stage("serialization and handoff"):
                save_native(font, work, basename, snapshot, payloads)


def main(argv=None):
    import argparse

    p = argparse.ArgumentParser(description="Private FontForge backend entry")
    p.add_argument("--purpose", choices=("font", "windows"), required=True)
    p.add_argument("--metrics-output", type=Path)
    args = p.parse_args(argv)
    generate(
        Path(__file__).resolve().parents[1],
        Path(os.environ["MTP2_BUILD_DIR"]),
        Path(os.environ["MTP2_SOURCE_POLICY_JSON"]),
        os.environ["MTP2_EDITION"],
        Path(os.environ["MTP2_ROMAN_REGULAR"]),
        Path(os.environ["MTP2_ROMAN_BOLD"]),
        os.environ[donors.INTERNAL_MODE],
        os.environ["MTP2_LARGE_VARIANTS"],
        purpose=args.purpose,
        metrics_path=args.metrics_output,
    )


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        raise SystemExit(130) from None
    except Exception as exc:
        print("error: " + str(exc), file=sys.stderr)
        raise SystemExit(1) from None
