#!/usr/bin/env python3
"""Check OTF/TTF layout by GID and table bytes, allowing curve approximation.

Audit metric-induced origin shifts separately from outline shape.
"""
import sys
from fontTools.ttLib import TTFont
from fontTools.pens.boundsPen import BoundsPen

# Font-grid allowances for this converter, not general TrueType validity rules.
CFF_LSB_TOLERANCE = 1.0
TT_ORIGIN_TOLERANCE = 2.0


def verify_outline_origins(cff_font, tt_font):
    """Check CFF sidebearings and TrueType origin shifts within conversion tolerances."""
    cff_set = cff_font.getGlyphSet()
    tt_set = tt_font.getGlyphSet()
    errors = []
    max_cff_error = max_tt_shift = 0.0
    for gid, (cn, tn) in enumerate(zip(cff_font.getGlyphOrder(),
                                       tt_font.getGlyphOrder())):
        cp = BoundsPen(cff_set)
        cff_set[cn].draw(cp)
        raw = BoundsPen(tt_set)
        glyph = tt_font['glyf'][tn]
        glyph.draw(raw, tt_font['glyf'])
        drawn = BoundsPen(tt_set)
        tt_set[tn].draw(drawn)
        label = f'GID {gid} ({cn})'
        if cp.bounds is None or raw.bounds is None or drawn.bounds is None:
            if not (cp.bounds is None and raw.bounds is None and drawn.bounds is None):
                errors.append(f'{label}: empty-outline coverage differs')
            elif cff_font['hmtx'][cn][1] or tt_font['hmtx'][tn][1]:
                errors.append(f'{label}: empty outline has nonzero sidebearing')
            continue
        cff_error = abs(cff_font['hmtx'][cn][1] - cp.bounds[0])
        max_cff_error = max(max_cff_error, cff_error)
        if cff_error > CFF_LSB_TOLERANCE:
            errors.append(f'{label}: CFF LSB differs from outline xMin by {cff_error:g}')
        shift = tt_font['hmtx'][tn][1] - glyph.xMin
        max_tt_shift = max(max_tt_shift, abs(shift))
        if abs(shift) > TT_ORIGIN_TOLERANCE:
            errors.append(f'{label}: TrueType metrics shift the raw outline by {shift:g}')
        expected = (raw.bounds[0] + shift, raw.bounds[1],
                    raw.bounds[2] + shift, raw.bounds[3])
        if any(abs(x - y) > 1e-6 for x, y in zip(drawn.bounds, expected)):
            errors.append(f'{label}: unexpected raw/metric-adjusted drawing relation')
    if errors:
        raise SystemExit('OTF/TTF origin contract failed:\n  ' + '\n  '.join(errors[:8]))
    return max_cff_error, max_tt_shift


def cmap_gid_map(font):
    order = font.getGlyphOrder()
    gid = {g: i for i, g in enumerate(order)}
    out = {}
    for table in font['cmap'].tables:
        if not table.isUnicode():
            continue
        for cp, g in table.cmap.items():
            out[cp] = gid[g]
    return out


def hmtx_by_gid(font):
    order = font.getGlyphOrder()
    return [font['hmtx'].metrics[g] for g in order]


def main(otf_path, ttf_path):
    a = TTFont(otf_path)
    b = TTFont(ttf_path)
    if len(a.getGlyphOrder()) != len(b.getGlyphOrder()):
        raise SystemExit('OTF/TTF glyph count differs')
    if cmap_gid_map(a) != cmap_gid_map(b):
        raise SystemExit('OTF/TTF Unicode cmap -> glyph ID mapping differs')
    if hmtx_by_gid(a) != hmtx_by_gid(b):
        raise SystemExit('OTF/TTF advance/sidebearing metrics differ')
    # These tables reference glyph IDs. With an unchanged glyph order they
    # must compile identically across CFF and glyf outline formats.
    for tag in ('MATH', 'GSUB', 'GPOS', 'GDEF'):
        if (tag in a) != (tag in b):
            raise SystemExit(f'OTF/TTF table presence differs: {tag}')
        if tag in a and a.getTableData(tag) != b.getTableData(tag):
            raise SystemExit(f'OTF/TTF table data differs: {tag}')
    if 'CFF ' not in a or 'glyf' in a:
        raise SystemExit('OTF is not the expected CFF build')
    if 'glyf' not in b or 'CFF ' in b:
        raise SystemExit('TTF is not the expected glyf build')
    verify_outline_origins(a, b)


if __name__ == '__main__':
    if len(sys.argv) != 3:
        raise SystemExit('usage: verify_otf_ttf.py INPUT.otf INPUT.ttf')
    main(sys.argv[1], sys.argv[2])
