# Copyright (c) 2026 Simon Rubinstein-Salzedo.
# Distributed under the LaTeX Project Public License, version 1.3c or later.
# Maintenance status: maintained. Current Maintainer: Simon Rubinstein-Salzedo.
# License: https://www.latex-project.org/lppl.txt
# See README.md for the list of files and attribution.
import fontforge
import re
import subprocess
from pathlib import Path

# --- Global font metrics (TFM designsize = 10pt; EM=1000) ---
EM   = 1000
LSB_REL, RSB_REL = 100, 100   # default bearings for relations
LSB_BIN, RSB_BIN =  90,  90   # for binary operators
LSB_ORD, RSB_ORD =  60,  60   # for ordinary symbols

# Reference dimensions from Computer Modern at 10pt.
# Heights/depths (em units)
GT_HT,   GT_DP   = 539,  39   # '>'
UP_HT,   UP_DP   =  694,  194 # \uparrow
DOWN_HT, DOWN_DP =  694,  194 # \downarrow
CIRC_HT, CIRC_DP =  694,  194 # \bigcirc
# Widths (em units)
PLUS_W           =  778       # '+' advance width

UP_W = 500
DUP_W = 611
STEP_W = DUP_W - UP_W
TRIPLE_W = DUP_W + STEP_W
QUAD_W   = DUP_W + 2*STEP_W

MINUS_HT, MINUS_DP = 583, 83
MINUS_CENTER = 0.5*(MINUS_HT - MINUS_DP)

