#!/usr/bin/env python
'OpenType analysis for GDL conversion'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2012 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'

import re, traceback, logging
from fontTools.ttLib.tables import otTables

def compress_strings(strings) :
    '''If we replace one column in the string with different lists, can we reduce the number
       of strings? Each string is a tuple of the string and a single value that will be put into
       a class as well when list compression occurs'''
    maxlen = max(map(lambda x: len(x[0]), strings))
    scores = []
    for r in range(maxlen) :
        allchars = {}
        count = 0
        for s in strings :
            if r >= len(s[0]) : continue
            c = tuple(s[0][0:r] + (s[0][r+1:] if r < len(s[0]) - 1 else []))
            if c in allchars :
                allchars[c] += 1
            else :
                allchars[c] = 0
            count += 1
        scores.append((max(allchars.values()), len(allchars), count))
    best = maxlen
    bestr = 0
    for r in range(maxlen) :
        score = maxlen - (scores[r][2] - scores[r][1])
        if score < best :
            best = score
            bestr = r
    numstrings = len(strings)
    i = 0
    allchars = {}
    while i < len(strings) :
        s = strings[i][0]
        if bestr >= len(s) :
            i += 1
            continue
        c = tuple(s[0:bestr] + (s[bestr+1:] if bestr < len(s) - 1 else []))
        if c in allchars :
            allchars[c][1].append(s[bestr])
            allchars[c][2].append(strings[i][1])
            strings.pop(i)
        else :
            allchars[c] = [i, [s[bestr]], [strings[i][1]]]
            i += 1
    for v in allchars.values() :
        if len(set(v[1])) != 1 :    # if all values in the list identical, don't output list
            strings[v[0]][0][bestr] = v[1]
        if len(v[2]) > 1 :          # don't need a list if length 1
            strings[v[0]][1] = v[2]
    return strings

def make_rule(left, right = None, before = None, after = None) :
    res = " ".join(map(lambda x: x or "_", left))
    if right :
        res += " > " + " ".join(map(lambda x: x or "_", right))
    if before or after :
        res += " / "
        if before : res += " ".join(map(lambda x: x or 'ANY', before))
        res += " " + "_ " * len(left) + " "
        if after : res += " ".join(map(lambda x: x or 'ANY', after))
    res += ";"
    return res

def add_class_classes(font, name, ctable) :
    vals = {}
    for k, v in ctable.classDefs.items() :
        if v not in vals : vals[v] = []
        vals[v].append(k)
    numk = max(vals.keys())
    res = [None] * (numk + 1)
    for k, v in vals.items() :
        if len(v) > 1 :
            res[k] = font.alias(name+"{}".format(k))
            font.addClass(res[k], map(font.glyph, v))
        else :
            res[k] = font.glyph(v[0]).GDLName()
    return res

vrgdlmap = {
    'XPlacement' : 'shift.x',
    'YPlacement' : 'shift.y',
    'XAdvance' : 'advance'
}
def valuerectogdl(vr) :
    res = "{"
    for k, v in vrgdlmap.items() :
        if hasattr(vr, k) :
            res += "{}={}; ".format(v, getattr(vr, k))
    res = res[:-1] + "}"
    if len(res) == 1 : return ""
    return res

def _add_method(*clazzes):
    """Returns a decorator function that adds a new method to one or
    more classes."""
    def wrapper(method):
        for c in clazzes:
            assert c.__name__ != 'DefaultTable', \
                    'Oops, table class not found.'
            assert not hasattr(c, method.__name__), \
                    "Oops, class '%s' has method '%s'." % (c.__name__,
                                                           method.__name__)
            setattr(c, method.__name__, method)
        return None
    return wrapper

@_add_method(otTables.Lookup)
def process(self, font, index) :
    for i, s in enumerate(self.SubTable) :
        if hasattr(s, 'process') :
            s.process(font, index + "_{}".format(i))
        else :
            logging.warning("No processing of {} {}_{}".format(str(s), index, i))

@_add_method(otTables.LookupList)
def process(self, font) :
    for i, s in enumerate(self.Lookup) :
        s.process(font, str(i))

