from fontTools.misc.fixedTools import (
    fixedToFloat as fi2fl,
    floatToFixed as fl2fi,
    floatToFixedToStr as fl2str,
    strToFixedToFloat as str2fl,
    ensureVersionIsLong as fi2ve,
    versionToFixed as ve2fi,
)
from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
from fontTools.ttLib import getSearchRange
from .otBase import (
    CountReference,
    FormatSwitchingBaseTable,
    OTTableReader,
    OTTableWriter,
    ValueRecordFactory,
)
from .otTables import (
    lookupTypes,
    AATStateTable,
    AATState,
    AATAction,
    ContextualMorphAction,
    LigatureMorphAction,
    InsertionMorphAction,
    MorxSubtable,
    ExtendMode as _ExtendMode,
    CompositeMode as _CompositeMode,
    NO_VARIATION_INDEX,
)
from itertools import zip_longest
from functools import partial
import re
import struct
from typing import Optional
import logging


log = logging.getLogger(__name__)
istuple = lambda t: isinstance(t, tuple)


def buildConverters(tableSpec, tableNamespace):
    """Given a table spec from otData.py, build a converter object for each
    field of the table. This is called for each table in otData.py, and
    the results are assigned to the corresponding class in otTables.py."""
    converters = []
    convertersByName = {}
    for tp, name, repeat, aux, descr in tableSpec:
        tableName = name
        if name.startswith("ValueFormat"):
            assert tp == "uint16"
            converterClass = ValueFormat
        elif name.endswith("Count") or name in ("StructLength", "MorphType"):
            converterClass = {
                "uint8": ComputedUInt8,
                "uint16": ComputedUShort,
                "uint32": ComputedULong,
            }[tp]
        elif name == "SubTable":
            converterClass = SubTable
        elif name == "ExtSubTable":
            converterClass = ExtSubTable
        elif name == "SubStruct":
            converterClass = SubStruct
        elif name == "FeatureParams":
            converterClass = FeatureParams
        elif name in ("CIDGlyphMapping", "GlyphCIDMapping"):
            converterClass = StructWithLength
        else:
            if not tp in converterMapping and "(" not in tp:
                tableName = tp
                converterClass = Struct
            else:
                converterClass = eval(tp, tableNamespace, converterMapping)

        conv = converterClass(name, repeat, aux, description=descr)

        if conv.tableClass:
            # A "template" such as OffsetTo(AType) knowss the table class already
            tableClass = conv.tableClass
        elif tp in ("MortChain", "MortSubtable", "MorxChain"):
            tableClass = tableNamespace.get(tp)
        else:
            tableClass = tableNamespace.get(tableName)

        if not conv.tableClass:
            conv.tableClass = tableClass

        if name in ["SubTable", "ExtSubTable", "SubStruct"]:
            conv.lookupTypes = tableNamespace["lookupTypes"]
            # also create reverse mapping
            for t in conv.lookupTypes.values():
                for cls in t.values():
                    convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
        if name == "FeatureParams":
            conv.featureParamTypes = tableNamespace["featureParamTypes"]
            conv.defaultFeatureParams = tableNamespace["FeatureParams"]
            for cls in conv.featureParamTypes.values():
                convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
        converters.append(conv)
        assert name not in convertersByName, name
        convertersByName[name] = conv
    return converters, convertersByName


class _MissingItem(tuple):
    __slots__ = ()


try:
    from collections import UserList
except ImportError:
    from UserList import UserList


class _LazyList(UserList):
    def __getslice__(self, i, j):
        return self.__getitem__(slice(i, j))

    def __getitem__(self, k):
        if isinstance(k, slice):
            indices = range(*k.indices(len(self)))
            return [self[i] for i in indices]
        item = self.data[k]
        if isinstance(item, _MissingItem):
            self.reader.seek(self.pos + item[0] * self.recordSize)
            item = self.conv.read(self.reader, self.font, {})
            self.data[k] = item
        return item

    def __add__(self, other):
        if isinstance(other, _LazyList):
            other = list(other)
        elif isinstance(other, list):
            pass
        else:
            return NotImplemented
        return list(self) + other

    def __radd__(self, other):
        if not isinstance(other, list):
            return NotImplemented
        return other + list(self)


class BaseConverter(object):
    """Base class for converter objects. Apart from the constructor, this
    is an abstract class."""

    def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
        self.name = name
        self.repeat = repeat
        self.aux = aux
        if self.aux and not self.repeat:
            self.aux = compile(self.aux, "<string>", "eval")
        self.tableClass = tableClass
        self.isCount = name.endswith("Count") or name in [
            "DesignAxisRecordSize",
            "ValueRecordSize",
        ]
        self.isLookupType = name.endswith("LookupType") or name == "MorphType"
        self.isPropagated = name in [
            "ClassCount",
            "Class2Count",
            "FeatureTag",
            "SettingsCount",
            "VarRegionCount",
            "MappingCount",
            "RegionAxisCount",
            "DesignAxisCount",
            "DesignAxisRecordSize",
            "AxisValueCount",
            "ValueRecordSize",
            "AxisCount",
            "BaseGlyphRecordCount",
            "LayerRecordCount",
        ]
        self.description = description

    def readArray(self, reader, font, tableDict, count):
        """Read an array of values from the reader."""
        lazy = font.lazy and count > 8
        if lazy:
            recordSize = self.getRecordSize(reader)
            if recordSize is NotImplemented:
                lazy = False
        if not lazy:
            l = []
            for i in range(count):
                l.append(self.read(reader, font, tableDict))
            return l
        else:
            l = _LazyList()
            l.reader = reader.copy()
            l.pos = l.reader.pos
            l.font = font
            l.conv = self
            l.recordSize = recordSize
            l.extend(_MissingItem([i]) for i in range(count))
            reader.advance(count * recordSize)
            return l

    def getRecordSize(self, reader):
        if hasattr(self, "staticSize"):
            return self.staticSize
        return NotImplemented

    def read(self, reader, font, tableDict):
        """Read a value from the reader."""
        raise NotImplementedError(self)

    def writeArray(self, writer, font, tableDict, values):
        try:
            for i, value in enumerate(values):
                self.write(writer, font, tableDict, value, i)
        except Exception as e:
            e.args = e.args + (i,)
            raise

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        """Write a value to the writer."""
        raise NotImplementedError(self)

    def xmlRead(self, attrs, content, font):
        """Read a value from XML."""
        raise NotImplementedError(self)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        """Write a value to XML."""
        raise NotImplementedError(self)

    varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")

    def getVarIndexOffset(self) -> Optional[int]:
        """If description has `VarIndexBase + {offset}`, return the offset else None."""
        m = self.varIndexBasePlusOffsetRE.search(self.description)
        if not m:
            return None
        return int(m.group(1))


class SimpleValue(BaseConverter):
    @staticmethod
    def toString(value):
        return value

    @staticmethod
    def fromString(value):
        return value

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        return self.fromString(attrs["value"])


class OptionalValue(SimpleValue):
    DEFAULT = None

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        if value != self.DEFAULT:
            attrs.append(("value", self.toString(value)))
        xmlWriter.simpletag(name, attrs)
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        if "value" in attrs:
            return self.fromString(attrs["value"])
        return self.DEFAULT


class IntValue(SimpleValue):
    @staticmethod
    def fromString(value):
        return int(value, 0)