# SVG sources and private-use Unicode codepoints.
GLYPHS = [
  (0xE100, "combgames-symbols-0.svg",   "tripleup", ("both", UP_HT,   UP_DP, TRIPLE_W, (LSB_ORD, RSB_ORD))),
  (0xE101, "combgames-symbols-1.svg", "tripledown", ("both", UP_HT,   UP_DP, TRIPLE_W, (LSB_ORD, RSB_ORD))),
  (0xE102, "combgames-symbols-2.svg",       "quadup", ("both", UP_HT,   UP_DP, QUAD_W, (LSB_ORD, RSB_ORD))),
  (0xE103, "combgames-symbols-3.svg",       "quaddown", ("both", UP_HT,   UP_DP, QUAD_W, (LSB_ORD, RSB_ORD))),
  (0xE104, "combgames-symbols-15.svg",       "gtfuzzy", ("htdp", GT_HT,   GT_DP, (LSB_REL,RSB_REL))),
  (0xE105, "combgames-symbols-16.svg",       "ltfuzzy", ("htdp", GT_HT,   GT_DP, (LSB_REL,RSB_REL))),
  (0xE106, "combgames-symbols-17.svg",       "fuzzy", ("htdp", GT_HT,   GT_DP, (LSB_REL,RSB_REL))),
  (0xE107, "combgames-symbols-30.svg",       "tinysymbol", ("width",  PLUS_W,       (LSB_BIN, RSB_BIN))),
  (0xE108, "combgames-symbols-31.svg", "minysymbol",
    ("width-center", PLUS_W, (LSB_BIN, RSB_BIN), MINUS_CENTER)),
  (0xE109, "combgames-symbols-40.svg",       "upsum", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE110, "combgames-symbols-41.svg",       "downsum", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE111, "combgames-symbols-50.svg",       "ko", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE112, "combgames-symbols-51.svg",       "Ko", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE113, "combgames-symbols-52.svg",       "kobar", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE114, "combgames-symbols-53.svg",       "Kobar", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE115, "combgames-symbols-70.svg",       "farstar", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE116, "combgames-symbols-80.svg",       "loony", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
  (0xE117, "combgames-symbols-81.svg",       "sunny", ("htdp", UP_HT,   UP_DP, (LSB_BIN, RSB_BIN))),
]

# --- Helpers ---
def norm_to_htdp(g, ht_em, dp_em, lsb, rsb):
    """Uniformly scale so top=+ht_em, bottom=-dp_em; set bearings/advance width."""
    xmin, ymin, xmax, ymax = g.boundingBox()
    gh = max(1, ymax - ymin)
    s  = float(ht_em + dp_em) / gh
    dx = -xmin*s
    dy = -ymin*s - dp_em
    g.transform((s,0,0,s, dx,dy))
    g.left_side_bearing, g.right_side_bearing = lsb, rsb
    g.width = lsb + int((xmax - xmin)*s) + rsb
    g.round()

def norm_to_width(g, adv_em, lsb, rsb, lock_htdp=None, lock_center=None):
    # uniform scale to target advance width
    xmin, ymin, xmax, ymax = g.boundingBox()
    gw = max(1, xmax - xmin)
    inner = max(1, adv_em - lsb - rsb)
    s  = float(inner) / gw
    dx = -xmin*s
    dy = -ymin*s
    g.transform((s,0,0,s, dx,dy))

    g.left_side_bearing, g.right_side_bearing = lsb, rsb
    g.width = adv_em

    # either lock by (HT,DP) or by vertical center (in em units)
    if lock_htdp is not None:
        HT, DP = lock_htdp
        xmin, ymin, xmax, ymax = g.boundingBox()
        g.transform((1,0,0,1, 0, -ymin - DP))      # bottom → −DP
    elif lock_center is not None:
        xmin, ymin, xmax, ymax = g.boundingBox()
        cur_center = 0.5*(ymax + ymin)
        g.transform((1,0,0,1, 0, lock_center - cur_center))
    g.round()


def norm_to_htdp_and_width(g, ht_em, dp_em, adv_em, lsb, rsb):
    """Anisotropic: hit both total height and advance width exactly."""
    xmin, ymin, xmax, ymax = g.boundingBox()
    gw = max(1, xmax - xmin)
    gh = max(1, ymax - ymin)
    sx = float(max(1, adv_em - lsb - rsb)) / gw
    sy = float(ht_em + dp_em) / gh
    dx = -xmin*sx
    dy = -ymin*sy - dp_em
    g.transform((sx,0,0,sy, dx,dy))
    g.left_side_bearing, g.right_side_bearing = lsb, rsb
    g.width = adv_em
    g.round()

def norm_overlay_zero(g):
    """Zero-width overlay glyph (e.g., bars)."""
    g.left_side_bearing = g.right_side_bearing = 0
    g.width = 0
    g.round()

# --- Build ---
f = fontforge.font()
f.encoding   = "UnicodeFull"
f.em         = EM
f.ascent     = 700
f.descent    = 300
f.fontname = "CombgamesSymbols"
f.familyname = "Combgames Symbols"
f.fullname = "Combgames Symbols"
f.copyright = "Copyright (c) 2026 Simon Rubinstein-Salzedo. Adapted from combgames by Raymond Chen, David Wolfe and Aaron N. Siegel. Licensed under LPPL 1.3c or later."

for cp, svg, name, pol in GLYPHS:
    g = f.createChar(cp, name)
    g.importOutlines(svg)
    kind = pol[0]

    if kind == "htdp":
        _, HT, DP, (lsb, rsb) = pol
        norm_to_htdp(g, HT, DP, lsb, rsb)
        if name == 'sunny':
            # Circular glyph: preserve its advance despite SVG rounding noise.
            g.width = lsb + HT + DP + rsb

    elif kind == "width":
        # ("width", W, (lsb,rsb))  OR  ("width", W, (lsb,rsb), (HT,DP))
        if len(pol) == 3:
            _, W, (lsb, rsb) = pol
            norm_to_width(g, W, lsb, rsb)
        else:
            _, W, (lsb, rsb), (HT, DP) = pol
            norm_to_width(g, W, lsb, rsb, lock_htdp=(HT, DP))

    elif kind == "width-center":
        # ("width-center", W, (lsb,rsb), center_em)
        _, W, (lsb, rsb), center = pol
        norm_to_width(g, W, lsb, rsb, lock_center=center)

    elif kind == "both":
        _, HT, DP, W, (lsb, rsb) = pol
        norm_to_htdp_and_width(g, HT, DP, W, lsb, rsb)

    elif kind == "overlay0":
        norm_overlay_zero(g)

    else:
        raise ValueError(f"Unknown policy for {name}: {pol}")


# Type 1 uses the explicit combgames-symbols encoding and needs no extra glyph.
f.generate("combgames-symbols.pfb")
# Temporary input for otftotfm: preserve pdfLaTeX spacing parameters.
f.generate("combgames-symbols-metrics.otf")
# fontspec probes a normal space when loading the OpenType font.
# Add it after the symbol glyphs so their names, codepoints and outlines stay fixed.
space = f.createChar(0x20, "space")
space.width = 250
f.generate("combgames-symbols.otf")
print("Wrote combgames-symbols.otf")

# otftotfm computes the TeX metrics. Use our stable encoding filename in the
# runtime map after checking that its glyph order matches the generated one.
subprocess.run([
    'otftotfm', '--x-height=font', '-e', 'combgames-symbols.enc',
    '-n', 'combgames-symbols', '--map-file=combgames-symbols.map',
    'combgames-symbols-metrics.otf'
], check=True)
mapping = Path('combgames-symbols.map')
text = mapping.read_text()
match = re.search(r'<\[([^\s]+)', text)
if not match:
    raise RuntimeError('No encoding file found in generated map')
generated = Path(match.group(1))
def slots(text):
    text = re.sub(r'%[^\n]*', '', text)
    return re.findall(r'/([^\s\[\]]+)', text.split('[', 1)[1].split(']', 1)[0])
if slots(generated.read_text()) != slots(Path('combgames-symbols.enc').read_text()):
    raise RuntimeError('Generated encoding differs from the supplied encoding')
mapping.write_text('combgames-symbols CombgamesSymbols '
    '"CombgamesSymbolsEncoding ReEncodeFont" '
    '<[combgames-symbols.enc <combgames-symbols.pfb\n')
print('Wrote combgames-symbols.tfm and combgames-symbols.map')