@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
def process(self, font, index) :
    x = self.ExtSubTable
    if hasattr(x, 'process') :
        x.process(font, index)
    else :
        logging.warning("No processing of {} {}".format(str(x), index))

@_add_method(otTables.SingleSubst)
def process(self, font, index) :
    cname = "cot_s{}".format(index)
    if not len(font.alias(cname)) : return
    lists = zip(*self.mapping.items())
    font.addClass(font.alias(cname+"l"), map(font.glyph, lists[0]))
    font.addClass(font.alias(cname+"r"), map(font.glyph, lists[1]))

@_add_method(otTables.MultipleSubst)
def process(self, font, index) :
    cname = "cot_m{}".format(index)
    if not len(font.alias(cname)) : return
    nums = len(self.Coverage.glyphs)
    strings = []
    for i in range(nums) :
        strings.append([self.Sequence[i].Substitute, self.Coverage.glyphs[i]])
    res = compress_strings(strings)
    count = 0
    rules = []
    for r in res :
        if hasattr(r[1], '__iter__') :
            lname = font.alias(cname+"l{}".format(count))
            font.addClass(lname, map(font.glyph, r[1]))
            rule = lname
        else :
            rule = font.glyph(r[1]).GDLName()
        rule += " _" * (len(r[0]) - 1) + " >"
        for c in r[0] :
            if hasattr(c, '__iter__') :
                rname = font.alias(cname+"r{}".format(count))
                font.addClass(rname, map(font.glyph, c))
                rule += " " + rname
                count += 1
            else :
                rule += " " + font.glyph(c).GDLName()
        rule += ';'
        rules.append(rule)
    font.addRules(rules, index)

@_add_method(otTables.LigatureSubst)
def process(self, font, index) :
    cname = "cot_l{}".format(index)
    if not len(font.alias(cname)) : return
    strings = []
    for lg, ls in self.ligatures.items() :
        for l in ls :
            strings.append([[lg] + l.Component, l.LigGlyph])
    res = compress_strings(strings)
    count = 0
    rules = []
    for r in res :
        rule = ""
        besti = 0
        for i, c in enumerate(r[0]) :
            if hasattr(c, '__iter__') :
                lname = font.alias(cname+"l{}".format(count))
                font.addClass(lname, map(font.glyph, c))
                rule += lname + " "
                besti = i
            else :
                rule += font.glyph(c).GDLName() + " "
        rule += "> " + "_ " * besti
        if hasattr(r[1], '__iter__') :
            rname = font.alias(cname+"r{}".format(count))
            font.addClass(rname, map(font.glyph, r[1]))
            rule += rname
            count += 1
        else :
            rule += font.glyph(r[1]).GDLName()
        rule += " _" * (len(r[0]) - 1 - besti) + ";"
        rules.append(rule)
    font.addRules(rules, index)

@_add_method(otTables.ChainContextSubst)
def process(self, font, index) :

    def procsubst(rule, action) :
        for s in rule.SubstLookupRecord :
            action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
    def procCover(cs, name) :
        res = []
        for i, c in enumerate(cs) :
            if len(c.glyphs) > 1 :
                n = font.alias(name+"{}".format(i))
                font.addClass(n, map(font.glyph, c.glyphs))
                res.append(n)
            else :
                res.append(font.glyph(c.glyphs[0]).GDLName())
        return res

    cname = "cot_c{}".format(index)
    if not len(font.alias(cname)) : return
    rules = []
    if self.Format == 1 :
        for i in range(len(self.ChainSubRuleSet)) :
            for r in self.ChainSubRuleSet[i].ChainSubRule :
                action = [self.Coverage.glyphs[i]] + r.Input
                procsubst(r, action)
                rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
    elif self.Format == 2 :
        ilist = add_class_classes(font, cname+"i", self.InputClassDef)
        if self.BacktrackClassDef :
            blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
        if self.LookAheadClassDef :
            alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
        for i, s in enumerate(self.ChainSubClassSet) :
            if s is None : continue
            for r in s.ChainSubClassRule :
                action = map(lambda x:ilist[x], [i]+r.Input)
                procsubst(r, action)
                rules.append(make_rule(action, None,
                                        map(lambda x:blist[x], r.Backtrack or []),
                                        map(lambda x:alist[x], r.LookAhead or [])))
    elif self.Format == 3 :
        backs = procCover(self.BacktrackCoverage, cname+"b")
        aheads = procCover(self.LookAheadCoverage, cname+"a")
        actions = procCover(self.InputCoverage, cname+"i")
        procsubst(self, actions)
        rules.append(make_rule(actions, None, backs, aheads))
    font.addRules(rules, index)