class Long(IntValue):
    staticSize = 4

    def read(self, reader, font, tableDict):
        return reader.readLong()

    def readArray(self, reader, font, tableDict, count):
        return reader.readLongArray(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeLong(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeLongArray(values)


class ULong(IntValue):
    staticSize = 4

    def read(self, reader, font, tableDict):
        return reader.readULong()

    def readArray(self, reader, font, tableDict, count):
        return reader.readULongArray(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeULong(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeULongArray(values)


class Flags32(ULong):
    @staticmethod
    def toString(value):
        return "0x%08X" % value


class VarIndex(OptionalValue, ULong):
    DEFAULT = NO_VARIATION_INDEX


class Short(IntValue):
    staticSize = 2

    def read(self, reader, font, tableDict):
        return reader.readShort()

    def readArray(self, reader, font, tableDict, count):
        return reader.readShortArray(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeShort(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeShortArray(values)


class UShort(IntValue):
    staticSize = 2

    def read(self, reader, font, tableDict):
        return reader.readUShort()

    def readArray(self, reader, font, tableDict, count):
        return reader.readUShortArray(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeUShort(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeUShortArray(values)


class Int8(IntValue):
    staticSize = 1

    def read(self, reader, font, tableDict):
        return reader.readInt8()

    def readArray(self, reader, font, tableDict, count):
        return reader.readInt8Array(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeInt8(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeInt8Array(values)


class UInt8(IntValue):
    staticSize = 1

    def read(self, reader, font, tableDict):
        return reader.readUInt8()

    def readArray(self, reader, font, tableDict, count):
        return reader.readUInt8Array(count)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeUInt8(value)

    def writeArray(self, writer, font, tableDict, values):
        writer.writeUInt8Array(values)


class UInt24(IntValue):
    staticSize = 3

    def read(self, reader, font, tableDict):
        return reader.readUInt24()

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeUInt24(value)


class ComputedInt(IntValue):
    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        if value is not None:
            xmlWriter.comment("%s=%s" % (name, value))
            xmlWriter.newline()


class ComputedUInt8(ComputedInt, UInt8):
    pass


class ComputedUShort(ComputedInt, UShort):
    pass


class ComputedULong(ComputedInt, ULong):
    pass


class Tag(SimpleValue):
    staticSize = 4

    def read(self, reader, font, tableDict):
        return reader.readTag()

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeTag(value)


class GlyphID(SimpleValue):
    staticSize = 2
    typecode = "H"

    def readArray(self, reader, font, tableDict, count):
        return font.getGlyphNameMany(
            reader.readArray(self.typecode, self.staticSize, count)
        )

    def read(self, reader, font, tableDict):
        return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))

    def writeArray(self, writer, font, tableDict, values):
        writer.writeArray(self.typecode, font.getGlyphIDMany(values))

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeValue(self.typecode, font.getGlyphID(value))


class GlyphID32(GlyphID):
    staticSize = 4
    typecode = "L"


class NameID(UShort):
    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", value)])
        if font and value:
            nameTable = font.get("name")
            if nameTable:
                name = nameTable.getDebugName(value)
                xmlWriter.write("  ")
                if name:
                    xmlWriter.comment(name)
                else:
                    xmlWriter.comment("missing from name table")
                    log.warning("name id %d missing from name table" % value)
        xmlWriter.newline()


class STATFlags(UShort):
    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", value)])
        flags = []
        if value & 0x01:
            flags.append("OlderSiblingFontAttribute")
        if value & 0x02:
            flags.append("ElidableAxisValueName")
        if flags:
            xmlWriter.write("  ")
            xmlWriter.comment(" ".join(flags))
        xmlWriter.newline()


class FloatValue(SimpleValue):
    @staticmethod
    def fromString(value):
        return float(value)


class DeciPoints(FloatValue):
    staticSize = 2

    def read(self, reader, font, tableDict):
        return reader.readUShort() / 10

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.writeUShort(round(value * 10))


class BaseFixedValue(FloatValue):
    staticSize = NotImplemented
    precisionBits = NotImplemented
    readerMethod = NotImplemented
    writerMethod = NotImplemented

    def read(self, reader, font, tableDict):
        return self.fromInt(getattr(reader, self.readerMethod)())

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        getattr(writer, self.writerMethod)(self.toInt(value))

    @classmethod
    def fromInt(cls, value):
        return fi2fl(value, cls.precisionBits)

    @classmethod
    def toInt(cls, value):
        return fl2fi(value, cls.precisionBits)

    @classmethod
    def fromString(cls, value):
        return str2fl(value, cls.precisionBits)

    @classmethod
    def toString(cls, value):
        return fl2str(value, cls.precisionBits)


class Fixed(BaseFixedValue):
    staticSize = 4
    precisionBits = 16
    readerMethod = "readLong"
    writerMethod = "writeLong"


class F2Dot14(BaseFixedValue):
    staticSize = 2
    precisionBits = 14
    readerMethod = "readShort"
    writerMethod = "writeShort"


class Angle(F2Dot14):
    # angles are specified in degrees, and encoded as F2Dot14 fractions of half
    # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
    bias = 0.0
    factor = 1.0 / (1 << 14) * 180  # 0.010986328125

    @classmethod
    def fromInt(cls, value):
        return (super().fromInt(value) + cls.bias) * 180

    @classmethod
    def toInt(cls, value):
        return super().toInt((value / 180) - cls.bias)

    @classmethod
    def fromString(cls, value):
        # quantize to nearest multiples of minimum fixed-precision angle
        return otRound(float(value) / cls.factor) * cls.factor

    @classmethod
    def toString(cls, value):
        return nearestMultipleShortestRepr(value, cls.factor)


class BiasedAngle(Angle):
    # A bias of 1.0 is used in the representation of start and end angles
    # of COLRv1 PaintSweepGradients to allow for encoding +360deg
    bias = 1.0


class Version(SimpleValue):
    staticSize = 4

    def read(self, reader, font, tableDict):
        value = reader.readLong()
        return value

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        value = fi2ve(value)
        writer.writeLong(value)

    @staticmethod
    def fromString(value):
        return ve2fi(value)

    @staticmethod
    def toString(value):
        return "0x%08x" % value

    @staticmethod
    def fromFloat(v):
        return fl2fi(v, 16)


class Char64(SimpleValue):
    """An ASCII string with up to 64 characters.

    Unused character positions are filled with 0x00 bytes.
    Used in Apple AAT fonts in the `gcid` table.
    """

    staticSize = 64

    def read(self, reader, font, tableDict):
        data = reader.readData(self.staticSize)
        zeroPos = data.find(b"\0")
        if zeroPos >= 0:
            data = data[:zeroPos]
        s = tostr(data, encoding="ascii", errors="replace")
        if s != tostr(data, encoding="ascii", errors="ignore"):
            log.warning('replaced non-ASCII characters in "%s"' % s)
        return s

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        data = tobytes(value, encoding="ascii", errors="replace")
        if data != tobytes(value, encoding="ascii", errors="ignore"):
            log.warning('replacing non-ASCII characters in "%s"' % value)
        if len(data) > self.staticSize:
            log.warning(
                'truncating overlong "%s" to %d bytes' % (value, self.staticSize)
            )
        data = (data + b"\0" * self.staticSize)[: self.staticSize]
        writer.writeData(data)


class Struct(BaseConverter):
    def getRecordSize(self, reader):
        return self.tableClass and self.tableClass.getRecordSize(reader)

    def read(self, reader, font, tableDict):
        table = self.tableClass()
        table.decompile(reader, font)
        return table

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        value.compile(writer, font)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        if value is None:
            if attrs:
                # If there are attributes (probably index), then
                # don't drop this even if it's NULL.  It will mess
                # up the array indices of the containing element.
                xmlWriter.simpletag(name, attrs + [("empty", 1)])
                xmlWriter.newline()
            else:
                pass  # NULL table, ignore
        else:
            value.toXML(xmlWriter, font, attrs, name=name)

    def xmlRead(self, attrs, content, font):
        if "empty" in attrs and safeEval(attrs["empty"]):
            return None
        table = self.tableClass()
        Format = attrs.get("Format")
        if Format is not None:
            table.Format = int(Format)

        noPostRead = not hasattr(table, "postRead")
        if noPostRead:
            # TODO Cache table.hasPropagated.
            cleanPropagation = False
            for conv in table.getConverters():
                if conv.isPropagated:
                    cleanPropagation = True
                    if not hasattr(font, "_propagator"):
                        font._propagator = {}
                    propagator = font._propagator
                    assert conv.name not in propagator, (conv.name, propagator)
                    setattr(table, conv.name, None)
                    propagator[conv.name] = CountReference(table.__dict__, conv.name)

        for element in content:
            if isinstance(element, tuple):
                name, attrs, content = element
                table.fromXML(name, attrs, content, font)
            else:
                pass

        table.populateDefaults(propagator=getattr(font, "_propagator", None))

        if noPostRead:
            if cleanPropagation:
                for conv in table.getConverters():
                    if conv.isPropagated:
                        propagator = font._propagator
                        del propagator[conv.name]
                        if not propagator:
                            del font._propagator

        return table

    def __repr__(self):
        return "Struct of " + repr(self.tableClass)


class StructWithLength(Struct):
    def read(self, reader, font, tableDict):
        pos = reader.pos
        table = self.tableClass()
        table.decompile(reader, font)
        reader.seek(pos + table.StructLength)
        return table

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        for convIndex, conv in enumerate(value.getConverters()):
            if conv.name == "StructLength":
                break
        lengthIndex = len(writer.items) + convIndex
        if isinstance(value, FormatSwitchingBaseTable):
            lengthIndex += 1  # implicit Format field
        deadbeef = {1: 0xDE, 2: 0xDEAD, 4: 0xDEADBEEF}[conv.staticSize]

        before = writer.getDataLength()
        value.StructLength = deadbeef
        value.compile(writer, font)
        length = writer.getDataLength() - before
        lengthWriter = writer.getSubWriter()
        conv.write(lengthWriter, font, tableDict, length)
        assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"[: conv.staticSize]
        writer.items[lengthIndex] = lengthWriter.getAllData()


class Table(Struct):
    staticSize = 2

    def readOffset(self, reader):
        return reader.readUShort()

    def writeNullOffset(self, writer):
        writer.writeUShort(0)

    def read(self, reader, font, tableDict):
        offset = self.readOffset(reader)
        if offset == 0:
            return None
        table = self.tableClass()
        reader = reader.getSubReader(offset)
        if font.lazy:
            table.reader = reader
            table.font = font
        else:
            table.decompile(reader, font)
        return table

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        if value is None:
            self.writeNullOffset(writer)
        else:
            subWriter = writer.getSubWriter()
            subWriter.name = self.name
            if repeatIndex is not None:
                subWriter.repeatIndex = repeatIndex
            writer.writeSubTable(subWriter, offsetSize=self.staticSize)
            value.compile(subWriter, font)


class LTable(Table):
    staticSize = 4

    def readOffset(self, reader):
        return reader.readULong()

    def writeNullOffset(self, writer):
        writer.writeULong(0)


# Table pointed to by a 24-bit, 3-byte long offset
class Table24(Table):
    staticSize = 3

    def readOffset(self, reader):
        return reader.readUInt24()

    def writeNullOffset(self, writer):
        writer.writeUInt24(0)


# TODO Clean / merge the SubTable and SubStruct


class SubStruct(Struct):
    def getConverter(self, tableType, lookupType):
        tableClass = self.lookupTypes[tableType][lookupType]
        return self.__class__(self.name, self.repeat, self.aux, tableClass)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs)


class SubTable(Table):
    def getConverter(self, tableType, lookupType):
        tableClass = self.lookupTypes[tableType][lookupType]
        return self.__class__(self.name, self.repeat, self.aux, tableClass)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs)


class ExtSubTable(LTable, SubTable):
    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer.Extension = True  # actually, mere presence of the field flags it as an Ext Subtable writer.
        Table.write(self, writer, font, tableDict, value, repeatIndex)


class FeatureParams(Table):
    def getConverter(self, featureTag):
        tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams)
        return self.__class__(self.name, self.repeat, self.aux, tableClass)


class ValueFormat(IntValue):
    staticSize = 2

    def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
        BaseConverter.__init__(
            self, name, repeat, aux, tableClass, description=description
        )
        self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")

    def read(self, reader, font, tableDict):
        format = reader.readUShort()
        reader[self.which] = ValueRecordFactory(format)
        return format

    def write(self, writer, font, tableDict, format, repeatIndex=None):
        writer.writeUShort(format)
        writer[self.which] = ValueRecordFactory(format)


class ValueRecord(ValueFormat):
    def getRecordSize(self, reader):
        return 2 * len(reader[self.which])

    def read(self, reader, font, tableDict):
        return reader[self.which].readValueRecord(reader, font)

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        writer[self.which].writeValueRecord(writer, font, value)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        if value is None:
            pass  # NULL table, ignore
        else:
            value.toXML(xmlWriter, font, self.name, attrs)

    def xmlRead(self, attrs, content, font):
        from .otBase import ValueRecord

        value = ValueRecord()
        value.fromXML(None, attrs, content, font)
        return value


class AATLookup(BaseConverter):
    BIN_SEARCH_HEADER_SIZE = 10

    def __init__(self, name, repeat, aux, tableClass, *, description=""):
        BaseConverter.__init__(
            self, name, repeat, aux, tableClass, description=description
        )
        if issubclass(self.tableClass, SimpleValue):
            self.converter = self.tableClass(name="Value", repeat=None, aux=None)
        else:
            self.converter = Table(
                name="Value", repeat=None, aux=None, tableClass=self.tableClass
            )

    def read(self, reader, font, tableDict):
        format = reader.readUShort()
        if format == 0:
            return self.readFormat0(reader, font)
        elif format == 2:
            return self.readFormat2(reader, font)
        elif format == 4:
            return self.readFormat4(reader, font)
        elif format == 6:
            return self.readFormat6(reader, font)
        elif format == 8:
            return self.readFormat8(reader, font)
        else:
            assert False, "unsupported lookup format: %d" % format

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        values = list(
            sorted([(font.getGlyphID(glyph), val) for glyph, val in value.items()])
        )
        # TODO: Also implement format 4.
        formats = list(
            sorted(
                filter(
                    None,
                    [
                        self.buildFormat0(writer, font, values),
                        self.buildFormat2(writer, font, values),
                        self.buildFormat6(writer, font, values),
                        self.buildFormat8(writer, font, values),
                    ],
                )
            )
        )
        # We use the format ID as secondary sort key to make the output
        # deterministic when multiple formats have same encoded size.
        dataSize, lookupFormat, writeMethod = formats[0]
        pos = writer.getDataLength()
        writeMethod()
        actualSize = writer.getDataLength() - pos
        assert (
            actualSize == dataSize
        ), "AATLookup format %d claimed to write %d bytes, but wrote %d" % (
            lookupFormat,
            dataSize,
            actualSize,
        )

    @staticmethod
    def writeBinSearchHeader(writer, numUnits, unitSize):
        writer.writeUShort(unitSize)
        writer.writeUShort(numUnits)
        searchRange, entrySelector, rangeShift = getSearchRange(
            n=numUnits, itemSize=unitSize
        )
        writer.writeUShort(searchRange)
        writer.writeUShort(entrySelector)
        writer.writeUShort(rangeShift)

    def buildFormat0(self, writer, font, values):
        numGlyphs = len(font.getGlyphOrder())
        if len(values) != numGlyphs:
            return None
        valueSize = self.converter.staticSize
        return (
            2 + numGlyphs * valueSize,
            0,
            lambda: self.writeFormat0(writer, font, values),
        )

    def writeFormat0(self, writer, font, values):
        writer.writeUShort(0)
        for glyphID_, value in values:
            self.converter.write(
                writer, font, tableDict=None, value=value, repeatIndex=None
            )

    def buildFormat2(self, writer, font, values):
        segStart, segValue = values[0]
        segEnd = segStart
        segments = []
        for glyphID, curValue in values[1:]:
            if glyphID != segEnd + 1 or curValue != segValue:
                segments.append((segStart, segEnd, segValue))
                segStart = segEnd = glyphID
                segValue = curValue
            else:
                segEnd = glyphID
        segments.append((segStart, segEnd, segValue))
        valueSize = self.converter.staticSize
        numUnits, unitSize = len(segments) + 1, valueSize + 4
        return (
            2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize,
            2,
            lambda: self.writeFormat2(writer, font, segments),
        )

    def writeFormat2(self, writer, font, segments):
        writer.writeUShort(2)
        valueSize = self.converter.staticSize
        numUnits, unitSize = len(segments), valueSize + 4
        self.writeBinSearchHeader(writer, numUnits, unitSize)
        for firstGlyph, lastGlyph, value in segments:
            writer.writeUShort(lastGlyph)
            writer.writeUShort(firstGlyph)
            self.converter.write(
                writer, font, tableDict=None, value=value, repeatIndex=None
            )
        writer.writeUShort(0xFFFF)
        writer.writeUShort(0xFFFF)
        writer.writeData(b"\x00" * valueSize)

    def buildFormat6(self, writer, font, values):
        valueSize = self.converter.staticSize
        numUnits, unitSize = len(values), valueSize + 2
        return (
            2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize,
            6,
            lambda: self.writeFormat6(writer, font, values),
        )

    def writeFormat6(self, writer, font, values):
        writer.writeUShort(6)
        valueSize = self.converter.staticSize
        numUnits, unitSize = len(values), valueSize + 2
        self.writeBinSearchHeader(writer, numUnits, unitSize)
        for glyphID, value in values:
            writer.writeUShort(glyphID)
            self.converter.write(
                writer, font, tableDict=None, value=value, repeatIndex=None
            )
        writer.writeUShort(0xFFFF)
        writer.writeData(b"\x00" * valueSize)

    def buildFormat8(self, writer, font, values):
        minGlyphID, maxGlyphID = values[0][0], values[-1][0]
        if len(values) != maxGlyphID - minGlyphID + 1:
            return None
        valueSize = self.converter.staticSize
        return (
            6 + len(values) * valueSize,
            8,
            lambda: self.writeFormat8(writer, font, values),
        )

    def writeFormat8(self, writer, font, values):
        firstGlyphID = values[0][0]
        writer.writeUShort(8)
        writer.writeUShort(firstGlyphID)
        writer.writeUShort(len(values))
        for _, value in values:
            self.converter.write(
                writer, font, tableDict=None, value=value, repeatIndex=None
            )

    def readFormat0(self, reader, font):
        numGlyphs = len(font.getGlyphOrder())
        data = self.converter.readArray(reader, font, tableDict=None, count=numGlyphs)
        return {font.getGlyphName(k): value for k, value in enumerate(data)}

    def readFormat2(self, reader, font):
        mapping = {}
        pos = reader.pos - 2  # start of table is at UShort for format
        unitSize, numUnits = reader.readUShort(), reader.readUShort()
        assert unitSize >= 4 + self.converter.staticSize, unitSize
        for i in range(numUnits):
            reader.seek(pos + i * unitSize + 12)
            last = reader.readUShort()
            first = reader.readUShort()
            value = self.converter.read(reader, font, tableDict=None)
            if last != 0xFFFF:
                for k in range(first, last + 1):
                    mapping[font.getGlyphName(k)] = value
        return mapping

    def readFormat4(self, reader, font):
        mapping = {}
        pos = reader.pos - 2  # start of table is at UShort for format
        unitSize = reader.readUShort()
        assert unitSize >= 6, unitSize
        for i in range(reader.readUShort()):
            reader.seek(pos + i * unitSize + 12)
            last = reader.readUShort()
            first = reader.readUShort()
            offset = reader.readUShort()
            if last != 0xFFFF:
                dataReader = reader.getSubReader(0)  # relative to current position
                dataReader.seek(pos + offset)  # relative to start of table
                data = self.converter.readArray(
                    dataReader, font, tableDict=None, count=last - first + 1
                )
                for k, v in enumerate(data):
                    mapping[font.getGlyphName(first + k)] = v
        return mapping

    def readFormat6(self, reader, font):
        mapping = {}
        pos = reader.pos - 2  # start of table is at UShort for format
        unitSize = reader.readUShort()
        assert unitSize >= 2 + self.converter.staticSize, unitSize
        for i in range(reader.readUShort()):
            reader.seek(pos + i * unitSize + 12)
            glyphID = reader.readUShort()
            value = self.converter.read(reader, font, tableDict=None)
            if glyphID != 0xFFFF:
                mapping[font.getGlyphName(glyphID)] = value
        return mapping

    def readFormat8(self, reader, font):
        first = reader.readUShort()
        count = reader.readUShort()
        data = self.converter.readArray(reader, font, tableDict=None, count=count)
        return {font.getGlyphName(first + k): value for (k, value) in enumerate(data)}

    def xmlRead(self, attrs, content, font):
        value = {}
        for element in content:
            if isinstance(element, tuple):
                name, a, eltContent = element
                if name == "Lookup":
                    value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font)
        return value

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.begintag(name, attrs)
        xmlWriter.newline()
        for glyph, value in sorted(value.items()):
            self.converter.xmlWrite(
                xmlWriter, font, value=value, name="Lookup", attrs=[("glyph", glyph)]
            )
        xmlWriter.endtag(name)
        xmlWriter.newline()


# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup
# followed by an offset to a glyph data table. Other than usual, the
# offsets in the AATLookup are not relative to the beginning of
# the beginning of the 'ankr' table, but relative to the glyph data table.
# So, to find the anchor data for a glyph, one needs to add the offset
# to the data table to the offset found in the AATLookup, and then use
# the sum of these two offsets to find the actual data.
class AATLookupWithDataOffset(BaseConverter):
    def read(self, reader, font, tableDict):
        lookupOffset = reader.readULong()
        dataOffset = reader.readULong()
        lookupReader = reader.getSubReader(lookupOffset)
        lookup = AATLookup("DataOffsets", None, None, UShort)
        offsets = lookup.read(lookupReader, font, tableDict)
        result = {}
        for glyph, offset in offsets.items():
            dataReader = reader.getSubReader(offset + dataOffset)
            item = self.tableClass()
            item.decompile(dataReader, font)
            result[glyph] = item
        return result

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        # We do not work with OTTableWriter sub-writers because
        # the offsets in our AATLookup are relative to our data
        # table, for which we need to provide an offset value itself.
        # It might have been possible to somehow make a kludge for
        # performing this indirect offset computation directly inside
        # OTTableWriter. But this would have made the internal logic
        # of OTTableWriter even more complex than it already is,
        # so we decided to roll our own offset computation for the
        # contents of the AATLookup and associated data table.
        offsetByGlyph, offsetByData, dataLen = {}, {}, 0
        compiledData = []
        for glyph in sorted(value, key=font.getGlyphID):
            subWriter = OTTableWriter()
            value[glyph].compile(subWriter, font)
            data = subWriter.getAllData()
            offset = offsetByData.get(data, None)
            if offset == None:
                offset = dataLen
                dataLen = dataLen + len(data)
                offsetByData[data] = offset
                compiledData.append(data)
            offsetByGlyph[glyph] = offset
        # For calculating the offsets to our AATLookup and data table,
        # we can use the regular OTTableWriter infrastructure.
        lookupWriter = writer.getSubWriter()
        lookup = AATLookup("DataOffsets", None, None, UShort)
        lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)

        dataWriter = writer.getSubWriter()
        writer.writeSubTable(lookupWriter, offsetSize=4)
        writer.writeSubTable(dataWriter, offsetSize=4)
        for d in compiledData:
            dataWriter.writeData(d)

    def xmlRead(self, attrs, content, font):
        lookup = AATLookup("DataOffsets", None, None, self.tableClass)
        return lookup.xmlRead(attrs, content, font)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        lookup = AATLookup("DataOffsets", None, None, self.tableClass)
        lookup.xmlWrite(xmlWriter, font, value, name, attrs)


class MorxSubtableConverter(BaseConverter):
    _PROCESSING_ORDERS = {
        # bits 30 and 28 of morx.CoverageFlags; see morx spec
        (False, False): "LayoutOrder",
        (True, False): "ReversedLayoutOrder",
        (False, True): "LogicalOrder",
        (True, True): "ReversedLogicalOrder",
    }

    _PROCESSING_ORDERS_REVERSED = {val: key for key, val in _PROCESSING_ORDERS.items()}

    def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
        BaseConverter.__init__(
            self, name, repeat, aux, tableClass, description=description
        )

    def _setTextDirectionFromCoverageFlags(self, flags, subtable):
        if (flags & 0x20) != 0:
            subtable.TextDirection = "Any"
        elif (flags & 0x80) != 0:
            subtable.TextDirection = "Vertical"
        else:
            subtable.TextDirection = "Horizontal"

    def read(self, reader, font, tableDict):
        pos = reader.pos
        m = MorxSubtable()
        m.StructLength = reader.readULong()
        flags = reader.readUInt8()
        orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0)
        m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey]
        self._setTextDirectionFromCoverageFlags(flags, m)
        m.Reserved = reader.readUShort()
        m.Reserved |= (flags & 0xF) << 16
        m.MorphType = reader.readUInt8()
        m.SubFeatureFlags = reader.readULong()
        tableClass = lookupTypes["morx"].get(m.MorphType)
        if tableClass is None:
            assert False, "unsupported 'morx' lookup type %s" % m.MorphType
        # To decode AAT ligatures, we need to know the subtable size.
        # The easiest way to pass this along is to create a new reader
        # that works on just the subtable as its data.
        headerLength = reader.pos - pos
        data = reader.data[reader.pos : reader.pos + m.StructLength - headerLength]
        assert len(data) == m.StructLength - headerLength
        subReader = OTTableReader(data=data, tableTag=reader.tableTag)
        m.SubStruct = tableClass()
        m.SubStruct.decompile(subReader, font)
        reader.seek(pos + m.StructLength)
        return m

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.begintag(name, attrs)
        xmlWriter.newline()
        xmlWriter.comment("StructLength=%d" % value.StructLength)
        xmlWriter.newline()
        xmlWriter.simpletag("TextDirection", value=value.TextDirection)
        xmlWriter.newline()
        xmlWriter.simpletag("ProcessingOrder", value=value.ProcessingOrder)
        xmlWriter.newline()
        if value.Reserved != 0:
            xmlWriter.simpletag("Reserved", value="0x%04x" % value.Reserved)
            xmlWriter.newline()
        xmlWriter.comment("MorphType=%d" % value.MorphType)
        xmlWriter.newline()
        xmlWriter.simpletag("SubFeatureFlags", value="0x%08x" % value.SubFeatureFlags)
        xmlWriter.newline()
        value.SubStruct.toXML(xmlWriter, font)
        xmlWriter.endtag(name)
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        m = MorxSubtable()
        covFlags = 0
        m.Reserved = 0
        for eltName, eltAttrs, eltContent in filter(istuple, content):
            if eltName == "CoverageFlags":
                # Only in XML from old versions of fonttools.
                covFlags = safeEval(eltAttrs["value"])
                orderKey = ((covFlags & 0x40) != 0, (covFlags & 0x10) != 0)
                m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey]
                self._setTextDirectionFromCoverageFlags(covFlags, m)
            elif eltName == "ProcessingOrder":
                m.ProcessingOrder = eltAttrs["value"]
                assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, (
                    "unknown ProcessingOrder: %s" % m.ProcessingOrder
                )
            elif eltName == "TextDirection":
                m.TextDirection = eltAttrs["value"]
                assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, (
                    "unknown TextDirection %s" % m.TextDirection
                )
            elif eltName == "Reserved":
                m.Reserved = safeEval(eltAttrs["value"])
            elif eltName == "SubFeatureFlags":
                m.SubFeatureFlags = safeEval(eltAttrs["value"])
            elif eltName.endswith("Morph"):
                m.fromXML(eltName, eltAttrs, eltContent, font)
            else:
                assert False, eltName
        m.Reserved = (covFlags & 0xF) << 16 | m.Reserved
        return m

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        covFlags = (value.Reserved & 0x000F0000) >> 16
        reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[
            value.ProcessingOrder
        ]
        covFlags |= 0x80 if value.TextDirection == "Vertical" else 0
        covFlags |= 0x40 if reverseOrder else 0
        covFlags |= 0x20 if value.TextDirection == "Any" else 0
        covFlags |= 0x10 if logicalOrder else 0
        value.CoverageFlags = covFlags
        lengthIndex = len(writer.items)
        before = writer.getDataLength()
        value.StructLength = 0xDEADBEEF
        # The high nibble of value.Reserved is actuallly encoded
        # into coverageFlags, so we need to clear it here.
        origReserved = value.Reserved  # including high nibble
        value.Reserved = value.Reserved & 0xFFFF  # without high nibble
        value.compile(writer, font)
        value.Reserved = origReserved  # restore original value
        assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"
        length = writer.getDataLength() - before
        writer.items[lengthIndex] = struct.pack(">L", length)


# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
# TODO: Untangle the implementation of the various lookup-specific formats.
class STXHeader(BaseConverter):
    def __init__(self, name, repeat, aux, tableClass, *, description=""):
        BaseConverter.__init__(
            self, name, repeat, aux, tableClass, description=description
        )
        assert issubclass(self.tableClass, AATAction)
        self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
        if issubclass(self.tableClass, ContextualMorphAction):
            self.perGlyphLookup = AATLookup("PerGlyphLookup", None, None, GlyphID)
        else:
            self.perGlyphLookup = None

    def read(self, reader, font, tableDict):
        table = AATStateTable()
        pos = reader.pos
        classTableReader = reader.getSubReader(0)
        stateArrayReader = reader.getSubReader(0)
        entryTableReader = reader.getSubReader(0)
        actionReader = None
        ligaturesReader = None
        table.GlyphClassCount = reader.readULong()
        classTableReader.seek(pos + reader.readULong())
        stateArrayReader.seek(pos + reader.readULong())
        entryTableReader.seek(pos + reader.readULong())
        if self.perGlyphLookup is not None:
            perGlyphTableReader = reader.getSubReader(0)
            perGlyphTableReader.seek(pos + reader.readULong())
        if issubclass(self.tableClass, LigatureMorphAction):
            actionReader = reader.getSubReader(0)
            actionReader.seek(pos + reader.readULong())
            ligComponentReader = reader.getSubReader(0)
            ligComponentReader.seek(pos + reader.readULong())
            ligaturesReader = reader.getSubReader(0)
            ligaturesReader.seek(pos + reader.readULong())
            numLigComponents = (ligaturesReader.pos - ligComponentReader.pos) // 2
            assert numLigComponents >= 0
            table.LigComponents = ligComponentReader.readUShortArray(numLigComponents)
            table.Ligatures = self._readLigatures(ligaturesReader, font)
        elif issubclass(self.tableClass, InsertionMorphAction):
            actionReader = reader.getSubReader(0)
            actionReader.seek(pos + reader.readULong())
        table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict)
        numStates = int(
            (entryTableReader.pos - stateArrayReader.pos) / (table.GlyphClassCount * 2)
        )
        for stateIndex in range(numStates):
            state = AATState()
            table.States.append(state)
            for glyphClass in range(table.GlyphClassCount):
                entryIndex = stateArrayReader.readUShort()
                state.Transitions[glyphClass] = self._readTransition(
                    entryTableReader, entryIndex, font, actionReader
                )
        if self.perGlyphLookup is not None:
            table.PerGlyphLookups = self._readPerGlyphLookups(
                table, perGlyphTableReader, font
            )
        return table

    def _readTransition(self, reader, entryIndex, font, actionReader):
        transition = self.tableClass()
        entryReader = reader.getSubReader(
            reader.pos + entryIndex * transition.staticSize
        )
        transition.decompile(entryReader, font, actionReader)
        return transition

    def _readLigatures(self, reader, font):
        limit = len(reader.data)
        numLigatureGlyphs = (limit - reader.pos) // 2
        return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs))

    def _countPerGlyphLookups(self, table):
        # Somewhat annoyingly, the morx table does not encode
        # the size of the per-glyph table. So we need to find
        # the maximum value that MorphActions use as index
        # into this table.
        numLookups = 0
        for state in table.States:
            for t in state.Transitions.values():
                if isinstance(t, ContextualMorphAction):
                    if t.MarkIndex != 0xFFFF:
                        numLookups = max(numLookups, t.MarkIndex + 1)
                    if t.CurrentIndex != 0xFFFF:
                        numLookups = max(numLookups, t.CurrentIndex + 1)
        return numLookups

    def _readPerGlyphLookups(self, table, reader, font):
        pos = reader.pos
        lookups = []
        for _ in range(self._countPerGlyphLookups(table)):
            lookupReader = reader.getSubReader(0)
            lookupReader.seek(pos + reader.readULong())
            lookups.append(self.perGlyphLookup.read(lookupReader, font, {}))
        return lookups

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        glyphClassWriter = OTTableWriter()
        self.classLookup.write(
            glyphClassWriter, font, tableDict, value.GlyphClasses, repeatIndex=None
        )
        glyphClassData = pad(glyphClassWriter.getAllData(), 2)
        glyphClassCount = max(value.GlyphClasses.values()) + 1
        glyphClassTableOffset = 16  # size of STXHeader
        if self.perGlyphLookup is not None:
            glyphClassTableOffset += 4

        glyphClassTableOffset += self.tableClass.actionHeaderSize
        actionData, actionIndex = self.tableClass.compileActions(font, value.States)
        stateArrayData, entryTableData = self._compileStates(
            font, value.States, glyphClassCount, actionIndex
        )
        stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
        entryTableOffset = stateArrayOffset + len(stateArrayData)
        perGlyphOffset = entryTableOffset + len(entryTableData)
        perGlyphData = pad(self._compilePerGlyphLookups(value, font), 4)
        if actionData is not None:
            actionOffset = entryTableOffset + len(entryTableData)
        else:
            actionOffset = None

        ligaturesOffset, ligComponentsOffset = None, None
        ligComponentsData = self._compileLigComponents(value, font)
        ligaturesData = self._compileLigatures(value, font)
        if ligComponentsData is not None:
            assert len(perGlyphData) == 0
            ligComponentsOffset = actionOffset + len(actionData)
            ligaturesOffset = ligComponentsOffset + len(ligComponentsData)

        writer.writeULong(glyphClassCount)
        writer.writeULong(glyphClassTableOffset)
        writer.writeULong(stateArrayOffset)
        writer.writeULong(entryTableOffset)
        if self.perGlyphLookup is not None:
            writer.writeULong(perGlyphOffset)
        if actionOffset is not None:
            writer.writeULong(actionOffset)
        if ligComponentsOffset is not None:
            writer.writeULong(ligComponentsOffset)
            writer.writeULong(ligaturesOffset)
        writer.writeData(glyphClassData)
        writer.writeData(stateArrayData)
        writer.writeData(entryTableData)
        writer.writeData(perGlyphData)
        if actionData is not None:
            writer.writeData(actionData)
        if ligComponentsData is not None:
            writer.writeData(ligComponentsData)
        if ligaturesData is not None:
            writer.writeData(ligaturesData)

    def _compileStates(self, font, states, glyphClassCount, actionIndex):
        stateArrayWriter = OTTableWriter()
        entries, entryIDs = [], {}
        for state in states:
            for glyphClass in range(glyphClassCount):
                transition = state.Transitions[glyphClass]
                entryWriter = OTTableWriter()
                transition.compile(entryWriter, font, actionIndex)
                entryData = entryWriter.getAllData()
                assert (
                    len(entryData) == transition.staticSize
                ), "%s has staticSize %d, " "but actually wrote %d bytes" % (
                    repr(transition),
                    transition.staticSize,
                    len(entryData),
                )
                entryIndex = entryIDs.get(entryData)
                if entryIndex is None:
                    entryIndex = len(entries)
                    entryIDs[entryData] = entryIndex
                    entries.append(entryData)
                stateArrayWriter.writeUShort(entryIndex)
        stateArrayData = pad(stateArrayWriter.getAllData(), 4)
        entryTableData = pad(bytesjoin(entries), 4)
        return stateArrayData, entryTableData

    def _compilePerGlyphLookups(self, table, font):
        if self.perGlyphLookup is None:
            return b""
        numLookups = self._countPerGlyphLookups(table)
        assert len(table.PerGlyphLookups) == numLookups, (
            "len(AATStateTable.PerGlyphLookups) is %d, "
            "but the actions inside the table refer to %d"
            % (len(table.PerGlyphLookups), numLookups)
        )
        writer = OTTableWriter()
        for lookup in table.PerGlyphLookups:
            lookupWriter = writer.getSubWriter()
            self.perGlyphLookup.write(lookupWriter, font, {}, lookup, None)
            writer.writeSubTable(lookupWriter, offsetSize=4)
        return writer.getAllData()

    def _compileLigComponents(self, table, font):
        if not hasattr(table, "LigComponents"):
            return None
        writer = OTTableWriter()
        for component in table.LigComponents:
            writer.writeUShort(component)
        return writer.getAllData()

    def _compileLigatures(self, table, font):
        if not hasattr(table, "Ligatures"):
            return None
        writer = OTTableWriter()
        for glyphName in table.Ligatures:
            writer.writeUShort(font.getGlyphID(glyphName))
        return writer.getAllData()

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.begintag(name, attrs)
        xmlWriter.newline()
        xmlWriter.comment("GlyphClassCount=%s" % value.GlyphClassCount)
        xmlWriter.newline()
        for g, klass in sorted(value.GlyphClasses.items()):
            xmlWriter.simpletag("GlyphClass", glyph=g, value=klass)
            xmlWriter.newline()
        for stateIndex, state in enumerate(value.States):
            xmlWriter.begintag("State", index=stateIndex)
            xmlWriter.newline()
            for glyphClass, trans in sorted(state.Transitions.items()):
                trans.toXML(
                    xmlWriter,
                    font=font,
                    attrs={"onGlyphClass": glyphClass},
                    name="Transition",
                )
            xmlWriter.endtag("State")
            xmlWriter.newline()
        for i, lookup in enumerate(value.PerGlyphLookups):
            xmlWriter.begintag("PerGlyphLookup", index=i)
            xmlWriter.newline()
            for glyph, val in sorted(lookup.items()):
                xmlWriter.simpletag("Lookup", glyph=glyph, value=val)
                xmlWriter.newline()
            xmlWriter.endtag("PerGlyphLookup")
            xmlWriter.newline()
        if hasattr(value, "LigComponents"):
            xmlWriter.begintag("LigComponents")
            xmlWriter.newline()
            for i, val in enumerate(getattr(value, "LigComponents")):
                xmlWriter.simpletag("LigComponent", index=i, value=val)
                xmlWriter.newline()
            xmlWriter.endtag("LigComponents")
            xmlWriter.newline()
        self._xmlWriteLigatures(xmlWriter, font, value, name, attrs)
        xmlWriter.endtag(name)
        xmlWriter.newline()

    def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs):
        if not hasattr(value, "Ligatures"):
            return
        xmlWriter.begintag("Ligatures")
        xmlWriter.newline()
        for i, g in enumerate(getattr(value, "Ligatures")):
            xmlWriter.simpletag("Ligature", index=i, glyph=g)
            xmlWriter.newline()
        xmlWriter.endtag("Ligatures")
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        table = AATStateTable()
        for eltName, eltAttrs, eltContent in filter(istuple, content):
            if eltName == "GlyphClass":
                glyph = eltAttrs["glyph"]
                value = eltAttrs["value"]
                table.GlyphClasses[glyph] = safeEval(value)
            elif eltName == "State":
                state = self._xmlReadState(eltAttrs, eltContent, font)
                table.States.append(state)
            elif eltName == "PerGlyphLookup":
                lookup = self.perGlyphLookup.xmlRead(eltAttrs, eltContent, font)
                table.PerGlyphLookups.append(lookup)
            elif eltName == "LigComponents":
                table.LigComponents = self._xmlReadLigComponents(
                    eltAttrs, eltContent, font
                )
            elif eltName == "Ligatures":
                table.Ligatures = self._xmlReadLigatures(eltAttrs, eltContent, font)
        table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
        return table

    def _xmlReadState(self, attrs, content, font):
        state = AATState()
        for eltName, eltAttrs, eltContent in filter(istuple, content):
            if eltName == "Transition":
                glyphClass = safeEval(eltAttrs["onGlyphClass"])
                transition = self.tableClass()
                transition.fromXML(eltName, eltAttrs, eltContent, font)
                state.Transitions[glyphClass] = transition
        return state

    def _xmlReadLigComponents(self, attrs, content, font):
        ligComponents = []
        for eltName, eltAttrs, _eltContent in filter(istuple, content):
            if eltName == "LigComponent":
                ligComponents.append(safeEval(eltAttrs["value"]))
        return ligComponents

    def _xmlReadLigatures(self, attrs, content, font):
        ligs = []
        for eltName, eltAttrs, _eltContent in filter(istuple, content):
            if eltName == "Ligature":
                ligs.append(eltAttrs["glyph"])
        return ligs


