#!/usr/bin/env python
'The main font object for GDL creation. Depends on fonttools'
__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 os, re, traceback
from silfont.gdl.glyph import Glyph
from silfont.gdl.psnames import Name
from xml.etree.cElementTree import ElementTree, parse, Element
from fontTools.ttLib import TTFont

# A collection of glyphs that have a given attachment point defined
class PointClass(object) :

    def __init__(self, name) :
        self.name = name
        self.glyphs = []
        self.dias = []

    def addBaseGlyph(self, g) :
        self.glyphs.append(g)

    def addDiaGlyph(self, g) :
        self.dias.append(g)
        g.isDia = True

    def hasDias(self) :
        if len(self.dias) and len(self.glyphs) :
            return True
        else :
            return False

    def classGlyphs(self, isDia = False) :
        if isDia :
            return self.dias
        else :
            return self.glyphs

    def isNotInClass(self, g, isDia = False) :
        if not g : return False
        if not g.isDia : return False

        if isDia :
            return g not in self.dias
        else :
            return g not in self.dias and g not in self.glyphs


class FontClass(object) :

    def __init__(self, elements = None, fname = None, lineno = None, generated = False, editable = False) :
        self.elements = elements or []
        self.fname = fname
        self.lineno = lineno
        self.generated = generated
        self.editable = editable

    def append(self, element) :
        self.elements.append(element)


class Font(object) :

    def __init__(self, fontfile) :
        self.glyphs = []
        self.psnames = {}
        self.canons = {}
        self.gdls = {}
        self.anchors = {}
        self.ligs = {}
        self.subclasses = {}
        self.points = {}
        self.classes = {}
        self.aliases = {}
        self.rules = {}
        self.posRules = {}
        if fontfile :
            self.font = TTFont(fontfile)
            for i, n in enumerate(self.font.getGlyphOrder()) :
                self.addGlyph(i, n)
        else :
            self.font = None

    def __len__(self) :
        return len(self.glyphs)

    # [] syntax returns the indicated element of the glyphs array.
    def __getitem__(self, y) :
        try :
            return self.glyphs[y]
        except IndexError :
            return None

    def glyph(self, name) :
        return self.psnames.get(name, None)

    def alias(self, s) :
        return self.aliases.get(s, s)

    def emunits(self) :
        return 0

    def initGlyphs(self, nGlyphs) :
        #print "Font::initGlyphs",nGlyphs
        self.glyphs = [None] * nGlyphs
        self.numRealGlyphs = nGlyphs  # does not include pseudo-glyphs
        self.psnames = {}
        self.canons = {}
        self.gdls = {}
        self.classes = {}

    def addGlyph(self, index = None, psName = None, gdlName = None, factory = Glyph) :
        #print "Font::addGlyph",index,psName,gdlName
        if psName in self.psnames :
            return self.psnames[psName]
        if index is not None and index < len(self.glyphs) and self.glyphs[index] :
            g = self.glyphs[index]
            return g
        g = factory(psName, index) # create a new glyph of the given class
        self.renameGlyph(g, psName, gdlName)
        if index is None :  # give it the next available index
            index = len(self.glyphs)
            self.glyphs.append(g)
        elif index >= len(self.glyphs) :
            self.glyphs.extend([None] * (len(self.glyphs) - index + 1))
        self.glyphs[index] = g
        return g

    def renameGlyph(self, g, name, gdlName = None) :
        if g.psname != name :
            for n in g.parseNames() :
                del self.psnames[n.psname]
                del self.canons[n.canonical()]
        if gdlName :
            self.setGDL(g, gdlName)
        else :
            self.setGDL(g, g.GDLName())
        for n in g.parseNames() :
            if n is None : break
            self.psnames[n.psname] = g
            self.canons[n.canonical()] = (n, g)

    def setGDL(self, glyph, name) :
        if not glyph : return
        n = glyph.GDLName()
        if n != name and n in self.gdls : del self.gdls[n]
        if name and name in self.gdls and self.gdls[name] is not glyph :
            count = 1
            index = -2
            name = name + "_1"
            while name in self.gdls :
                if self.gdls[name] is glyph : break
                count = count + 1
                name = name[0:index] + "_" + str(count)
                if count == 10 : index = -3
                if count == 100 : index = -4
        self.gdls[name] = glyph
        glyph.setGDL(name)

    def addClass(self, name, elements, fname = None, lineno = 0, generated = False, editable = False) :
        if name :
            self.classes[name] = FontClass(elements, fname, lineno, generated, editable)

    def addGlyphClass(self, name, gid, editable = False) :
        if name not in self.classes :
            self.classes[name] = FontClass()
        if gid not in self.classes[name].elements :
            self.classes[name].append(gid)

    def addRules(self, rules, index) :
        self.rules[index] = rules

    def addPosRules(self, rules, index) :
        self.posRules[index] = rules

    def classUpdated(self, name, value) :
        c = []
        if name in self.classes :
            for gid in self.classes[name].elements :
                g = self[gid]
                if g : g.removeClass(name)
        if value is None and name in classes :
            del self.classes[name]
            return
        for n in value.split() :
            g = self.gdls.get(n, None)
            if g :
                c.append(g.gid)
                g.addClass(name)
        if name in self.classes :
            self.classes[name].elements = c
        else :
            self.classes[name] = FontClass(c)

    # Return the list of classes that should be updated in the AP XML file.
    # This does not include classes that are auto-generated or defined in the hand-crafted GDL code.
    def filterAutoClasses(self, names, autoGdlFile) :
        res = []
        for n in names :
            c = self.classes[n]
            if not c.generated and (not c.fname or c.fname == autoGdlFile) : res.append(n)
        return res

    def loadAlias(self, fname) :
        with open(fname) as f :
            for l in f.readlines() :
                l = l.strip()
                l = re.sub(ur'#.*$', '', l).strip()
                if not len(l) : continue
                try :
                    k, v = re.split(ur'\s*[,;\s]\s*', l, 1)
                except ValueError :
                    k = l
                    v = ''
                self.aliases[k] = v

    # TODO: move this method to GraideFont, or refactor
    def loadAP(self, apFileName) :
        if not os.path.exists(apFileName) : return False
        etree = parse(apFileName)
        self.initGlyphs(len(etree.getroot())) # guess each child is a glyph
        i = 0
        for e in etree.getroot().iterfind("glyph") :
            g = self.addGlyph(i, e.get('PSName'))
            g.readAP(e, self)
            i += 1
        return True

    def saveAP(self, apFileName, autoGdlFile) :
        root = Element('font')
        root.set('upem', str(self.emunits()))
        root.set('producer', 'graide 1.0')
        root.text = "\n\n"
        for g in self.glyphs :
            if g : g.createAP(root, self, autoGdlFile)
        ElementTree(root).write(apFileName, encoding="utf-8", xml_declaration=True)

    def createClasses(self) :
        self.subclasses = {}
        for k, v in self.canons.items() :
            if v[0].ext :
                h = v[0].head()
                o = self.canons.get(h.canonical(), None)
                if o :
                    if v[0].ext not in self.subclasses : self.subclasses[v[0].ext] = {}
                    self.subclasses[v[0].ext][o[1].GDLName()] = v[1].GDLName()