@_add_method(otTables.SinglePos)
def process(self, font, index) :
    cname = "cot_p{}".format(index)
    if self.Format == 1 :
        font.addClass(font.alias(cname), map(font.glyph, self.Coverage.glyphs))
        rule = cname + " " + valuerectogdl(self.Value)
        font.addPosRules([rule], index)
    elif self.Format == 2 :
        rules = []
        for i, g in enumerage(map(font.glyph, self.Coverage.glyphs)) :
            rule = font.glyph(g).GDLName()
            rule += " " + valuerectogdl(self.Value[i])
            rules.append(rule)
        font.addPosRules(rules, index)

@_add_method(otTables.PairPos)
def process(self, font, index) :
    pass

@_add_method(otTables.CursivePos)
def process(self, font, index) :
    apname = "P{}".format(index)
    if not len(font.alias(apname)) : return
    if self.Format == 1 :
        mark_names = self.Coverage.glyphs
        for i, g in enumerate(map(font.glyph, mark_names)) :
            rec = self.EntryExitRecord[i]
            if rec.EntryAnchor is not None :
                g.setAnchor(font.alias(apname+"_{}M".format(rec.EntryAnchor)),
                            rec.EntryAnchor.XCoordinate, rec.EntryAnchor.YCoordinate)
            if rec.ExitAnchor is not None :
                g.setAnchor(font.alias(apname+"_{}S".format(rec.ExitAnchor)),
                            rec.ExitAnchor.XCoordinate, rec.ExitAnchor.YCoordinate)

@_add_method(otTables.MarkBasePos)
def process(self, font, index) :
    apname = "P{}".format(index)
    if not len(font.alias(apname)) : return
    if self.Format == 1 :
        mark_names = self.MarkCoverage.glyphs
        for i, g in enumerate(map(font.glyph, mark_names)) :
            rec = self.MarkArray.MarkRecord[i]
            g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
                        rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
        base_names = self.BaseCoverage.glyphs
        for i, g in enumerate(map(font.glyph, base_names)) :
            for j,b in enumerate(self.BaseArray.BaseRecord[i].BaseAnchor) :
                if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
                                    b.XCoordinate, b.YCoordinate)

@_add_method(otTables.MarkMarkPos)
def process(self, font, index) :
    apname = "P{}".format(index)
    if not len(font.alias(apname)) : return
    if self.Format == 1 :
        mark_names = self.Mark1Coverage.glyphs
        for i, g in enumerate(map(font.glyph, mark_names)) :
            rec = self.Mark1Array.MarkRecord[i]
            g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
                        rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
        base_names = self.Mark2Coverage.glyphs
        for i, g in enumerate(map(font.glyph, base_names)) :
            for j,b in enumerate(self.Mark2Array.Mark2Record[i].Mark2Anchor) :
                if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
                                    b.XCoordinate, b.YCoordinate)