class CIDGlyphMap(BaseConverter):
    def read(self, reader, font, tableDict):
        numCIDs = reader.readUShort()
        result = {}
        for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)):
            if glyphID != 0xFFFF:
                result[cid] = font.getGlyphName(glyphID)
        return result

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()}
        count = max(items) + 1 if items else 0
        writer.writeUShort(count)
        for cid in range(count):
            writer.writeUShort(items.get(cid, 0xFFFF))

    def xmlRead(self, attrs, content, font):
        result = {}
        for eName, eAttrs, _eContent in filter(istuple, content):
            if eName == "CID":
                result[safeEval(eAttrs["cid"])] = eAttrs["glyph"].strip()
        return result

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.begintag(name, attrs)
        xmlWriter.newline()
        for cid, glyph in sorted(value.items()):
            if glyph is not None and glyph != 0xFFFF:
                xmlWriter.simpletag("CID", cid=cid, glyph=glyph)
                xmlWriter.newline()
        xmlWriter.endtag(name)
        xmlWriter.newline()


class GlyphCIDMap(BaseConverter):
    def read(self, reader, font, tableDict):
        glyphOrder = font.getGlyphOrder()
        count = reader.readUShort()
        cids = reader.readUShortArray(count)
        if count > len(glyphOrder):
            log.warning(
                "GlyphCIDMap has %d elements, "
                "but the font has only %d glyphs; "
                "ignoring the rest" % (count, len(glyphOrder))
            )
        result = {}
        for glyphID in range(min(len(cids), len(glyphOrder))):
            cid = cids[glyphID]
            if cid != 0xFFFF:
                result[glyphOrder[glyphID]] = cid
        return result

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        items = {
            font.getGlyphID(g): cid
            for g, cid in value.items()
            if cid is not None and cid != 0xFFFF
        }
        count = max(items) + 1 if items else 0
        writer.writeUShort(count)
        for glyphID in range(count):
            writer.writeUShort(items.get(glyphID, 0xFFFF))

    def xmlRead(self, attrs, content, font):
        result = {}
        for eName, eAttrs, _eContent in filter(istuple, content):
            if eName == "CID":
                result[eAttrs["glyph"]] = safeEval(eAttrs["value"])
        return result

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.begintag(name, attrs)
        xmlWriter.newline()
        for glyph, cid in sorted(value.items()):
            if cid is not None and cid != 0xFFFF:
                xmlWriter.simpletag("CID", glyph=glyph, value=cid)
                xmlWriter.newline()
        xmlWriter.endtag(name)
        xmlWriter.newline()


