# Copyright 2013 Google, Inc. All Rights Reserved.
#
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader

from fontTools import ttLib, cffLib
from fontTools.misc.psCharStrings import T2WidthExtractor
from fontTools.ttLib.tables.DefaultTable import DefaultTable
from fontTools.merge.base import add_method, mergeObjects
from fontTools.merge.cmap import computeMegaCmap
from fontTools.merge.util import *
import logging


log = logging.getLogger("fontTools.merge")


ttLib.getTableClass("maxp").mergeMap = {
    "*": max,
    "tableTag": equal,
    "tableVersion": equal,
    "numGlyphs": sum,
    "maxStorage": first,
    "maxFunctionDefs": first,
    "maxInstructionDefs": first,
    # TODO When we correctly merge hinting data, update these values:
    # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
}

headFlagsMergeBitMap = {
    "size": 16,
    "*": bitwise_or,
    1: bitwise_and,  # Baseline at y = 0
    2: bitwise_and,  # lsb at x = 0
    3: bitwise_and,  # Force ppem to integer values. FIXME?
    5: bitwise_and,  # Font is vertical
    6: lambda bit: 0,  # Always set to zero
    11: bitwise_and,  # Font data is 'lossless'
    13: bitwise_and,  # Optimized for ClearType
    14: bitwise_and,  # Last resort font. FIXME? equal or first may be better
    15: lambda bit: 0,  # Always set to zero
}

ttLib.getTableClass("head").mergeMap = {
    "tableTag": equal,
    "tableVersion": max,
    "fontRevision": max,
    "checkSumAdjustment": lambda lst: 0,  # We need *something* here
    "magicNumber": equal,
    "flags": mergeBits(headFlagsMergeBitMap),
    "unitsPerEm": equal,
    "created": current_time,
    "modified": current_time,
    "xMin": min,
    "yMin": min,
    "xMax": max,
    "yMax": max,
    "macStyle": first,
    "lowestRecPPEM": max,
    "fontDirectionHint": lambda lst: 2,
    "indexToLocFormat": first,
    "glyphDataFormat": equal,
}

ttLib.getTableClass("hhea").mergeMap = {
    "*": equal,
    "tableTag": equal,
    "tableVersion": max,
    "ascent": max,
    "descent": min,
    "lineGap": max,
    "advanceWidthMax": max,
    "minLeftSideBearing": min,
    "minRightSideBearing": min,
    "xMaxExtent": max,
    "caretSlopeRise": first,
    "caretSlopeRun": first,
    "caretOffset": first,
    "numberOfHMetrics": recalculate,
}

ttLib.getTableClass("vhea").mergeMap = {
    "*": equal,
    "tableTag": equal,
    "tableVersion": max,
    "ascent": max,
    "descent": min,
    "lineGap": max,
    "advanceHeightMax": max,
    "minTopSideBearing": min,
    "minBottomSideBearing": min,
    "yMaxExtent": max,
    "caretSlopeRise": first,
    "caretSlopeRun": first,
    "caretOffset": first,
    "numberOfVMetrics": recalculate,
}

os2FsTypeMergeBitMap = {
    "size": 16,
    "*": lambda bit: 0,
    1: bitwise_or,  # no embedding permitted
    2: bitwise_and,  # allow previewing and printing documents
    3: bitwise_and,  # allow editing documents
    8: bitwise_or,  # no subsetting permitted
    9: bitwise_or,  # no embedding of outlines permitted
}


def mergeOs2FsType(lst):
    lst = list(lst)
    if all(item == 0 for item in lst):
        return 0

    # Compute least restrictive logic for each fsType value
    for i in range(len(lst)):
        # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
        if lst[i] & 0x000C:
            lst[i] &= ~0x0002
        # set bit 2 (allow previewing) if bit 3 is set (allow editing)
        elif lst[i] & 0x0008:
            lst[i] |= 0x0004
        # set bits 2 and 3 if everything is allowed
        elif lst[i] == 0:
            lst[i] = 0x000C

    fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
    # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
    if fsType & 0x0002:
        fsType &= ~0x000C
    return fsType


ttLib.getTableClass("OS/2").mergeMap = {
    "*": first,
    "tableTag": equal,
    "version": max,
    "xAvgCharWidth": first,  # Will be recalculated at the end on the merged font
    "fsType": mergeOs2FsType,  # Will be overwritten
    "panose": first,  # FIXME: should really be the first Latin font
    "ulUnicodeRange1": bitwise_or,
    "ulUnicodeRange2": bitwise_or,
    "ulUnicodeRange3": bitwise_or,
    "ulUnicodeRange4": bitwise_or,
    "fsFirstCharIndex": min,
    "fsLastCharIndex": max,
    "sTypoAscender": max,
    "sTypoDescender": min,
    "sTypoLineGap": max,
    "usWinAscent": max,
    "usWinDescent": max,
    # Version 1
    "ulCodePageRange1": onlyExisting(bitwise_or),
    "ulCodePageRange2": onlyExisting(bitwise_or),
    # Version 2, 3, 4
    "sxHeight": onlyExisting(max),
    "sCapHeight": onlyExisting(max),
    "usDefaultChar": onlyExisting(first),
    "usBreakChar": onlyExisting(first),
    "usMaxContext": onlyExisting(max),
    # version 5
    "usLowerOpticalPointSize": onlyExisting(min),
    "usUpperOpticalPointSize": onlyExisting(max),
}