@_add_method(otTables.ContextSubst)
def process(self, font, index) :

    def procsubst(rule, action) :
        for s in rule.SubstLookupRecord :
            action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
    def procCover(cs, name) :
        res = []
        for i, c in enumerate(cs) :
            if len(c.glyphs) > 1 :
                n = font.alias(name+"{}".format(i))
                font.addClass(n, map(font.glyph, c.glyphs))
                res.append(n)
            else :
                res.append(font.glyph(c.glyphs[0]).GDLName())
        return res

    cname = "cot_cs{}".format(index)
    if not len(font.alias(cname)) : return
    rules = []
    if self.Format == 1 :
        for i in range(len(self.SubRuleSet)) :
            for r in self.SubRuleSet[i].SubRule :
                action = [self.Coverage.glyphs[i]] + r.Input
                procsubst(r, action)
                rules.append(make_rule(action, None, None, None))
    elif self.Format == 2 :
        ilist = add_class_classes(font, cname+"i", self.ClassDef)
        for i, s in enumerate(self.SubClassSet) :
            if s is None : continue
            for r in s.SubClassRule :
                action = map(lambda x:ilist[x], [i]+r.Class)
                procsubst(r, action)
                rules.append(make_rule(action, None, None, None))
    elif self.Format == 3 :
        actions = procCover(self.Coverage, cname+"i")
        procsubst(self, actions)
        rules.append(make_rule(actions, None, None, None))
    font.addRules(rules, index)

@_add_method(otTables.ContextPos)
def process(self, font, index) :

    def procsubst(rule, action) :
        for s in rule.PosLookupRecord :
            action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
    def procCover(cs, name) :
        res = []
        for i, c in enumerate(cs) :
            if len(c.glyphs) > 1 :
                n = font.alias(name+"{}".format(i))
                font.addClass(n, map(font.glyph, c.glyphs))
                res.append(n)
            else :
                res.append(font.glyph(c.glyphs[0]).GDLName())
        return res

    cname = "cot_cp{}".format(index)
    if not len(font.alias(cname)) : return
    rules = []
    if self.Format == 1 :
        for i in range(len(self.PosRuleSet)) :
            for r in self.PosRuleSet[i] :
                action = [self.Coverage.glyphs[i]] + r.Input
                procsubst(r, action)
                rules.append(make_rule(action, None, None, None))
    elif self.Format == 2 :
        ilist = add_class_classes(font, cname+"i", self.ClassDef)
        for i, s in enumerate(self.PosClassSet) :
            if s is None : continue
            for r in s.PosClassRule :
                action = map(lambda x:ilist[x], [i]+r.Class)
                procsubst(r, action)
                rules.append(make_rule(action, None, None, None))
    elif self.Format == 3 :
        actions = procCover(self.Coverage, cname+"i")
        procsubst(self, actions)
        rules.append(make_rule(actions, None, None, None))
    font.addPosRules(rules, index)

@_add_method(otTables.ChainContextPos)
def process(self, font, index) :

    def procsubst(rule, action) :
        for s in rule.PosLookupRecord :
            action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
    def procCover(cs, name) :
        res = []
        for i, c in enumerate(cs) :
            if len(c.glyphs) > 1 :
                n = font.alias(name+"{}".format(i))
                font.addClass(n, map(font.glyph, c.glyphs))
                res.append(n)
            else :
                res.append(font.glyph(c.glyphs[0]).GDLName())
        return res

    cname = "cot_c{}".format(index)
    if not len(font.alias(cname)) : return
    rules = []
    if self.Format == 1 :
        for i in range(len(self.ChainPosRuleSet)) :
            for r in self.ChainPosRuleSet[i].ChainPosRule :
                action = [self.Coverage.glyphs[i]] + r.Input
                procsubst(r, action)
                rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
    elif self.Format == 2 :
        ilist = add_class_classes(font, cname+"i", self.InputClassDef)
        if self.BacktrackClassDef :
            blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
        if self.LookAheadClassDef :
            alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
        for i, s in enumerate(self.ChainPosClassSet) :
            if s is None : continue
            for r in s.ChainPosClassRule :
                action = map(lambda x:ilist[x], [i]+r.Input)
                procsubst(r, action)
                rules.append(make_rule(action, None,
                                        map(lambda x:blist[x], r.Backtrack or []),
                                        map(lambda x:alist[x], r.LookAhead or [])))
    elif self.Format == 3 :
        backs = procCover(self.BacktrackCoverage, cname+"b")
        aheads = procCover(self.LookAheadCoverage, cname+"a")
        actions = procCover(self.InputCoverage, cname+"i")
        procsubst(self, actions)
        rules.append(make_rule(actions, None, backs, aheads))
    font.addPosRules(rules, index)