#        for g in self.glyphs :
#            if not g : continue
#            for c in g.classes :
#                if c not in self.classes :
#                    self.classes[c] = []
#                self.classes[c].append(g.gid)

    def calculatePointClasses(self) :
        self.points = {}
        for g in self.glyphs :
            if not g : continue
            for apName in g.anchors.keys() :
                genericName = apName[:-1] # without the M or S
                if genericName not in self.points :
                    self.points[genericName] = PointClass(genericName)
                if apName.endswith('S') :
                    self.points[genericName].addBaseGlyph(g)
                else :
                    self.points[genericName].addDiaGlyph(g)

    def calculateOTLookups(self) :
        if self.font :
            for t in ('GSUB', 'GPOS') :
                if t in self.font :
                    self.font[t].table.LookupList.process(self)

    def getPointClasses(self) :
        if len(self.points) == 0 :
            self.calculatePointClasses()
        return self.points

    def ligClasses(self) :
        self.ligs = {}
        for g in self.glyphs :
            if not g or not g.name : continue
            (h, t) = g.name.split_last()
            if t :
                o = self.canons.get(h.canonical(), None)
                if o and o[0].ext == t.ext :
                    t.ext = None
                    t.cname = None
                    tn = t.canonical(noprefix = True)
                    if tn in self.ligs :
                        self.ligs[tn].append((g.GDLName(), o[0].GDL()))
                    else :
                        self.ligs[tn] = [(g.GDLName(), o[0].GDL())]

    def outGDL(self, fh, args) :
        munits = self.emunits()
        fh.write('table(glyph) {MUnits = ' + str(munits) + '};\n')
        nglyphs = 0
        for g in self.glyphs :
            if not g or not g.psname : continue
            if g.psname == '.notdef' :
                fh.write(g.GDLName() + ' = glyphid(0)')
            else :
               fh.write(g.GDLName() + ' = postscript("' + g.psname + '")')
            outs = []
            if len(g.anchors) :
                for a in g.anchors.keys() :
                    v = g.anchors[a]
                    outs.append(a + "=point(" + str(int(v[0])) + "m, " + str(int(v[1])) + "m)")
            for (p, v) in g.gdl_properties.items() :
                outs.append("%s=%s" % (p, v))
            if len(outs) : fh.write(" {" + "; ".join(outs) + "}")
            fh.write(";\n")
            nglyphs += 1
        fh.write("\n")
        fh.write("\n/* Point Classes */\n")
        for p in sorted(self.points.values(), key=lambda x: x.name) :
            if not p.hasDias() : continue
            n = p.name + "Dia"
            self.outclass(fh, "c" + n, p.classGlyphs(True))
            self.outclass(fh, "cTakes" + n, p.classGlyphs(False))
            self.outclass(fh, 'cn' + n, filter(lambda x : p.isNotInClass(x, True), self.glyphs))
            self.outclass(fh, 'cnTakes' + n, filter(lambda x : p.isNotInClass(x, False), self.glyphs))
        fh.write("\n/* Classes */\n")
        for c in sorted(self.classes.keys()) : # c = class name, l = class object
            if c not in self.subclasses and not self.classes[c].generated :  # don't output the class to the AP file if it was autogenerated
                self.outclass(fh, c, self.classes[c].elements)
        for p in self.subclasses.keys() :
            ins = []
            outs = []
            for k, v in self.subclasses[p].items() :
                ins.append(k)
                outs.append(v)
            n = p.replace('.', '_')
            self.outclass(fh, 'cno_' + n, ins)
            self.outclass(fh, 'c' + n, outs)
        fh.write("/* Ligature Classes */\n")
        for k in sorted(self.ligs.keys()) :
            self.outclass(fh, "clig" + k, map(lambda x: self.gdls[x[0]], self.ligs[k]))
            self.outclass(fh, "cligno_" + k, map(lambda x: self.gdls[x[1]], self.ligs[k]))
        fh.write("\nendtable;\n")
        fh.write("/* Substitution Rules */\n")
        for k, v in sorted(self.rules.items(), key=lambda x:map(int,x[0].split('_'))) :
            fh.write('\n// lookup ' + k + '\n')
            fh.write('// ' + "\n// ".join(v) + "\n")
        fh.write("\n/* Positioning Rules */\n")
        for k, v in sorted(self.posRules.items(), key=lambda x:map(int,x[0].split('_'))) :
            fh.write('\n// lookup ' + k + '\n')
            fh.write('// ' + "\n// ".join(v) + "\n")
        fh.write("\n\n#define MAXGLYPH %d\n\n" % (nglyphs - 1))
        if args.include :
            fh.write("#include \"%s\"\n" % args.include)

    def outPosRules(self, fh, num) :
        fh.write("""
#ifndef opt2
#define opt(x) [x]?
#define opt2(x) [opt(x) x]?
#define opt3(x) [opt2(x) x]?
#define opt4(x) [opt3(x) x]?
#endif
#define posrule(x) c##x##Dia {attach{to=@1; at=x##S; with=x##M}} / cTakes##x##Dia opt4(cnTakes##x##Dia) _;

table(positioning);
pass(%d);
""" % num)
        for p in self.points.values() :
            if p.hasDias() :
                fh.write("posrule(%s);\n" % p.name)
        fh.write("endpass;\nendtable;\n")


    def outclass(self, fh, name, glyphs) :
        fh.write(name + " = (")
        count = 1
        sep = ""
        for g in glyphs :
            if not g : continue


            if isinstance(g, basestring) :
                fh.write(sep + g)
            else :
                if g.GDLName() is None :
                    print "Can't output " + str(g.gid) + " to class " + name
                else :
                    fh.write(sep + g.GDLName())
            if count % 8 == 0 :
                sep = ',\n         '
            else :
                sep = ', '
            count += 1
        fh.write(');\n\n')

