"""Build Talven Text from a pinned OFL Glyphs source and explicit outline edits."""
from pathlib import Path
import hashlib
import json
import shutil

import glyphsLib
from fontmake.font_project import FontProject
from fontTools.pens.recordingPen import DecomposingRecordingPen
from fontTools.ttLib import TTFont
from fontTools.varLib.instancer import instantiateVariableFont

ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'source' / 'Upstream-AtkinsonHyperlegibleNext.glyphs'
EXPECTED = 'ddf54acf72c41905e327270abf66a0d6eb479455be103cff4511746f43e628d0'
FAMILY = 'Talven Text'
COPYRIGHT = ('Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors '
             '(https://github.com/googlefonts/atkinson-hyperlegible-next). '
             'Copyright 2026 The Talven Text Project Authors. '
             'Modified version, not endorsed by the upstream authors.')


def lift(y):
    """Raise x-height from 496 to 520, retaining baseline, cap height and extents."""
    if 0 <= y <= 496:
        return y * 520 / 496
    if 496 < y < 668:
        return y + 24 * (668 - y) / 172
    return y


def draw(glyph, commands, width):
    glyph.clearContours()
    glyph.clearComponents()
    pen = glyph.getPen()
    for method, args in commands:
        getattr(pen, method)(*args)
    glyph.width = width


def M(x, y): return ('moveTo', ((x, y),))
def L(x, y): return ('lineTo', ((x, y),))
def C(a, b, c): return ('curveTo', (a, b, c))
def Z(): return ('closePath', ())


def ellipse(cx, cy, rx, ry, clockwise=True):
    k = 0.55228475
    pts = [M(cx, cy-ry), C((cx-rx*k,cy-ry),(cx-rx,cy-ry*k),(cx-rx,cy)),
           C((cx-rx,cy+ry*k),(cx-rx*k,cy+ry),(cx,cy+ry)),
           C((cx+rx*k,cy+ry),(cx+rx,cy+ry*k),(cx+rx,cy)),
           C((cx+rx,cy-ry*k),(cx+rx*k,cy-ry),(cx,cy-ry)), Z()]
    if clockwise:
        return pts
    return [M(cx,cy-ry), C((cx+rx*k,cy-ry),(cx+rx,cy-ry*k),(cx+rx,cy)),
            C((cx+rx,cy+ry*k),(cx+rx*k,cy+ry),(cx,cy+ry)),
            C((cx-rx*k,cy+ry),(cx-rx,cy+ry*k),(cx-rx,cy)),
            C((cx-rx,cy-ry*k),(cx-rx*k,cy-ry),(cx,cy-ry)), Z()]


def custom_outlines(ufo, stem):
    """Seven new outlines. Corresponding masters have the same path topology."""
    s = stem
    h = .88 * s
    # c: wider mouth, rounded bowl, diagonal terminals.
    draw(ufo['c'], [M(457,105), L(457-.88*s,105+.47*s),
        C((357,67+h),(324,-12+h),(271,-12+h)),
        C((150,-12+h),(36+s,127),(36+s,260)),
        C((36+s,393),(158,532-h),(274,532-h)),
        C((326,532-h),(365,471-h),(452-.84*s,419-.48*s)), L(452,419),
        C((407,493),(353,532),(274,532)), C((125,532),(36,423),(36,260)),
        C((36,99),(124,-12),(271,-12)), C((352,-12),(416,39),(457,105)), Z()], 497)
    # e: raised crossbar and open lower terminal. Keep an enclosed upper counter.
    draw(ufo['e'], [M(480,99), L(480-.86*s,99+.44*s),
        C((366,63+h),(333,-12+h),(279,-12+h)),
        C((193,-12+h),(36+s,134),(36+s,247)), L(515,247),
        C((526,407),(432,532),(278,532)), C((130,532),(36,422),(36,260)),
        C((36,98),(130,-12),(279,-12)), C((366,-12),(438,31),(480,99)), Z(),
        M(36+s,247+.86*s), C((54+s,451-h),(157,532-h),(278,532-h)),
        C((387,532-h),(515-s,414),(515-s,247+.86*s)), L(36+s,247+.86*s), Z()], 550)
    # l: a longer curved foot, distinct from both the barred I and flagged 1.
    draw(ufo['l'], [M(68,708), L(68,133), C((68,28),(95,-5),(188,-5)),
        L(268,-5), L(268,h-5), L(208,h-5),
        C((68+s,h-5),(68+s,92),(68+s,137)), L(68+s,708), Z()], 300)
    # I: short, slightly flared horizontal bars, with a plain middle stem.
    width = 398
    x = width/2
    b = .80*s
    draw(ufo['I'], [M(49,0),L(349,0),L(349,b),L(x+s/2,b-4),
        L(x+s/2,668-b+4),L(349,668-b),L(349,668),L(49,668),
        L(49,668-b),L(x-s/2,668-b+4),L(x-s/2,b-4),L(49,b),Z()], width)
    # 1: diagonal flag and full baseline foot.
    x = 224
    draw(ufo['one'], [M(64,0),L(388,0),L(388,h),L(x+s/2,h),
        L(x+s/2,668),L(x-s/2+12,668),L(51,542),L(51+.50*s,542-.76*s),
        L(x-s/2,668-1.55*s),L(x-s/2,h),L(64,h),Z()], 441)
    # r: short curved shoulder. The open space separates rn from m.
    draw(ufo['r'], [M(72,0),L(72+s,0),L(72+s,281),
        C((72+s,395),(223,532-h),(316,532-h)),L(345,532-h),L(345,532),
        C((251,541),(203,490),(72+s,424)),L(72+s*.73,520),L(72,520),Z()], 370)
    # 0: oval with a central dot, replacing the source's diagonal slash.
    draw(ufo['zero'], ellipse(306,335,246,347)+
        ellipse(306,335,246-s,347-h,False)+ellipse(306,335,.41*s,.41*s), 612)
    # Match precomposed accents and dynamically positioned combining marks.
    for name, anchor_name, x, y in [
        ('c','bottom',271,0), ('e','ogonek',400,22),
        ('I','ogonek',349,10), ('l','center',120,365),
    ]:
        for anchor in ufo[name].anchors:
            if anchor.name == anchor_name: anchor.x=x;anchor.y=y


