"""Check the serialized product pair."""
from fontTools.pens.basePen import NullPen
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import otTables as ot

from build_config import FONT_REVISION, FONT_VERSION, identity
from layout import lang_systems, references
from windows_policy import read as read_windows, errors as windows_errors


def average_width_errors(font):
    total = count = 0
    for name in font.getGlyphOrder():
        width = font['hmtx'][name][0]
        if width:
            total += width
            count += 1
    if not count:
        return ['xAvgCharWidth: no nonzero advances']
    quotient, remainder = divmod(total, count)
    expected = quotient + int(2 * remainder >= count)
    if not 0 <= expected <= 32767 or font['OS/2'].xAvgCharWidth != expected:
        return [
            'xAvgCharWidth differs from the independently calculated average'
        ]
    return []


def windows_ansi_errors(font):
    cmap = font.getBestCmap() or {}
    names = set(font.getGlyphOrder())
    if any(
        cmap.get(cp) in (None, '.notdef') or cmap[cp] not in names
        for cp in range(0x20, 0x7F)
    ):
        return ['ASCII cmap missing valid glyphs']
    if not font['OS/2'].ulCodePageRange1 & 1:
        return ['Windows ANSI bit 0 missing']
    return []


def _glyph_references(node, names, seen=None):
    """Check references in the layout objects emitted by our generators."""
    if seen is None:
        seen = set()
    if node is None or isinstance(node, (str, int, float, bool, bytes)):
        return
    if id(node) in seen:
        return
    seen.add(id(node))
    used = []
    if isinstance(node, ot.Coverage):
        used.extend(node.glyphs)
    elif isinstance(node, ot.ClassDef):
        used.extend(node.classDefs)
    elif isinstance(node, ot.SingleSubst):
        used.extend(node.mapping)
        used.extend(node.mapping.values())
    elif isinstance(node, (ot.AlternateSubst, ot.MultipleSubst)):
        mapping = node.alternates if isinstance(
            node, ot.AlternateSubst
        ) else node.mapping
        used.extend(mapping)
        used.extend(g for values in mapping.values() for g in values)
    elif isinstance(node, ot.LigatureSubst):
        used.extend(node.ligatures)
    elif isinstance(node, ot.Ligature):
        used.extend(node.Component)
        used.append(node.LigGlyph)
    elif isinstance(node, ot.PairValueRecord):
        used.append(node.SecondGlyph)
    elif isinstance(node, ot.GlyphPartRecord):
        used.append(node.glyph)
    elif isinstance(node, ot.MathGlyphVariantRecord):
        used.append(node.VariantGlyph)
    if any(name not in names for name in used):
        raise ValueError('layout references a missing glyph')
    if isinstance(node, dict):
        children = node.values()
    elif isinstance(node, (list, tuple)):
        children = node
    else:
        children = vars(node).values() if hasattr(node, '__dict__') else ()
    for child in children:
        _glyph_references(child, names, seen)


def _layout(font):
    names = set(font.getGlyphOrder())
    for tag in ('MATH', 'GSUB', 'GPOS', 'GDEF'):
        table = font[tag].table
        _glyph_references(table, names)
        if tag not in ('GSUB', 'GPOS'):
            continue
        lookups = table.LookupList.Lookup
        features = table.FeatureList.FeatureRecord
        if (
            table.LookupList.LookupCount != len(lookups)
            or table.FeatureList.FeatureCount != len(features)
        ):
            raise ValueError(tag + ' count mismatch')
        for lookup in lookups:
            if lookup.SubTableCount != len(lookup.SubTable):
                raise ValueError(tag + ' subtable count mismatch')
        for feature in features:
            indices = feature.Feature.LookupListIndex
            if (
                feature.Feature.LookupCount != len(indices)
                or any(not 0 <= i < len(lookups) for i in indices)
            ):
                raise ValueError(
                    tag + ' feature lookup reference out of range'
                )
        for record in references(table):
            if not 0 <= record.LookupListIndex < len(lookups):
                raise ValueError(
                    tag + ' context lookup reference out of range'
                )
        for lang in lang_systems(table):
            if (
                lang.FeatureCount != len(lang.FeatureIndex)
                or any(not 0 <= i < len(features) for i in lang.FeatureIndex)
            ):
                raise ValueError(
                    tag + ' language feature reference out of range'
                )
            if (
                lang.ReqFeatureIndex != 65535
                and not 0 <= lang.ReqFeatureIndex < len(features)
            ):
                raise ValueError(
                    tag + ' required feature reference out of range'
                )
    math = font['MATH'].table
    variants = math.MathVariants
    for axis in ('Vert', 'Horiz'):
        coverage = getattr(variants, axis + 'GlyphCoverage')
        constructions = getattr(variants, axis + 'GlyphConstruction')
        if (
            len(coverage.glyphs) != len(constructions)
            or getattr(variants, axis + 'GlyphCount') != len(constructions)
        ):
            raise ValueError('MATH construction count mismatch')
        for construction in constructions:
            if construction.VariantCount != len(
                construction.MathGlyphVariantRecord
            ):
                raise ValueError('MATH variant count mismatch')
            assembly = construction.GlyphAssembly
            if assembly and assembly.PartCount != len(assembly.PartRecords):
                raise ValueError('MATH assembly count mismatch')