class DeltaValue(BaseConverter):
    def read(self, reader, font, tableDict):
        StartSize = tableDict["StartSize"]
        EndSize = tableDict["EndSize"]
        DeltaFormat = tableDict["DeltaFormat"]
        assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
        nItems = EndSize - StartSize + 1
        nBits = 1 << DeltaFormat
        minusOffset = 1 << nBits
        mask = (1 << nBits) - 1
        signMask = 1 << (nBits - 1)

        DeltaValue = []
        tmp, shift = 0, 0
        for i in range(nItems):
            if shift == 0:
                tmp, shift = reader.readUShort(), 16
            shift = shift - nBits
            value = (tmp >> shift) & mask
            if value & signMask:
                value = value - minusOffset
            DeltaValue.append(value)
        return DeltaValue

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        StartSize = tableDict["StartSize"]
        EndSize = tableDict["EndSize"]
        DeltaFormat = tableDict["DeltaFormat"]
        DeltaValue = value
        assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
        nItems = EndSize - StartSize + 1
        nBits = 1 << DeltaFormat
        assert len(DeltaValue) == nItems
        mask = (1 << nBits) - 1

        tmp, shift = 0, 16
        for value in DeltaValue:
            shift = shift - nBits
            tmp = tmp | ((value & mask) << shift)
            if shift == 0:
                writer.writeUShort(tmp)
                tmp, shift = 0, 16
        if shift != 16:
            writer.writeUShort(tmp)

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", value)])
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        return safeEval(attrs["value"])