def build():
    if hashlib.sha256(SOURCE.read_bytes()).hexdigest() != EXPECTED:
        raise RuntimeError('The upstream source hash does not match the pinned source.')
    source = glyphsLib.GSFont(str(SOURCE))
    ds = glyphsLib.to_designspace(source, family_name=FAMILY)
    masters_dir = ROOT/'source'/'masters'
    masters_dir.mkdir(exist_ok=True)
    for source_desc in ds.sources:
        ufo = source_desc.font
        # New contours are inserted before decomposition so accents and alternate
        # forms inherit them. Existing nonspacing marks keep a zero advance.
        stem = next(p.x for p in ufo['H'].contours[0].points if p.y == 0 and p.x > 100) - ufo['H'].contours[0].points[0].x
        # The lower-case stem is slightly lighter than the capital stem.
        lower_stem = stem-10 if stem < 120 else stem-11
        # Save all original outlines before any coordinate transform.
        # Custom outlines are drawn in the final 520-unit x-height design.
        recordings = {}
        for glyph in ufo:
            pen = DecomposingRecordingPen(ufo)
            glyph.draw(pen)
            recordings[glyph.name] = pen.value
        for glyph in ufo:
            glyph.clearContours(); glyph.clearComponents()
            pen = glyph.getPen()
            for op, pts in recordings[glyph.name]:
                getattr(pen,op)(*[(p[0],lift(p[1])) if p is not None else None for p in pts])
            for anchor in glyph.anchors:
                anchor.y = lift(anchor.y)
        # Install custom bases, then reconstruct affected composite derivatives
        # from the original source component records in a second pass below.
        custom_outlines(ufo, lower_stem)
        # Rebuild composed accents and tabular zero from the edited base outlines.
        original = glyphsLib.to_designspace(source, family_name=FAMILY).sources[ds.sources.index(source_desc)].font
        affected = {'c','e','l','I','one','r','zero'}
        def uses_custom(name, seen=None):
            if name in affected: return True
            seen = set() if seen is None else seen
            if name in seen: return False
            seen.add(name)
            return any(uses_custom(c.baseGlyph,seen) for c in original[name].components)
        # Draw components recursively, preserving transformed source mark contours.
        from fontTools.pens.transformPen import TransformPen
        def draw_updated(name, pen):
            old = original[name]
            if not old.components or name in affected:
                ufo[name].draw(pen)
                return
            # These source composites have no own contours.
            if old.contours:
                raise RuntimeError(f'Mixed composite needs explicit handling: {name}')
            for comp in old.components:
                transform = comp.transformation
                # Source accents sit on the original baseline. Their own points
                # and anchors already received the vertical profile adjustment.
                draw_updated(comp.baseGlyph, TransformPen(pen, transform))
        for name in original.keys():
            if name in affected or not original[name].components or not uses_custom(name): continue
            pen = DecomposingRecordingPen(ufo)
            draw_updated(name,pen)
            g = ufo[name]
            g.clearContours();g.clearComponents();pen.replay(g.getPen())
            # Preserve original tabular advances. Other accent variants follow base width.
            bases = [c.baseGlyph for c in original[name].components if original[c.baseGlyph].width>0]
            if len(bases)==1 and not name.endswith('.tf'):
                base = bases[0]
                g.width = ufo[base].width + original[name].width - original[base].width
        # These four source glyphs contain merged outlines rather than components.
        # Reuse the new base and attach the existing mark at an explicit anchor.
        for name, base, mark, anchor in [
            ('ccedilla','c','cedillacomb',(271,0)),
            ('eogonek','e','ogonekcomb',(400,22)),
            ('Iogonek','I','ogonekcomb',(349,10)),
            ('lslash','l','slashshortcomb',(120,365)),
        ]:
            g=ufo[name]
            g.clearContours();g.clearComponents()
            ufo[base].draw(g.getPen())
            mark_anchor=next(a for a in ufo[mark].anchors if a.name.startswith('_'))
            ufo[mark].draw(TransformPen(g.getPen(),
                (1,0,0,1,anchor[0]-mark_anchor.x,anchor[1]-mark_anchor.y)))
            g.width=ufo[base].width
        # Tabular 1 uses the same new shape centered in the existing digit advance.
        if 'one.tf' in ufo:
            g=ufo['one.tf']; advance=g.width
            g.clearContours();g.clearComponents()
            ufo['one'].draw(TransformPen(g.getPen(),(1,0,0,1,(advance-ufo['one'].width)/2,0)))
        # Proportional zero changed width; re-center its tabular form explicitly.
        if 'zero.tf' in ufo:
            g=ufo['zero.tf'];advance=g.width
            g.clearContours();g.clearComponents()
            ufo['zero'].draw(TransformPen(g.getPen(),(1,0,0,1,(advance-ufo['zero'].width)/2,0)))
        for glyph in ufo:
            if glyph.name == 'lcaron':
                from fontTools.pens.boundsPen import BoundsPen
                bounds_pen=BoundsPen(ufo);glyph.draw(bounds_pen)
                glyph.width=max(glyph.width,bounds_pen.bounds[2]+30)
            if glyph.width > 0:
                shift=5
                for contour in glyph.contours:
                    for p in contour.points: p.x += shift
                for anchor in glyph.anchors: anchor.x += shift
                glyph.width += 10
            if glyph.name in ('space','nbspace'): glyph.width += 14
        info=ufo.info
        info.familyName=FAMILY;info.styleMapFamilyName=FAMILY
        info.versionMajor=0;info.versionMinor=101
        info.copyright=COPYRIGHT
        info.openTypeNameDesigner='Talven Text Project; upstream designers credited in FONTLOG.md'
        info.openTypeNameManufacturer='Talven Text Project'
        info.openTypeNameDesignerURL=None; info.openTypeNameManufacturerURL=None
        info.openTypeOS2VendorID='NONE'
        info.openTypeNameDescription='Research-informed prototype. Custom derivative of Atkinson Hyperlegible Next. No measured reading benefit is claimed.'
        info.openTypeNameSampleText='A quiet place to read. Il1 O0 rn m.'
        info.xHeight=520
        info.postscriptBlueValues=[-15,0,520,535,668,683,708,723]
        info.postscriptUnderlineThickness=56
        info.openTypeOS2TypoAscender=1000;info.openTypeOS2TypoDescender=-330
        info.openTypeHheaAscender=1000;info.openTypeHheaDescender=-330
        info.openTypeOS2WinAscent=1050;info.openTypeOS2WinDescent=430
        # Remove stale source hint zones. The generated variable TTF is unhinted.
        for key in list(ufo.lib):
            if 'TTFStems' in key or 'TTFZones' in key: del ufo.lib[key]
        source_desc.name=f'{FAMILY} {info.styleName}'
        path=masters_dir/f'TalvenText-{info.styleName}.ufo'
        if path.exists(): shutil.rmtree(path)
        ufo.save(path)
        source_desc.path=str(path)
    for instance in ds.instances:
        instance.familyName=FAMILY
        instance.postScriptFontName=None
        instance.styleMapFamilyName=None
    # Rename any source-specific variable-font filename without changing axis mapping.
    for vf in ds.variableFonts:
        vf.name='TalvenText'; vf.filename='TalvenText-Full.ttf'
    ds_path=ROOT/'source'/'TalvenText.designspace'
    ds.write(ds_path)
    full=ROOT/'source'/'TalvenText-Full.ttf'
    FontProject().run_from_designspace(str(ds_path),output=['variable'],output_path=str(full),
        use_production_names=True, remove_overlaps=False)
    font=TTFont(full)
    font=instantiateVariableFont(font,{'wght':(300,400,700)},inplace=False)
    # Set a fresh family identity in the actual shipped binary.
    names={0:COPYRIGHT,1:FAMILY,2:'Regular',3:'0.101;NONE;TalvenText-Regular',
           4:FAMILY+' Regular',5:'Version 0.101',6:'TalvenText-Regular',16:FAMILY,17:'Regular',
           25:'TalvenText'}
    for name_id,value in names.items():
        font['name'].removeNames(nameID=name_id)
        font['name'].setName(value,name_id,3,1,0x409)
    from fontTools.ttLib.tables._f_v_a_r import NamedInstance
    font['fvar'].instances=[]
    for weight,style in [(300,'Light'),(400,'Regular'),(500,'Medium'),(600,'Semibold'),(700,'Bold')]:
        instance=NamedInstance()
        instance.subfamilyNameID=font['name'].addName(style,platforms=((3,1,0x409),))
        instance.coordinates={'wght':weight}
        instance.postscriptNameID=0xffff
        font['fvar'].instances.append(instance)
    # Use a compact STAT table for the shipping weight range.
    from fontTools.otlLib.builder import buildStatTable
    buildStatTable(font,[{'tag':'wght','name':'Weight','values':[
        {'value':300,'name':'Light'}, {'value':400,'name':'Regular','flags':2,'linkedValue':700},
        {'value':500,'name':'Medium'}, {'value':600,'name':'Semibold'}, {'value':700,'name':'Bold'}]}])
    font['name'].removeNames(platformID=1)
    # Keep every encoded character and all reachable layout glyphs, pruning
    # decomposed source helper glyphs that can no longer be reached.
    from fontTools import subset
    options=subset.Options()
    options.layout_features=['*'];options.name_IDs=['*']
    options.name_languages=['*'];options.glyph_names=True;options.notdef_outline=True
    # Axis restriction can leave invariant glyphs out of the in-memory gvar
    # dictionary. The subsetter expects an entry for each retained glyph.
    for glyph_name in font.getGlyphOrder():
        if glyph_name not in font['gvar'].variations:
            font['gvar'].variations[glyph_name]=[]
    subsetter=subset.Subsetter(options=options)
    subsetter.populate(unicodes=font.getBestCmap().keys())
    subsetter.subset(font)
    # Standard smart dropout control for an otherwise unhinted font.
    from fontTools.ttLib import newTable
    from fontTools.ttLib.tables.ttProgram import Program
    font['prep']=newTable('prep');font['prep'].program=Program()
    font['prep'].program.fromBytecode(bytes.fromhex('b8 01 ff 85 b0 04 8d'))
    font['gasp']=newTable('gasp');font['gasp'].gaspRange={65535:15}
    font['head'].fontRevision=.101
    fonts=ROOT/'fonts';fonts.mkdir(exist_ok=True)
    variable_path=fonts/'TalvenText-Variable.ttf';font.save(variable_path)
    web=TTFont(variable_path);web.flavor='woff2';web.save(fonts/'TalvenText-Variable.woff2')
    for weight,style in [(300,'Light'),(400,'Regular'),(500,'Medium'),(600,'Semibold'),(700,'Bold')]:
        static=instantiateVariableFont(TTFont(variable_path),{'wght':weight},inplace=True)
        legacy=FAMILY if weight in (400,700) else f'{FAMILY} {style}'
        for nid,value in {1:legacy,2:style if weight in (400,700) else 'Regular',
             3:f'0.101;NONE;TalvenText-{style}',4:f'{FAMILY} {style}',6:f'TalvenText-{style}',16:FAMILY,17:style}.items():
            static['name'].removeNames(nameID=nid);static['name'].setName(value,nid,3,1,0x409)
        static['OS/2'].usWeightClass=weight
        static['OS/2'].fsSelection &= ~((1<<5)|(1<<6))
        static['head'].macStyle &= ~1
        if weight==700:
            static['OS/2'].fsSelection|=1<<5;static['head'].macStyle|=1
        else:static['OS/2'].fsSelection|=1<<6
        static.save(fonts/f'TalvenText-{style}.ttf')
    manifest={'family':FAMILY,'version':'0.101','unitsPerEm':1000,'xHeight':520,'capHeight':668,
        'weightRange':[300,700],'defaultWeight':400,'glyphs':len(font.getGlyphOrder()),
        'encodedCodepoints':len(font.getBestCmap()),'customBases':['c','e','l','I','one','r','zero'],
        'upstreamCommit':'7925f50f649b3813257faf2f4c0b381011f434f1','upstreamSHA256':EXPECTED,
        'files':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(fonts.iterdir())}}
    (ROOT/'font-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
    full.unlink()
    print(json.dumps(manifest,indent=2))

if __name__=='__main__':
    build()