def _one(font, edition):
    required = (
        'head', 'maxp', 'hmtx', 'hhea', 'OS/2', 'name', 'cmap', 'MATH', 'GSUB',
        'GPOS', 'GDEF'
    )
    missing = set(required) - set(font.keys())
    if missing:
        raise ValueError('missing final tables: ' + ', '.join(sorted(missing)))
    order = font.getGlyphOrder()
    if (
        not order
        or len(order) != len(set(order))
        or font['maxp'].numGlyphs != len(order)
    ):
        raise ValueError('final glyph count/order mismatch')
    if set(font['hmtx'].metrics) != set(order):
        raise ValueError('hmtx glyph set mismatch')
    names = set(order)
    for subtable in font['cmap'].tables:
        if any(
            name not in names or name == '.notdef'
            for name in subtable.cmap.values()
        ):
            raise ValueError('invalid final cmap reference')
    cmap = font.getBestCmap() or {}
    required_chars = (0x20, 0x2061, 0x2062, 0x2063, 0x221A, 0x222B)
    if any(cp not in cmap for cp in required_chars):
        raise ValueError('required product character missing')
    failures = windows_ansi_errors(font) + average_width_errors(font)
    if failures:
        raise ValueError('; '.join(failures))
    family, basename, unique = identity(edition)
    expected_names = {
        1: family, 2: 'Regular', 3: unique, 4: family, 5: FONT_VERSION,
        6: basename, 16: family, 17: 'Regular'
    }
    for key, expected in expected_names.items():
        values = {n.toUnicode() for n in font['name'].names if n.nameID == key}
        if values != {expected}:
            raise ValueError('final name/edition mismatch: ' + str(key))
    if (
        font['head'].fontRevision != FONT_REVISION
        or font['head'].unitsPerEm != 1000
    ):
        raise ValueError('final revision/UPM mismatch')
    os2, hhea = font['OS/2'], font['hhea']
    typo = [os2.sTypoAscender, os2.sTypoDescender, os2.sTypoLineGap]
    horizontal_header = [hhea.ascent, hhea.descent, hhea.lineGap]
    if typo != [806, -194, 200] or horizontal_header != [806, -194, 200]:
        raise ValueError('final vertical metrics mismatch')
    _layout(font)


def check_pair(
    otf_path, ttf_path, edition, windows_policy=None, input_manifest=None
):
    with TTFont(otf_path) as otf, TTFont(ttf_path) as ttf:
        if 'CFF ' not in otf or 'glyf' not in ttf:
            raise ValueError('expected final CFF OTF and quadratic TTF')
        for font in (otf, ttf):
            _one(font, edition)
        a, b = otf.getGlyphOrder(), ttf.getGlyphOrder()
        if len(a) != len(b):
            raise ValueError('OTF/TTF glyph count mismatch')

        def cmap_gids(font):
            return [
                (
                    t.platformID, t.platEncID, t.language, t.format,
                    {
                        cp: font.getGlyphID(name)
                        for cp, name in t.cmap.items()
                    }
                )
                for t in font['cmap'].tables
            ]
        if cmap_gids(otf) != cmap_gids(ttf):
            raise ValueError('OTF/TTF cmap GID mismatch')
        if [otf['hmtx'][n] for n in a] != [ttf['hmtx'][n] for n in b]:
            raise ValueError('OTF/TTF GID-ordered hmtx mismatch')
        for tag in ('MATH', 'GSUB', 'GPOS', 'GDEF'):
            if otf.reader[tag] != ttf.reader[tag]:
                raise ValueError('OTF/TTF layout bytes differ: ' + tag)
        cff = otf['CFF '].cff
        if len(cff.topDictIndex) != 1:
            raise ValueError('requires singleton CFF1')
        top = cff.topDictIndex[0]
        if (
            list(top.charset) != a
            or len(top.CharStrings.charStrings) != len(a)
        ):
            raise ValueError('CFF GID/charstring count mismatch')
        for name in a:
            char = top.CharStrings[name]
            char.draw(NullPen())
            if char.width != otf['hmtx'][name][0]:
                raise ValueError(
                    'decoded CFF width differs from hmtx: ' + name
                )
        if windows_policy:
            obj = read_windows(windows_policy)
            failures = windows_errors(
                ttf, obj, 'source', font_path=ttf_path, edition=edition,
                manifest=input_manifest, source_path=otf_path
            )
            if failures:
                raise ValueError('; '.join(failures))
        elif [
            ttf['OS/2'].usWinAscent, ttf['OS/2'].usWinDescent
        ] != [otf['OS/2'].usWinAscent, otf['OS/2'].usWinDescent]:
            raise ValueError('legacy OTF/TTF Windows metrics mismatch')