class VarIdxMapValue(BaseConverter):
    def read(self, reader, font, tableDict):
        fmt = tableDict["EntryFormat"]
        nItems = tableDict["MappingCount"]

        innerBits = 1 + (fmt & 0x000F)
        innerMask = (1 << innerBits) - 1
        outerMask = 0xFFFFFFFF - innerMask
        outerShift = 16 - innerBits

        entrySize = 1 + ((fmt & 0x0030) >> 4)
        readArray = {
            1: reader.readUInt8Array,
            2: reader.readUShortArray,
            3: reader.readUInt24Array,
            4: reader.readULongArray,
        }[entrySize]

        return [
            (((raw & outerMask) << outerShift) | (raw & innerMask))
            for raw in readArray(nItems)
        ]

    def write(self, writer, font, tableDict, value, repeatIndex=None):
        fmt = tableDict["EntryFormat"]
        mapping = value
        writer["MappingCount"].setValue(len(mapping))

        innerBits = 1 + (fmt & 0x000F)
        innerMask = (1 << innerBits) - 1
        outerShift = 16 - innerBits

        entrySize = 1 + ((fmt & 0x0030) >> 4)
        writeArray = {
            1: writer.writeUInt8Array,
            2: writer.writeUShortArray,
            3: writer.writeUInt24Array,
            4: writer.writeULongArray,
        }[entrySize]

        writeArray(
            [
                (((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask))
                for idx in mapping
            ]
        )


class VarDataValue(BaseConverter):
    def read(self, reader, font, tableDict):
        values = []

        regionCount = tableDict["VarRegionCount"]
        wordCount = tableDict["NumShorts"]

        # https://github.com/fonttools/fonttools/issues/2279
        longWords = bool(wordCount & 0x8000)
        wordCount = wordCount & 0x7FFF

        if longWords:
            readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray
        else:
            readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array

        n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
        values.extend(readBigArray(n1))
        values.extend(readSmallArray(n2 - n1))
        if n2 > regionCount:  # Padding
            del values[regionCount:]

        return values

    def write(self, writer, font, tableDict, values, repeatIndex=None):
        regionCount = tableDict["VarRegionCount"]
        wordCount = tableDict["NumShorts"]

        # https://github.com/fonttools/fonttools/issues/2279
        longWords = bool(wordCount & 0x8000)
        wordCount = wordCount & 0x7FFF

        (writeBigArray, writeSmallArray) = {
            False: (writer.writeShortArray, writer.writeInt8Array),
            True: (writer.writeLongArray, writer.writeShortArray),
        }[longWords]

        n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
        writeBigArray(values[:n1])
        writeSmallArray(values[n1:regionCount])
        if n2 > regionCount:  # Padding
            writer.writeSmallArray([0] * (n2 - regionCount))

    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", value)])
        xmlWriter.newline()

    def xmlRead(self, attrs, content, font):
        return safeEval(attrs["value"])


class LookupFlag(UShort):
    def xmlWrite(self, xmlWriter, font, value, name, attrs):
        xmlWriter.simpletag(name, attrs + [("value", value)])
        flags = []
        if value & 0x01:
            flags.append("rightToLeft")
        if value & 0x02:
            flags.append("ignoreBaseGlyphs")
        if value & 0x04:
            flags.append("ignoreLigatures")
        if value & 0x08:
            flags.append("ignoreMarks")
        if value & 0x10:
            flags.append("useMarkFilteringSet")
        if value & 0xFF00:
            flags.append("markAttachmentType[%i]" % (value >> 8))
        if flags:
            xmlWriter.comment(" ".join(flags))
        xmlWriter.newline()


class _UInt8Enum(UInt8):
    enumClass = NotImplemented

    def read(self, reader, font, tableDict):
        return self.enumClass(super().read(reader, font, tableDict))

    @classmethod
    def fromString(cls, value):
        return getattr(cls.enumClass, value.upper())

    @classmethod
    def toString(cls, value):
        return cls.enumClass(value).name.lower()


class ExtendMode(_UInt8Enum):
    enumClass = _ExtendMode


class CompositeMode(_UInt8Enum):
    enumClass = _CompositeMode


converterMapping = {
    # type		class
    "int8": Int8,
    "int16": Short,
    "uint8": UInt8,
    "uint16": UShort,
    "uint24": UInt24,
    "uint32": ULong,
    "char64": Char64,
    "Flags32": Flags32,
    "VarIndex": VarIndex,
    "Version": Version,
    "Tag": Tag,
    "GlyphID": GlyphID,
    "GlyphID32": GlyphID32,
    "NameID": NameID,
    "DeciPoints": DeciPoints,
    "Fixed": Fixed,
    "F2Dot14": F2Dot14,
    "Angle": Angle,
    "BiasedAngle": BiasedAngle,
    "struct": Struct,
    "Offset": Table,
    "LOffset": LTable,
    "Offset24": Table24,
    "ValueRecord": ValueRecord,
    "DeltaValue": DeltaValue,
    "VarIdxMapValue": VarIdxMapValue,
    "VarDataValue": VarDataValue,
    "LookupFlag": LookupFlag,
    "ExtendMode": ExtendMode,
    "CompositeMode": CompositeMode,
    "STATFlags": STATFlags,
    # AAT
    "CIDGlyphMap": CIDGlyphMap,
    "GlyphCIDMap": GlyphCIDMap,
    "MortChain": StructWithLength,
    "MortSubtable": StructWithLength,
    "MorxChain": StructWithLength,
    "MorxSubtable": MorxSubtableConverter,
    # "Template" types
    "AATLookup": lambda C: partial(AATLookup, tableClass=C),
    "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C),
    "STXHeader": lambda C: partial(STXHeader, tableClass=C),
    "OffsetTo": lambda C: partial(Table, tableClass=C),
    "LOffsetTo": lambda C: partial(LTable, tableClass=C),
    "LOffset24To": lambda C: partial(Table24, tableClass=C),
}
