from fontemon_blender_addon.fontTools.misc.py23 import *
from fontemon_blender_addon.fontTools.misc import sstruct
from fontemon_blender_addon.fontTools.misc.textTools import binary2num, safeEval
from fontemon_blender_addon.fontTools.feaLib.error import FeatureLibError
from fontemon_blender_addon.fontTools.feaLib.lookupDebugInfo import (
    LookupDebugInfo,
    LOOKUP_DEBUG_INFO_KEY,
    LOOKUP_DEBUG_ENV_VAR,
)
from fontemon_blender_addon.fontTools.feaLib.parser import Parser
from fontemon_blender_addon.fontTools.feaLib.ast import FeatureFile
from fontemon_blender_addon.fontTools.otlLib import builder as otl
from fontemon_blender_addon.fontTools.otlLib.maxContextCalc import maxCtxFont
from fontemon_blender_addon.fontTools.ttLib import newTable, getTableModule
from fontemon_blender_addon.fontTools.ttLib.tables import otBase, otTables
from fontemon_blender_addon.fontTools.otlLib.builder import (
    AlternateSubstBuilder,
    ChainContextPosBuilder,
    ChainContextSubstBuilder,
    LigatureSubstBuilder,
    MultipleSubstBuilder,
    CursivePosBuilder,
    MarkBasePosBuilder,
    MarkLigPosBuilder,
    MarkMarkPosBuilder,
    ReverseChainSingleSubstBuilder,
    SingleSubstBuilder,
    ClassPairPosSubtableBuilder,
    PairPosBuilder,
    SinglePosBuilder,
    ChainContextualRule,
)
from fontemon_blender_addon.fontTools.otlLib.error import OpenTypeLibError
from collections import defaultdict
import itertools
import logging
import warnings
import os


log = logging.getLogger(__name__)


def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
    """Add features from a file to a font. Note that this replaces any features
    currently present.

    Args:
        font (feaLib.ttLib.TTFont): The font object.
        featurefile: Either a path or file object (in which case we
            parse it into an AST), or a pre-parsed AST instance.
        tables: If passed, restrict the set of affected tables to those in the
            list.
        debug: Whether to add source debugging information to the font in the
            ``Debg`` table

    """
    builder = Builder(font, featurefile)
    builder.build(tables=tables, debug=debug)


def addOpenTypeFeaturesFromString(
    font, features, filename=None, tables=None, debug=False
):
    """Add features from a string to a font. Note that this replaces any
    features currently present.

    Args:
        font (feaLib.ttLib.TTFont): The font object.
        features: A string containing feature code.
        filename: The directory containing ``filename`` is used as the root of
            relative ``include()`` paths; if ``None`` is provided, the current
            directory is assumed.
        tables: If passed, restrict the set of affected tables to those in the
            list.
        debug: Whether to add source debugging information to the font in the
            ``Debg`` table

    """

    featurefile = UnicodeIO(tounicode(features))
    if filename:
        featurefile.name = filename
    addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)