@add_method(ttLib.getTableClass("OS/2"))
def merge(self, m, tables):
    DefaultTable.merge(self, m, tables)
    if self.version < 2:
        # bits 8 and 9 are reserved and should be set to zero
        self.fsType &= ~0x0300
    if self.version >= 3:
        # Only one of bits 1, 2, and 3 may be set. We already take
        # care of bit 1 implications in mergeOs2FsType. So unset
        # bit 2 if bit 3 is already set.
        if self.fsType & 0x0008:
            self.fsType &= ~0x0004
    return self


ttLib.getTableClass("post").mergeMap = {
    "*": first,
    "tableTag": equal,
    "formatType": max,
    "isFixedPitch": min,
    "minMemType42": max,
    "maxMemType42": lambda lst: 0,
    "minMemType1": max,
    "maxMemType1": lambda lst: 0,
    "mapping": onlyExisting(sumDicts),
    "extraNames": lambda lst: [],
}

ttLib.getTableClass("vmtx").mergeMap = ttLib.getTableClass("hmtx").mergeMap = {
    "tableTag": equal,
    "metrics": sumDicts,
}

ttLib.getTableClass("name").mergeMap = {
    "tableTag": equal,
    "names": first,  # FIXME? Does mixing name records make sense?
}

ttLib.getTableClass("loca").mergeMap = {
    "*": recalculate,
    "tableTag": equal,
}

ttLib.getTableClass("glyf").mergeMap = {
    "tableTag": equal,
    "glyphs": sumDicts,
    "glyphOrder": sumLists,
    "axisTags": equal,
}


@add_method(ttLib.getTableClass("glyf"))
def merge(self, m, tables):
    for i, table in enumerate(tables):
        for g in table.glyphs.values():
            if i:
                # Drop hints for all but first font, since
                # we don't map functions / CVT values.
                g.removeHinting()
            # Expand composite glyphs to load their
            # composite glyph names.
            if g.isComposite() or g.isVarComposite():
                g.expand(table)
    return DefaultTable.merge(self, m, tables)


ttLib.getTableClass("prep").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("fpgm").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("cvt ").mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass("gasp").mergeMap = lambda self, lst: first(
    lst
)  # FIXME? Appears irreconcilable


@add_method(ttLib.getTableClass("CFF "))
def merge(self, m, tables):
    if any(hasattr(table.cff[0], "FDSelect") for table in tables):
        raise NotImplementedError("Merging CID-keyed CFF tables is not supported yet")

    for table in tables:
        table.cff.desubroutinize()

    newcff = tables[0]
    newfont = newcff.cff[0]
    private = newfont.Private
    newDefaultWidthX, newNominalWidthX = private.defaultWidthX, private.nominalWidthX
    storedNamesStrings = []
    glyphOrderStrings = []
    glyphOrder = set(newfont.getGlyphOrder())

    for name in newfont.strings.strings:
        if name not in glyphOrder:
            storedNamesStrings.append(name)
        else:
            glyphOrderStrings.append(name)

    chrset = list(newfont.charset)
    newcs = newfont.CharStrings
    log.debug("FONT 0 CharStrings: %d.", len(newcs))

    for i, table in enumerate(tables[1:], start=1):
        font = table.cff[0]
        defaultWidthX, nominalWidthX = (
            font.Private.defaultWidthX,
            font.Private.nominalWidthX,
        )
        widthsDiffer = (
            defaultWidthX != newDefaultWidthX or nominalWidthX != newNominalWidthX
        )
        font.Private = private
        fontGlyphOrder = set(font.getGlyphOrder())
        for name in font.strings.strings:
            if name in fontGlyphOrder:
                glyphOrderStrings.append(name)
        cs = font.CharStrings
        gs = table.cff.GlobalSubrs
        log.debug("Font %d CharStrings: %d.", i, len(cs))
        chrset.extend(font.charset)
        if newcs.charStringsAreIndexed:
            for i, name in enumerate(cs.charStrings, start=len(newcs)):
                newcs.charStrings[name] = i
                newcs.charStringsIndex.items.append(None)
        for name in cs.charStrings:
            if widthsDiffer:
                c = cs[name]
                defaultWidthXToken = object()
                extractor = T2WidthExtractor([], [], nominalWidthX, defaultWidthXToken)
                extractor.execute(c)
                width = extractor.width
                if width is not defaultWidthXToken:
                    c.program.pop(0)
                else:
                    width = defaultWidthX
                if width != newDefaultWidthX:
                    c.program.insert(0, width - newNominalWidthX)
            newcs[name] = cs[name]

    newfont.charset = chrset
    newfont.numGlyphs = len(chrset)
    newfont.strings.strings = glyphOrderStrings + storedNamesStrings

    return newcff


@add_method(ttLib.getTableClass("cmap"))
def merge(self, m, tables):
    # TODO Handle format=14.
    if not hasattr(m, "cmap"):
        computeMegaCmap(m, tables)
    cmap = m.cmap

    cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF}
    self.tables = []
    module = ttLib.getTableModule("cmap")
    if len(cmapBmpOnly) != len(cmap):
        # format-12 required.
        cmapTable = module.cmap_classes[12](12)
        cmapTable.platformID = 3
        cmapTable.platEncID = 10
        cmapTable.language = 0
        cmapTable.cmap = cmap
        self.tables.append(cmapTable)
    # always create format-4
    cmapTable = module.cmap_classes[4](4)
    cmapTable.platformID = 3
    cmapTable.platEncID = 1
    cmapTable.language = 0
    cmapTable.cmap = cmapBmpOnly
    # ordered by platform then encoding
    self.tables.insert(0, cmapTable)
    self.tableVersion = 0
    self.numSubTables = len(self.tables)
    return self