class Builder(object):

    supportedTables = frozenset(
        Tag(tag)
        for tag in [
            "BASE",
            "GDEF",
            "GPOS",
            "GSUB",
            "OS/2",
            "head",
            "hhea",
            "name",
            "vhea",
        ]
    )

    def __init__(self, font, featurefile):
        self.font = font
        # 'featurefile' can be either a path or file object (in which case we
        # parse it into an AST), or a pre-parsed AST instance
        if isinstance(featurefile, FeatureFile):
            self.parseTree, self.file = featurefile, None
        else:
            self.parseTree, self.file = None, featurefile
        self.glyphMap = font.getReverseGlyphMap()
        self.default_language_systems_ = set()
        self.script_ = None
        self.lookupflag_ = 0
        self.lookupflag_markFilterSet_ = None
        self.language_systems = set()
        self.seen_non_DFLT_script_ = False
        self.named_lookups_ = {}
        self.cur_lookup_ = None
        self.cur_lookup_name_ = None
        self.cur_feature_name_ = None
        self.lookups_ = []
        self.lookup_locations = {"GSUB": {}, "GPOS": {}}
        self.features_ = {}  # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
        self.required_features_ = {}  # ('latn', 'DEU ') --> 'scmp'
        # for feature 'aalt'
        self.aalt_features_ = []  # [(location, featureName)*], for 'aalt'
        self.aalt_location_ = None
        self.aalt_alternates_ = {}
        # for 'featureNames'
        self.featureNames_ = set()
        self.featureNames_ids_ = {}
        # for 'cvParameters'
        self.cv_parameters_ = set()
        self.cv_parameters_ids_ = {}
        self.cv_num_named_params_ = {}
        self.cv_characters_ = defaultdict(list)
        # for feature 'size'
        self.size_parameters_ = None
        # for table 'head'
        self.fontRevision_ = None  # 2.71
        # for table 'name'
        self.names_ = []
        # for table 'BASE'
        self.base_horiz_axis_ = None
        self.base_vert_axis_ = None
        # for table 'GDEF'
        self.attachPoints_ = {}  # "a" --> {3, 7}
        self.ligCaretCoords_ = {}  # "f_f_i" --> {300, 600}
        self.ligCaretPoints_ = {}  # "f_f_i" --> {3, 7}
        self.glyphClassDefs_ = {}  # "fi" --> (2, (file, line, column))
        self.markAttach_ = {}  # "acute" --> (4, (file, line, column))
        self.markAttachClassID_ = {}  # frozenset({"acute", "grave"}) --> 4
        self.markFilterSets_ = {}  # frozenset({"acute", "grave"}) --> 4
        # for table 'OS/2'
        self.os2_ = {}
        # for table 'hhea'
        self.hhea_ = {}
        # for table 'vhea'
        self.vhea_ = {}

    def build(self, tables=None, debug=False):
        if self.parseTree is None:
            self.parseTree = Parser(self.file, self.glyphMap).parse()
        self.parseTree.build(self)
        # by default, build all the supported tables
        if tables is None:
            tables = self.supportedTables
        else:
            tables = frozenset(tables)
            unsupported = tables - self.supportedTables
            if unsupported:
                unsupported_string = ", ".join(sorted(unsupported))
                raise NotImplementedError(
                    "The following tables were requested but are unsupported: "
                    f"{unsupported_string}."
                )
        if "GSUB" in tables:
            self.build_feature_aalt_()
        if "head" in tables:
            self.build_head()
        if "hhea" in tables:
            self.build_hhea()
        if "vhea" in tables:
            self.build_vhea()
        if "name" in tables:
            self.build_name()
        if "OS/2" in tables:
            self.build_OS_2()
        for tag in ("GPOS", "GSUB"):
            if tag not in tables:
                continue
            table = self.makeTable(tag)
            if (
                table.ScriptList.ScriptCount > 0
                or table.FeatureList.FeatureCount > 0
                or table.LookupList.LookupCount > 0
            ):
                fontTable = self.font[tag] = newTable(tag)
                fontTable.table = table
            elif tag in self.font:
                del self.font[tag]
        if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
            self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
        if "GDEF" in tables:
            gdef = self.buildGDEF()
            if gdef:
                self.font["GDEF"] = gdef
            elif "GDEF" in self.font:
                del self.font["GDEF"]
        if "BASE" in tables:
            base = self.buildBASE()
            if base:
                self.font["BASE"] = base
            elif "BASE" in self.font:
                del self.font["BASE"]
        if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
            self.buildDebg()

    def get_chained_lookup_(self, location, builder_class):
        result = builder_class(self.font, location)
        result.lookupflag = self.lookupflag_
        result.markFilterSet = self.lookupflag_markFilterSet_
        self.lookups_.append(result)
        return result

    def add_lookup_to_feature_(self, lookup, feature_name):
        for script, lang in self.language_systems:
            key = (script, lang, feature_name)
            self.features_.setdefault(key, []).append(lookup)

    def get_lookup_(self, location, builder_class):
        if (
            self.cur_lookup_
            and type(self.cur_lookup_) == builder_class
            and self.cur_lookup_.lookupflag == self.lookupflag_
            and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
        ):
            return self.cur_lookup_
        if self.cur_lookup_name_ and self.cur_lookup_:
            raise FeatureLibError(
                "Within a named lookup block, all rules must be of "
                "the same lookup type and flag",
                location,
            )
        self.cur_lookup_ = builder_class(self.font, location)
        self.cur_lookup_.lookupflag = self.lookupflag_
        self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
        self.lookups_.append(self.cur_lookup_)
        if self.cur_lookup_name_:
            # We are starting a lookup rule inside a named lookup block.
            self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
        if self.cur_feature_name_:
            # We are starting a lookup rule inside a feature. This includes
            # lookup rules inside named lookups inside features.
            self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
        return self.cur_lookup_

    def build_feature_aalt_(self):
        if not self.aalt_features_ and not self.aalt_alternates_:
            return
        alternates = {g: set(a) for g, a in self.aalt_alternates_.items()}
        for location, name in self.aalt_features_ + [(None, "aalt")]:
            feature = [
                (script, lang, feature, lookups)
                for (script, lang, feature), lookups in self.features_.items()
                if feature == name
            ]
            # "aalt" does not have to specify its own lookups, but it might.
            if not feature and name != "aalt":
                raise FeatureLibError(
                    "Feature %s has not been defined" % name, location
                )
            for script, lang, feature, lookups in feature:
                for lookuplist in lookups:
                    if not isinstance(lookuplist, list):
                        lookuplist = [lookuplist]
                    for lookup in lookuplist:
                        for glyph, alts in lookup.getAlternateGlyphs().items():
                            alternates.setdefault(glyph, set()).update(alts)
        single = {
            glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
        }
        # TODO: Figure out the glyph alternate ordering used by makeotf.
        # https://github.com/fontemon_blender_addon.fontTools/fontemon_blender_addon.fontTools/issues/836
        multi = {
            glyph: sorted(repl, key=self.font.getGlyphID)
            for glyph, repl in alternates.items()
            if len(repl) > 1
        }
        if not single and not multi:
            return
        self.features_ = {
            (script, lang, feature): lookups
            for (script, lang, feature), lookups in self.features_.items()
            if feature != "aalt"
        }
        old_lookups = self.lookups_
        self.lookups_ = []
        self.start_feature(self.aalt_location_, "aalt")
        if single:
            single_lookup = self.get_lookup_(location, SingleSubstBuilder)
            single_lookup.mapping = single
        if multi:
            multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
            multi_lookup.alternates = multi
        self.end_feature()
        self.lookups_.extend(old_lookups)

    def build_head(self):
        if not self.fontRevision_:
            return
        table = self.font.get("head")
        if not table:  # this only happens for unit tests
            table = self.font["head"] = newTable("head")
            table.decompile(b"\0" * 54, self.font)
            table.tableVersion = 1.0
            table.created = table.modified = 3406620153  # 2011-12-13 11:22:33
        table.fontRevision = self.fontRevision_

    def build_hhea(self):
        if not self.hhea_:
            return
        table = self.font.get("hhea")
        if not table:  # this only happens for unit tests
            table = self.font["hhea"] = newTable("hhea")
            table.decompile(b"\0" * 36, self.font)
            table.tableVersion = 0x00010000
        if "caretoffset" in self.hhea_:
            table.caretOffset = self.hhea_["caretoffset"]
        if "ascender" in self.hhea_:
            table.ascent = self.hhea_["ascender"]
        if "descender" in self.hhea_:
            table.descent = self.hhea_["descender"]
        if "linegap" in self.hhea_:
            table.lineGap = self.hhea_["linegap"]

    def build_vhea(self):
        if not self.vhea_:
            return
        table = self.font.get("vhea")
        if not table:  # this only happens for unit tests
            table = self.font["vhea"] = newTable("vhea")
            table.decompile(b"\0" * 36, self.font)
            table.tableVersion = 0x00011000
        if "verttypoascender" in self.vhea_:
            table.ascent = self.vhea_["verttypoascender"]
        if "verttypodescender" in self.vhea_:
            table.descent = self.vhea_["verttypodescender"]
        if "verttypolinegap" in self.vhea_:
            table.lineGap = self.vhea_["verttypolinegap"]

    def get_user_name_id(self, table):
        # Try to find first unused font-specific name id
        nameIDs = [name.nameID for name in table.names]
        for user_name_id in range(256, 32767):
            if user_name_id not in nameIDs:
                return user_name_id

    def buildFeatureParams(self, tag):
        params = None
        if tag == "size":
            params = otTables.FeatureParamsSize()
            (
                params.DesignSize,
                params.SubfamilyID,
                params.RangeStart,
                params.RangeEnd,
            ) = self.size_parameters_
            if tag in self.featureNames_ids_:
                params.SubfamilyNameID = self.featureNames_ids_[tag]
            else:
                params.SubfamilyNameID = 0
        elif tag in self.featureNames_:
            if not self.featureNames_ids_:
                # name table wasn't selected among the tables to build; skip
                pass
            else:
                assert tag in self.featureNames_ids_
                params = otTables.FeatureParamsStylisticSet()
                params.Version = 0
                params.UINameID = self.featureNames_ids_[tag]
        elif tag in self.cv_parameters_:
            params = otTables.FeatureParamsCharacterVariants()
            params.Format = 0
            params.FeatUILabelNameID = self.cv_parameters_ids_.get(
                (tag, "FeatUILabelNameID"), 0
            )
            params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
                (tag, "FeatUITooltipTextNameID"), 0
            )
            params.SampleTextNameID = self.cv_parameters_ids_.get(
                (tag, "SampleTextNameID"), 0
            )
            params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
            params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
                (tag, "ParamUILabelNameID_0"), 0
            )
            params.CharCount = len(self.cv_characters_[tag])
            params.Character = self.cv_characters_[tag]
        return params

    def build_name(self):
        if not self.names_:
            return
        table = self.font.get("name")
        if not table:  # this only happens for unit tests
            table = self.font["name"] = newTable("name")
            table.names = []
        for name in self.names_:
            nameID, platformID, platEncID, langID, string = name
            # For featureNames block, nameID is 'feature tag'
            # For cvParameters blocks, nameID is ('feature tag', 'block name')
            if not isinstance(nameID, int):
                tag = nameID
                if tag in self.featureNames_:
                    if tag not in self.featureNames_ids_:
                        self.featureNames_ids_[tag] = self.get_user_name_id(table)
                        assert self.featureNames_ids_[tag] is not None
                    nameID = self.featureNames_ids_[tag]
                elif tag[0] in self.cv_parameters_:
                    if tag not in self.cv_parameters_ids_:
                        self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
                        assert self.cv_parameters_ids_[tag] is not None
                    nameID = self.cv_parameters_ids_[tag]
            table.setName(string, nameID, platformID, platEncID, langID)

    def build_OS_2(self):
        if not self.os2_:
            return
        table = self.font.get("OS/2")
        if not table:  # this only happens for unit tests
            table = self.font["OS/2"] = newTable("OS/2")
            data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
            table.decompile(data, self.font)
        version = 0
        if "fstype" in self.os2_:
            table.fsType = self.os2_["fstype"]
        if "panose" in self.os2_:
            panose = getTableModule("OS/2").Panose()
            (
                panose.bFamilyType,
                panose.bSerifStyle,
                panose.bWeight,
                panose.bProportion,
                panose.bContrast,
                panose.bStrokeVariation,
                panose.bArmStyle,
                panose.bLetterForm,
                panose.bMidline,
                panose.bXHeight,
            ) = self.os2_["panose"]
            table.panose = panose
        if "typoascender" in self.os2_:
            table.sTypoAscender = self.os2_["typoascender"]
        if "typodescender" in self.os2_:
            table.sTypoDescender = self.os2_["typodescender"]
        if "typolinegap" in self.os2_:
            table.sTypoLineGap = self.os2_["typolinegap"]
        if "winascent" in self.os2_:
            table.usWinAscent = self.os2_["winascent"]
        if "windescent" in self.os2_:
            table.usWinDescent = self.os2_["windescent"]
        if "vendor" in self.os2_:
            table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
        if "weightclass" in self.os2_:
            table.usWeightClass = self.os2_["weightclass"]
        if "widthclass" in self.os2_:
            table.usWidthClass = self.os2_["widthclass"]
        if "unicoderange" in self.os2_:
            table.setUnicodeRanges(self.os2_["unicoderange"])
        if "codepagerange" in self.os2_:
            pages = self.build_codepages_(self.os2_["codepagerange"])
            table.ulCodePageRange1, table.ulCodePageRange2 = pages
            version = 1
        if "xheight" in self.os2_:
            table.sxHeight = self.os2_["xheight"]
            version = 2
        if "capheight" in self.os2_:
            table.sCapHeight = self.os2_["capheight"]
            version = 2
        if "loweropsize" in self.os2_:
            table.usLowerOpticalPointSize = self.os2_["loweropsize"]
            version = 5
        if "upperopsize" in self.os2_:
            table.usUpperOpticalPointSize = self.os2_["upperopsize"]
            version = 5

        def checkattr(table, attrs):
            for attr in attrs:
                if not hasattr(table, attr):
                    setattr(table, attr, 0)

        table.version = max(version, table.version)
        # this only happens for unit tests
        if version >= 1:
            checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
        if version >= 2:
            checkattr(
                table,
                (
                    "sxHeight",
                    "sCapHeight",
                    "usDefaultChar",
                    "usBreakChar",
                    "usMaxContext",
                ),
            )
        if version >= 5:
            checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))

    def build_codepages_(self, pages):
        pages2bits = {
            1252: 0,
            1250: 1,
            1251: 2,
            1253: 3,
            1254: 4,
            1255: 5,
            1256: 6,
            1257: 7,
            1258: 8,
            874: 16,
            932: 17,
            936: 18,
            949: 19,
            950: 20,
            1361: 21,
            869: 48,
            866: 49,
            865: 50,
            864: 51,
            863: 52,
            862: 53,
            861: 54,
            860: 55,
            857: 56,
            855: 57,
            852: 58,
            775: 59,
            737: 60,
            708: 61,
            850: 62,
            437: 63,
        }
        bits = [pages2bits[p] for p in pages if p in pages2bits]
        pages = []
        for i in range(2):
            pages.append("")
            for j in range(i * 32, (i + 1) * 32):
                if j in bits:
                    pages[i] += "1"
                else:
                    pages[i] += "0"
        return [binary2num(p[::-1]) for p in pages]

    def buildBASE(self):
        if not self.base_horiz_axis_ and not self.base_vert_axis_:
            return None
        base = otTables.BASE()
        base.Version = 0x00010000
        base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
        base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)

        result = newTable("BASE")
        result.table = base
        return result

    def buildBASEAxis(self, axis):
        if not axis:
            return
        bases, scripts = axis
        axis = otTables.Axis()
        axis.BaseTagList = otTables.BaseTagList()
        axis.BaseTagList.BaselineTag = bases
        axis.BaseTagList.BaseTagCount = len(bases)
        axis.BaseScriptList = otTables.BaseScriptList()
        axis.BaseScriptList.BaseScriptRecord = []
        axis.BaseScriptList.BaseScriptCount = len(scripts)
        for script in sorted(scripts):
            record = otTables.BaseScriptRecord()
            record.BaseScriptTag = script[0]
            record.BaseScript = otTables.BaseScript()
            record.BaseScript.BaseLangSysCount = 0
            record.BaseScript.BaseValues = otTables.BaseValues()
            record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
            record.BaseScript.BaseValues.BaseCoord = []
            record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
            for c in script[2]:
                coord = otTables.BaseCoord()
                coord.Format = 1
                coord.Coordinate = c
                record.BaseScript.BaseValues.BaseCoord.append(coord)
            axis.BaseScriptList.BaseScriptRecord.append(record)
        return axis

    def buildGDEF(self):
        gdef = otTables.GDEF()
        gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
        gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
        gdef.LigCaretList = otl.buildLigCaretList(
            self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
        )
        gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
        gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
        gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
        if any(
            (
                gdef.GlyphClassDef,
                gdef.AttachList,
                gdef.LigCaretList,
                gdef.MarkAttachClassDef,
                gdef.MarkGlyphSetsDef,
            )
        ):
            result = newTable("GDEF")
            result.table = gdef
            return result
        else:
            return None

    def buildGDEFGlyphClassDef_(self):
        if self.glyphClassDefs_:
            classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
        else:
            classes = {}
            for lookup in self.lookups_:
                classes.update(lookup.inferGlyphClasses())
            for markClass in self.parseTree.markClasses.values():
                for markClassDef in markClass.definitions:
                    for glyph in markClassDef.glyphSet():
                        classes[glyph] = 3
        if classes:
            result = otTables.GlyphClassDef()
            result.classDefs = classes
            return result
        else:
            return None

    def buildGDEFMarkAttachClassDef_(self):
        classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
        if not classDefs:
            return None
        result = otTables.MarkAttachClassDef()
        result.classDefs = classDefs
        return result

    def buildGDEFMarkGlyphSetsDef_(self):
        sets = []
        for glyphs, id_ in sorted(
            self.markFilterSets_.items(), key=lambda item: item[1]
        ):
            sets.append(glyphs)
        return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)

    def buildDebg(self):
        if "Debg" not in self.font:
            self.font["Debg"] = newTable("Debg")
            self.font["Debg"].data = {}
        self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations

    def buildLookups_(self, tag):
        assert tag in ("GPOS", "GSUB"), tag
        for lookup in self.lookups_:
            lookup.lookup_index = None
        lookups = []
        for lookup in self.lookups_:
            if lookup.table != tag:
                continue
            lookup.lookup_index = len(lookups)
            # LFDS modified
            # self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
            #     location=str(lookup.location),
            #     name=self.get_lookup_name_(lookup),
            #     feature=None,
            # )
            lookups.append(lookup)
        try:
            otLookups = [l.build() for l in lookups]
        except OpenTypeLibError as e:
            raise FeatureLibError(str(e), e.location) from e
        return otLookups

    def makeTable(self, tag):
        table = getattr(otTables, tag, None)()
        table.Version = 0x00010000
        table.ScriptList = otTables.ScriptList()
        table.ScriptList.ScriptRecord = []
        table.FeatureList = otTables.FeatureList()
        table.FeatureList.FeatureRecord = []
        table.LookupList = otTables.LookupList()
        table.LookupList.Lookup = self.buildLookups_(tag)

        # Build a table for mapping (tag, lookup_indices) to feature_index.
        # For example, ('liga', (2,3,7)) --> 23.
        feature_indices = {}
        required_feature_indices = {}  # ('latn', 'DEU') --> 23
        scripts = {}  # 'latn' --> {'DEU': [23, 24]} for feature #23,24
        # Sort the feature table by feature tag:
        # https://github.com/fontemon_blender_addon.fontTools/fontemon_blender_addon.fontTools/issues/568
        sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
        for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
            script, lang, feature_tag = key
            # l.lookup_index will be None when a lookup is not needed
            # for the table under construction. For example, substitution
            # rules will have no lookup_index while building GPOS tables.
            lookup_indices = tuple(
                [l.lookup_index for l in lookups if l.lookup_index is not None]
            )

            size_feature = tag == "GPOS" and feature_tag == "size"
            if len(lookup_indices) == 0 and not size_feature:
                continue
            #LFDS modified
            # for ix in lookup_indices:
            #     try:
            #         self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
            #             str(ix)
            #         ]._replace(feature=key)
            #     except KeyError:
            #         warnings.warn("feaLib.Builder subclass needs upgrading to "
            #             "stash debug information. See fontemon_blender_addon.fontTools#2065.")

            feature_key = (feature_tag, lookup_indices)
            feature_index = feature_indices.get(feature_key)
            if feature_index is None:
                feature_index = len(table.FeatureList.FeatureRecord)
                frec = otTables.FeatureRecord()
                frec.FeatureTag = feature_tag
                frec.Feature = otTables.Feature()
                frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
                frec.Feature.LookupListIndex = list(lookup_indices)
                frec.Feature.LookupCount = len(lookup_indices)
                table.FeatureList.FeatureRecord.append(frec)
                feature_indices[feature_key] = feature_index
            scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
            if self.required_features_.get((script, lang)) == feature_tag:
                required_feature_indices[(script, lang)] = feature_index

        # Build ScriptList.
        for script, lang_features in sorted(scripts.items()):
            srec = otTables.ScriptRecord()
            srec.ScriptTag = script
            srec.Script = otTables.Script()
            srec.Script.DefaultLangSys = None
            srec.Script.LangSysRecord = []
            for lang, feature_indices in sorted(lang_features.items()):
                langrec = otTables.LangSysRecord()
                langrec.LangSys = otTables.LangSys()
                langrec.LangSys.LookupOrder = None

                req_feature_index = required_feature_indices.get((script, lang))
                if req_feature_index is None:
                    langrec.LangSys.ReqFeatureIndex = 0xFFFF
                else:
                    langrec.LangSys.ReqFeatureIndex = req_feature_index

                langrec.LangSys.FeatureIndex = [
                    i for i in feature_indices if i != req_feature_index
                ]
                langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)

                if lang == "dflt":
                    srec.Script.DefaultLangSys = langrec.LangSys
                else:
                    langrec.LangSysTag = lang
                    srec.Script.LangSysRecord.append(langrec)
            srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
            table.ScriptList.ScriptRecord.append(srec)

        table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
        table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
        table.LookupList.LookupCount = len(table.LookupList.Lookup)
        return table

    def get_lookup_name_(self, lookup):
        rev = {v: k for k, v in self.named_lookups_.items()}
        if lookup in rev:
            return rev[lookup]
        return None

    def add_language_system(self, location, script, language):
        # OpenType Feature File Specification, section 4.b.i
        if script == "DFLT" and language == "dflt" and self.default_language_systems_:
            raise FeatureLibError(
                'If "languagesystem DFLT dflt" is present, it must be '
                "the first of the languagesystem statements",
                location,
            )
        if script == "DFLT":
            if self.seen_non_DFLT_script_:
                raise FeatureLibError(
                    'languagesystems using the "DFLT" script tag must '
                    "precede all other languagesystems",
                    location,
                )
        else:
            self.seen_non_DFLT_script_ = True
        if (script, language) in self.default_language_systems_:
            raise FeatureLibError(
                '"languagesystem %s %s" has already been specified'
                % (script.strip(), language.strip()),
                location,
            )
        self.default_language_systems_.add((script, language))

    def get_default_language_systems_(self):
        # OpenType Feature File specification, 4.b.i. languagesystem:
        # If no "languagesystem" statement is present, then the
        # implementation must behave exactly as though the following
        # statement were present at the beginning of the feature file:
        # languagesystem DFLT dflt;
        if self.default_language_systems_:
            return frozenset(self.default_language_systems_)
        else:
            return frozenset({("DFLT", "dflt")})

    def start_feature(self, location, name):
        self.language_systems = self.get_default_language_systems_()
        self.script_ = "DFLT"
        self.cur_lookup_ = None
        self.cur_feature_name_ = name
        self.lookupflag_ = 0
        self.lookupflag_markFilterSet_ = None
        if name == "aalt":
            self.aalt_location_ = location

    def end_feature(self):
        assert self.cur_feature_name_ is not None
        self.cur_feature_name_ = None
        self.language_systems = None
        self.cur_lookup_ = None
        self.lookupflag_ = 0
        self.lookupflag_markFilterSet_ = None

    def start_lookup_block(self, location, name):
        if name in self.named_lookups_:
            raise FeatureLibError(
                'Lookup "%s" has already been defined' % name, location
            )
        if self.cur_feature_name_ == "aalt":
            raise FeatureLibError(
                "Lookup blocks cannot be placed inside 'aalt' features; "
                "move it out, and then refer to it with a lookup statement",
                location,
            )
        self.cur_lookup_name_ = name
        self.named_lookups_[name] = None
        self.cur_lookup_ = None
        if self.cur_feature_name_ is None:
            self.lookupflag_ = 0
            self.lookupflag_markFilterSet_ = None

    def end_lookup_block(self):
        assert self.cur_lookup_name_ is not None
        self.cur_lookup_name_ = None
        self.cur_lookup_ = None
        if self.cur_feature_name_ is None:
            self.lookupflag_ = 0
            self.lookupflag_markFilterSet_ = None

    def add_lookup_call(self, lookup_name):
        assert lookup_name in self.named_lookups_, lookup_name
        self.cur_lookup_ = None
        lookup = self.named_lookups_[lookup_name]
        self.add_lookup_to_feature_(lookup, self.cur_feature_name_)

    def set_font_revision(self, location, revision):
        self.fontRevision_ = revision

    def set_language(self, location, language, include_default, required):
        assert len(language) == 4
        if self.cur_feature_name_ in ("aalt", "size"):
            raise FeatureLibError(
                "Language statements are not allowed "
                'within "feature %s"' % self.cur_feature_name_,
                location,
            )
        if self.cur_feature_name_ is None:
            raise FeatureLibError(
                "Language statements are not allowed "
                "within standalone lookup blocks",
                location,
            )
        self.cur_lookup_ = None

        key = (self.script_, language, self.cur_feature_name_)
        lookups = self.features_.get((key[0], "dflt", key[2]))
        if (language == "dflt" or include_default) and lookups:
            self.features_[key] = lookups[:]
        else:
            self.features_[key] = []
        self.language_systems = frozenset([(self.script_, language)])

        if required:
            key = (self.script_, language)
            if key in self.required_features_:
                raise FeatureLibError(
                    "Language %s (script %s) has already "
                    "specified feature %s as its required feature"
                    % (
                        language.strip(),
                        self.script_.strip(),
                        self.required_features_[key].strip(),
                    ),
                    location,
                )
            self.required_features_[key] = self.cur_feature_name_

    def getMarkAttachClass_(self, location, glyphs):
        glyphs = frozenset(glyphs)
        id_ = self.markAttachClassID_.get(glyphs)
        if id_ is not None:
            return id_
        id_ = len(self.markAttachClassID_) + 1
        self.markAttachClassID_[glyphs] = id_
        for glyph in glyphs:
            if glyph in self.markAttach_:
                _, loc = self.markAttach_[glyph]
                raise FeatureLibError(
                    "Glyph %s already has been assigned "
                    "a MarkAttachmentType at %s" % (glyph, loc),
                    location,
                )
            self.markAttach_[glyph] = (id_, location)
        return id_

    def getMarkFilterSet_(self, location, glyphs):
        glyphs = frozenset(glyphs)
        id_ = self.markFilterSets_.get(glyphs)
        if id_ is not None:
            return id_
        id_ = len(self.markFilterSets_)
        self.markFilterSets_[glyphs] = id_
        return id_

    def set_lookup_flag(self, location, value, markAttach, markFilter):
        value = value & 0xFF
        if markAttach:
            markAttachClass = self.getMarkAttachClass_(location, markAttach)
            value = value | (markAttachClass << 8)
        if markFilter:
            markFilterSet = self.getMarkFilterSet_(location, markFilter)
            value = value | 0x10
            self.lookupflag_markFilterSet_ = markFilterSet
        else:
            self.lookupflag_markFilterSet_ = None
        self.lookupflag_ = value

    def set_script(self, location, script):
        if self.cur_feature_name_ in ("aalt", "size"):
            raise FeatureLibError(
                "Script statements are not allowed "
                'within "feature %s"' % self.cur_feature_name_,
                location,
            )
        if self.cur_feature_name_ is None:
            raise FeatureLibError(
                "Script statements are not allowed " "within standalone lookup blocks",
                location,
            )
        if self.language_systems == {(script, "dflt")}:
            # Nothing to do.
            return
        self.cur_lookup_ = None
        self.script_ = script
        self.lookupflag_ = 0
        self.lookupflag_markFilterSet_ = None
        self.set_language(location, "dflt", include_default=True, required=False)

    def find_lookup_builders_(self, lookups):
        """Helper for building chain contextual substitutions

        Given a list of lookup names, finds the LookupBuilder for each name.
        If an input name is None, it gets mapped to a None LookupBuilder.
        """
        lookup_builders = []
        for lookuplist in lookups:
            if lookuplist is not None:
                lookup_builders.append(
                    [self.named_lookups_.get(l.name) for l in lookuplist]
                )
            else:
                lookup_builders.append(None)
        return lookup_builders

    def add_attach_points(self, location, glyphs, contourPoints):
        for glyph in glyphs:
            self.attachPoints_.setdefault(glyph, set()).update(contourPoints)

    def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
        lookup = self.get_lookup_(location, ChainContextPosBuilder)
        lookup.rules.append(
            ChainContextualRule(
                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
            )
        )

    def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
        lookup = self.get_lookup_(location, ChainContextSubstBuilder)
        lookup.rules.append(
            ChainContextualRule(
                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
            )
        )

    def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
        if self.cur_feature_name_ == "aalt":
            alts = self.aalt_alternates_.setdefault(glyph, set())
            alts.update(replacement)
            return
        if prefix or suffix:
            chain = self.get_lookup_(location, ChainContextSubstBuilder)
            lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
            chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
        else:
            lookup = self.get_lookup_(location, AlternateSubstBuilder)
        if glyph in lookup.alternates:
            raise FeatureLibError(
                'Already defined alternates for glyph "%s"' % glyph, location
            )
        lookup.alternates[glyph] = replacement

    def add_feature_reference(self, location, featureName):
        if self.cur_feature_name_ != "aalt":
            raise FeatureLibError(
                'Feature references are only allowed inside "feature aalt"', location
            )
        self.aalt_features_.append((location, featureName))

    def add_featureName(self, tag):
        self.featureNames_.add(tag)

    def add_cv_parameter(self, tag):
        self.cv_parameters_.add(tag)

    def add_to_cv_num_named_params(self, tag):
        """Adds new items to ``self.cv_num_named_params_``
        or increments the count of existing items."""
        if tag in self.cv_num_named_params_:
            self.cv_num_named_params_[tag] += 1
        else:
            self.cv_num_named_params_[tag] = 1

    def add_cv_character(self, character, tag):
        self.cv_characters_[tag].append(character)

    def set_base_axis(self, bases, scripts, vertical):
        if vertical:
            self.base_vert_axis_ = (bases, scripts)
        else:
            self.base_horiz_axis_ = (bases, scripts)

    def set_size_parameters(
        self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
    ):
        if self.cur_feature_name_ != "size":
            raise FeatureLibError(
                "Parameters statements are not allowed "
                'within "feature %s"' % self.cur_feature_name_,
                location,
            )
        self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
        for script, lang in self.language_systems:
            key = (script, lang, self.cur_feature_name_)
            self.features_.setdefault(key, [])

    def add_ligature_subst(
        self, location, prefix, glyphs, suffix, replacement, forceChain
    ):
        if prefix or suffix or forceChain:
            chain = self.get_lookup_(location, ChainContextSubstBuilder)
            lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
            chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
        else:
            lookup = self.get_lookup_(location, LigatureSubstBuilder)

        # OpenType feature file syntax, section 5.d, "Ligature substitution":
        # "Since the OpenType specification does not allow ligature
        # substitutions to be specified on target sequences that contain
        # glyph classes, the implementation software will enumerate
        # all specific glyph sequences if glyph classes are detected"
        for g in sorted(itertools.product(*glyphs)):
            lookup.ligatures[g] = replacement

    def add_multiple_subst(
        self, location, prefix, glyph, suffix, replacements, forceChain=False
    ):
        if prefix or suffix or forceChain:
            chain = self.get_lookup_(location, ChainContextSubstBuilder)
            sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
            sub.mapping[glyph] = replacements
            chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
            return
        lookup = self.get_lookup_(location, MultipleSubstBuilder)
        if glyph in lookup.mapping:
            if replacements == lookup.mapping[glyph]:
                log.info(
                    "Removing duplicate multiple substitution from glyph"
                    ' "%s" to %s%s',
                    glyph,
                    replacements,
                    f" at {location}" if location else "",
                )
            else:
                raise FeatureLibError(
                    'Already defined substitution for glyph "%s"' % glyph, location
                )
        lookup.mapping[glyph] = replacements

    def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
        lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
        lookup.rules.append((old_prefix, old_suffix, mapping))

    def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
        if self.cur_feature_name_ == "aalt":
            for (from_glyph, to_glyph) in mapping.items():
                alts = self.aalt_alternates_.setdefault(from_glyph, set())
                alts.add(to_glyph)
            return
        if prefix or suffix or forceChain:
            self.add_single_subst_chained_(location, prefix, suffix, mapping)
            return
        lookup = self.get_lookup_(location, SingleSubstBuilder)
        for (from_glyph, to_glyph) in mapping.items():
            if from_glyph in lookup.mapping:
                if to_glyph == lookup.mapping[from_glyph]:
                    log.info(
                        "Removing duplicate single substitution from glyph"
                        ' "%s" to "%s" at %s',
                        from_glyph,
                        to_glyph,
                        location,
                    )
                else:
                    raise FeatureLibError(
                        'Already defined rule for replacing glyph "%s" by "%s"'
                        % (from_glyph, lookup.mapping[from_glyph]),
                        location,
                    )
            lookup.mapping[from_glyph] = to_glyph

    def add_single_subst_chained_(self, location, prefix, suffix, mapping):
        # https://github.com/fontemon_blender_addon.fontTools/fontemon_blender_addon.fontTools/issues/512
        chain = self.get_lookup_(location, ChainContextSubstBuilder)
        sub = chain.find_chainable_single_subst(set(mapping.keys()))
        if sub is None:
            sub = self.get_chained_lookup_(location, SingleSubstBuilder)
        sub.mapping.update(mapping)
        chain.rules.append(
            ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
        )

    def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
        lookup = self.get_lookup_(location, CursivePosBuilder)
        lookup.add_attachment(
            location,
            glyphclass,
            makeOpenTypeAnchor(entryAnchor),
            makeOpenTypeAnchor(exitAnchor),
        )

    def add_marks_(self, location, lookupBuilder, marks):
        """Helper for add_mark_{base,liga,mark}_pos."""
        for _, markClass in marks:
            for markClassDef in markClass.definitions:
                for mark in markClassDef.glyphs.glyphSet():
                    if mark not in lookupBuilder.marks:
                        otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor)
                        lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
                    else:
                        existingMarkClass = lookupBuilder.marks[mark][0]
                        if markClass.name != existingMarkClass:
                            raise FeatureLibError(
                                "Glyph %s cannot be in both @%s and @%s"
                                % (mark, existingMarkClass, markClass.name),
                                location,
                            )

    def add_mark_base_pos(self, location, bases, marks):
        builder = self.get_lookup_(location, MarkBasePosBuilder)
        self.add_marks_(location, builder, marks)
        for baseAnchor, markClass in marks:
            otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
            for base in bases:
                builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor

    def add_mark_lig_pos(self, location, ligatures, components):
        builder = self.get_lookup_(location, MarkLigPosBuilder)
        componentAnchors = []
        for marks in components:
            anchors = {}
            self.add_marks_(location, builder, marks)
            for ligAnchor, markClass in marks:
                anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor)
            componentAnchors.append(anchors)
        for glyph in ligatures:
            builder.ligatures[glyph] = componentAnchors

    def add_mark_mark_pos(self, location, baseMarks, marks):
        builder = self.get_lookup_(location, MarkMarkPosBuilder)
        self.add_marks_(location, builder, marks)
        for baseAnchor, markClass in marks:
            otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
            for baseMark in baseMarks:
                builder.baseMarks.setdefault(baseMark, {})[
                    markClass.name
                ] = otBaseAnchor

    def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
        lookup = self.get_lookup_(location, PairPosBuilder)
        v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
        v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
        lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)

    def add_subtable_break(self, location):
        self.cur_lookup_.add_subtable_break(location)

    def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
        lookup = self.get_lookup_(location, PairPosBuilder)
        v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
        v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
        lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)

    def add_single_pos(self, location, prefix, suffix, pos, forceChain):
        if prefix or suffix or forceChain:
            self.add_single_pos_chained_(location, prefix, suffix, pos)
        else:
            lookup = self.get_lookup_(location, SinglePosBuilder)
            for glyphs, value in pos:
                otValueRecord = makeOpenTypeValueRecord(value, pairPosContext=False)
                for glyph in glyphs:
                    try:
                        lookup.add_pos(location, glyph, otValueRecord)
                    except OpenTypeLibError as e:
                        raise FeatureLibError(str(e), e.location) from e

    def add_single_pos_chained_(self, location, prefix, suffix, pos):
        # https://github.com/fontemon_blender_addon.fontTools/fontemon_blender_addon.fontTools/issues/514
        chain = self.get_lookup_(location, ChainContextPosBuilder)
        targets = []
        for _, _, _, lookups in chain.rules:
            targets.extend(lookups)
        subs = []
        for glyphs, value in pos:
            if value is None:
                subs.append(None)
                continue
            otValue = makeOpenTypeValueRecord(value, pairPosContext=False)
            sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
            if sub is None:
                sub = self.get_chained_lookup_(location, SinglePosBuilder)
                targets.append(sub)
            for glyph in glyphs:
                sub.add_pos(location, glyph, otValue)
            subs.append(sub)
        assert len(pos) == len(subs), (pos, subs)
        chain.rules.append(
            ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
        )

    def setGlyphClass_(self, location, glyph, glyphClass):
        oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
        if oldClass and oldClass != glyphClass:
            raise FeatureLibError(
                "Glyph %s was assigned to a different class at %s"
                % (glyph, oldLocation),
                location,
            )
        self.glyphClassDefs_[glyph] = (glyphClass, location)

    def add_glyphClassDef(
        self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
    ):
        for glyph in baseGlyphs:
            self.setGlyphClass_(location, glyph, 1)
        for glyph in ligatureGlyphs:
            self.setGlyphClass_(location, glyph, 2)
        for glyph in markGlyphs:
            self.setGlyphClass_(location, glyph, 3)
        for glyph in componentGlyphs:
            self.setGlyphClass_(location, glyph, 4)

    def add_ligatureCaretByIndex_(self, location, glyphs, carets):
        for glyph in glyphs:
            if glyph not in self.ligCaretPoints_:
                self.ligCaretPoints_[glyph] = carets

    def add_ligatureCaretByPos_(self, location, glyphs, carets):
        for glyph in glyphs:
            if glyph not in self.ligCaretCoords_:
                self.ligCaretCoords_[glyph] = carets

    def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
        self.names_.append([nameID, platformID, platEncID, langID, string])

    def add_os2_field(self, key, value):
        self.os2_[key] = value

    def add_hhea_field(self, key, value):
        self.hhea_[key] = value

    def add_vhea_field(self, key, value):
        self.vhea_[key] = value


def makeOpenTypeAnchor(anchor):
    """ast.Anchor --> otTables.Anchor"""
    if anchor is None:
        return None
    deviceX, deviceY = None, None
    if anchor.xDeviceTable is not None:
        deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
    if anchor.yDeviceTable is not None:
        deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
    return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)


_VALUEREC_ATTRS = {
    name[0].lower() + name[1:]: (name, isDevice)
    for _, name, isDevice, _ in otBase.valueRecordFormat
    if not name.startswith("Reserved")
}


def makeOpenTypeValueRecord(v, pairPosContext):
    """ast.ValueRecord --> otBase.ValueRecord"""
    if not v:
        return None

    vr = {}
    for astName, (otName, isDevice) in _VALUEREC_ATTRS.items():
        val = getattr(v, astName, None)
        if val:
            vr[otName] = otl.buildDevice(dict(val)) if isDevice else val
    if pairPosContext and not vr:
        vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
    valRec = otl.buildValue(vr)
    return valRec
