from fontTools.misc.textTools import bytesjoin, safeEval, readHex
from fontTools.misc.encodingTools import getEncoding
from fontTools.ttLib import getSearchRange, TTLibError
from fontTools.unicode import Unicode
from . import DefaultTable
import sys
import struct
import array
import logging

log = logging.getLogger(__name__)


def _make_map(font, chars, gids):
    assert len(chars) == len(gids)
    glyphNames = font.getGlyphNameMany(gids)
    cmap = {}
    for char, gid, name in zip(chars, gids, glyphNames):
        if gid == 0:
            continue
        cmap[char] = name
    return cmap


class table__c_m_a_p(DefaultTable.DefaultTable):
    """Character to Glyph Index Mapping Table

    This class represents the `cmap <https://docs.microsoft.com/en-us/typography/opentype/spec/cmap>`_
    table, which maps between input characters (in Unicode or other system encodings)
    and glyphs within the font. The ``cmap`` table contains one or more subtables
    which determine the mapping of of characters to glyphs across different platforms
    and encoding systems.

    ``table__c_m_a_p`` objects expose an accessor ``.tables`` which provides access
    to the subtables, although it is normally easier to retrieve individual subtables
    through the utility methods described below. To add new subtables to a font,
    first determine the subtable format (if in doubt use format 4 for glyphs within
    the BMP, format 12 for glyphs outside the BMP, and format 14 for Unicode Variation
    Sequences) construct subtable objects with ``CmapSubtable.newSubtable(format)``,
    and append them to the ``.tables`` list.

    Within a subtable, the mapping of characters to glyphs is provided by the ``.cmap``
    attribute.

    Example::

            cmap4_0_3 = CmapSubtable.newSubtable(4)
            cmap4_0_3.platformID = 0
            cmap4_0_3.platEncID = 3
            cmap4_0_3.language = 0
            cmap4_0_3.cmap = { 0xC1: "Aacute" }

            cmap = newTable("cmap")
            cmap.tableVersion = 0
            cmap.tables = [cmap4_0_3]

    See also https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
    """

    def getcmap(self, platformID, platEncID):
        """Returns the first subtable which matches the given platform and encoding.

        Args:
                platformID (int): The platform ID. Use 0 for Unicode, 1 for Macintosh
                        (deprecated for new fonts), 2 for ISO (deprecated) and 3 for Windows.
                encodingID (int): Encoding ID. Interpretation depends on the platform ID.
                        See the OpenType specification for details.

        Returns:
                An object which is a subclass of :py:class:`CmapSubtable` if a matching
                subtable is found within the font, or ``None`` otherwise.
        """

        for subtable in self.tables:
            if subtable.platformID == platformID and subtable.platEncID == platEncID:
                return subtable
        return None  # not found

    def getBestCmap(
        self,
        cmapPreferences=(
            (3, 10),
            (0, 6),
            (0, 4),
            (3, 1),
            (0, 3),
            (0, 2),
            (0, 1),
            (0, 0),
        ),
    ):
        """Returns the 'best' Unicode cmap dictionary available in the font
        or ``None``, if no Unicode cmap subtable is available.

        By default it will search for the following (platformID, platEncID)
        pairs in order::

                        (3, 10), # Windows Unicode full repertoire
                        (0, 6),  # Unicode full repertoire (format 13 subtable)
                        (0, 4),  # Unicode 2.0 full repertoire
                        (3, 1),  # Windows Unicode BMP
                        (0, 3),  # Unicode 2.0 BMP
                        (0, 2),  # Unicode ISO/IEC 10646
                        (0, 1),  # Unicode 1.1
                        (0, 0)   # Unicode 1.0

        This particular order matches what HarfBuzz uses to choose what
        subtable to use by default. This order prefers the largest-repertoire
        subtable, and among those, prefers the Windows-platform over the
        Unicode-platform as the former has wider support.

        This order can be customized via the ``cmapPreferences`` argument.
        """
        for platformID, platEncID in cmapPreferences:
            cmapSubtable = self.getcmap(platformID, platEncID)
            if cmapSubtable is not None:
                return cmapSubtable.cmap
        return None  # None of the requested cmap subtables were found

    def buildReversed(self):
        """Builds a reverse mapping dictionary

        Iterates over all Unicode cmap tables and returns a dictionary mapping
        glyphs to sets of codepoints, such as::

                {
                        'one': {0x31}
                        'A': {0x41,0x391}
                }

        The values are sets of Unicode codepoints because
        some fonts map different codepoints to the same glyph.
        For example, ``U+0041 LATIN CAPITAL LETTER A`` and ``U+0391
        GREEK CAPITAL LETTER ALPHA`` are sometimes the same glyph.
        """
        result = {}
        for subtable in self.tables:
            if subtable.isUnicode():
                for codepoint, name in subtable.cmap.items():
                    result.setdefault(name, set()).add(codepoint)
        return result

    def buildReversedMin(self):
        result = {}
        for subtable in self.tables:
            if subtable.isUnicode():
                for codepoint, name in subtable.cmap.items():
                    if name in result:
                        result[name] = min(result[name], codepoint)
                    else:
                        result[name] = codepoint
        return result

    def decompile(self, data, ttFont):
        if len(data) < 4:
            raise TTLibError("cmap table is too short")
        tableVersion, numSubTables = struct.unpack(">HH", data[:4])
        self.tableVersion = int(tableVersion)
        self.tables = tables = []
        seenOffsets = {}
        for i in range(numSubTables):
            entry = data[4 + i * 8 : 4 + (i + 1) * 8]
            if len(entry) < 8:
                raise TTLibError("cmap subtable directory is truncated")
            platformID, platEncID, offset = struct.unpack(">HHL", entry)
            platformID, platEncID = int(platformID), int(platEncID)
            # subtableOffset is an unsigned Offset32 pointing somewhere inside
            # the cmap table; a malformed font can put it past the end of the
            # data, so guard the header reads below instead of letting
            # struct.unpack raise struct.error.
            if offset + 4 > len(data):
                raise TTLibError(
                    "cmap subtable offset %d is out of bounds (data length %d)"
                    % (offset, len(data))
                )
            format, length = struct.unpack(">HH", data[offset : offset + 4])
            if format in [8, 10, 12, 13]:
                if offset + 8 > len(data):
                    raise TTLibError("cmap subtable header is truncated")
                format, reserved, length = struct.unpack(
                    ">HHL", data[offset : offset + 8]
                )
            elif format in [14]:
                if offset + 6 > len(data):
                    raise TTLibError("cmap subtable header is truncated")
                format, length = struct.unpack(">HL", data[offset : offset + 6])

            if not length:
                log.error(
                    "cmap subtable is reported as having zero length: platformID %s, "
                    "platEncID %s, format %s offset %s. Skipping table.",
                    platformID,
                    platEncID,
                    format,
                    offset,
                )
                continue
            table = CmapSubtable.newSubtable(format)
            table.platformID = platformID
            table.platEncID = platEncID
            # Note that by default we decompile only the subtable header info;
            # any other data gets decompiled only when an attribute of the
            # subtable is referenced.
            #
            # Make sure the body is actually present and large enough for the
            # fixed header decompileHeader will unpack. Otherwise the slice is
            # short and we hit either an AssertionError (silenced under python
            # -O, leaving a bogus length) or a struct.error deep inside
            # decompileHeader; surface a TTLibError in both cases. Formats
            # without a fixed header (8, 10 and any unrecognised format, which
            # fall back to cmap_format_unknown) have an empty headerFormat and
            # so no minimum.
            minHeaderSize = struct.calcsize(table.headerFormat)
            if length < minHeaderSize:
                raise TTLibError(
                    "cmap subtable header is truncated: length %d < %d"
                    % (length, minHeaderSize)
                )
            if offset + length > len(data):
                raise TTLibError(
                    "cmap subtable is truncated: needs %d bytes, only %d available"
                    % (length, len(data) - offset)
                )
            table.decompileHeader(data[offset : offset + int(length)], ttFont)
            if offset in seenOffsets:
                table.data = None  # Mark as decompiled
                table.cmap = tables[seenOffsets[offset]].cmap
            else:
                seenOffsets[offset] = i
            tables.append(table)
        if ttFont.lazy is False:  # Be lazy for None and True
            self.ensureDecompiled()

    def ensureDecompiled(self, recurse=False):
        # The recurse argument is unused, but part of the signature of
        # ensureDecompiled across the library.
        for st in self.tables:
            st.ensureDecompiled()

    def compile(self, ttFont):
        self.tables.sort()  # sort according to the spec; see CmapSubtable.__lt__()
        keys = [(t.platformID, t.platEncID, t.language) for t in self.tables]
        seen = set()
        duplicates = {k for k in keys if k in seen or seen.add(k)}
        if duplicates:
            raise ValueError(
                "cmap subtables have duplicate (platformID, platEncID, language) "
                f"entries, which the OpenType spec does not allow: {duplicates}"
            )
        numSubTables = len(self.tables)
        totalOffset = 4 + 8 * numSubTables
        data = struct.pack(">HH", self.tableVersion, numSubTables)
        tableData = b""
        seen = (
            {}
        )  # Some tables are the same object reference. Don't compile them twice.
        done = (
            {}
        )  # Some tables are different objects, but compile to the same data chunk
        for table in self.tables:
            offset = seen.get(id(table.cmap))
            if offset is None:
                chunk = table.compile(ttFont)
                offset = done.get(chunk)
                if offset is None:
                    offset = seen[id(table.cmap)] = done[chunk] = totalOffset + len(
                        tableData
                    )
                    tableData = tableData + chunk
            data = data + struct.pack(">HHL", table.platformID, table.platEncID, offset)
        return data + tableData

    def toXML(self, writer, ttFont):
        writer.simpletag("tableVersion", version=self.tableVersion)
        writer.newline()
        for table in self.tables:
            table.toXML(writer, ttFont)

    def fromXML(self, name, attrs, content, ttFont):
        if name == "tableVersion":
            self.tableVersion = safeEval(attrs["version"])
            return
        if name[:12] != "cmap_format_":
            return
        if not hasattr(self, "tables"):
            self.tables = []
        format = safeEval(name[12:])
        table = CmapSubtable.newSubtable(format)
        table.platformID = safeEval(attrs["platformID"])
        table.platEncID = safeEval(attrs["platEncID"])
        table.fromXML(name, attrs, content, ttFont)
        self.tables.append(table)


class CmapSubtable(object):
    """Base class for all cmap subtable formats.

    Subclasses which handle the individual subtable formats are named
    ``cmap_format_0``, ``cmap_format_2`` etc. Use :py:meth:`getSubtableClass`
    to retrieve the concrete subclass, or :py:meth:`newSubtable` to get a
    new subtable object for a given format.

    The object exposes a ``.cmap`` attribute, which contains a dictionary mapping
    character codepoints to glyph names.
    """

    headerFormat = ">HHH"

    @staticmethod
    def getSubtableClass(format):
        """Return the subtable class for a format."""
        return cmap_classes.get(format, cmap_format_unknown)

    @staticmethod
    def newSubtable(format):
        """Return a new instance of a subtable for the given format
        ."""
        subtableClass = CmapSubtable.getSubtableClass(format)
        return subtableClass(format)

    def __init__(self, format):
        self.format = format
        self.data = None
        self.ttFont = None
        self.platformID = None  #: The platform ID of this subtable
        self.platEncID = None  #: The encoding ID of this subtable (interpretation depends on ``platformID``)
        self.language = (
            None  #: The language ID of this subtable (Macintosh platform only)
        )

    def ensureDecompiled(self, recurse=False):
        # The recurse argument is unused, but part of the signature of
        # ensureDecompiled across the library.
        if self.data is None:
            return
        self.decompile(None, None)  # use saved data.
        self.data = None  # Once this table has been decompiled, make sure we don't
        # just return the original data. Also avoids recursion when
        # called with an attribute that the cmap subtable doesn't have.

    def __getattr__(self, attr):
        # allow lazy decompilation of subtables.
        if attr[:2] == "__":  # don't handle requests for member functions like '__lt__'
            raise AttributeError(attr)
        if self.data is None:
            raise AttributeError(attr)
        self.ensureDecompiled()
        return getattr(self, attr)

    def decompileHeader(self, data, ttFont):
        headerSize = struct.calcsize(self.headerFormat)
        format, length, language = struct.unpack(self.headerFormat, data[:headerSize])
        assert (
            len(data) == length
        ), "corrupt cmap table format %d (data length: %d, header length: %d)" % (
            format,
            len(data),
            length,
        )
        self.format = int(format)
        self.length = int(length)
        self.language = int(language)
        self.data = data[headerSize:]
        self.ttFont = ttFont

    def toXML(self, writer, ttFont):
        writer.begintag(
            self.__class__.__name__,
            [
                ("platformID", self.platformID),
                ("platEncID", self.platEncID),
                ("language", self.language),
            ],
        )
        writer.newline()
        codes = sorted(self.cmap.items())
        self._writeCodes(codes, writer)
        writer.endtag(self.__class__.__name__)
        writer.newline()

    def getEncoding(self, default=None):
        """Returns the Python encoding name for this cmap subtable based on its platformID,
        platEncID, and language.  If encoding for these values is not known, by default
        ``None`` is returned.  That can be overridden by passing a value to the ``default``
        argument.

        Note that if you want to choose a "preferred" cmap subtable, most of the time
        ``self.isUnicode()`` is what you want as that one only returns true for the modern,
        commonly used, Unicode-compatible triplets, not the legacy ones.
        """
        return getEncoding(self.platformID, self.platEncID, self.language, default)

    def isUnicode(self):
        """Returns true if the characters are interpreted as Unicode codepoints."""
        return self.platformID == 0 or (
            self.platformID == 3 and self.platEncID in [0, 1, 10]
        )

    def isSymbol(self):
        """Returns true if the subtable is for the Symbol encoding (3,0)"""
        return self.platformID == 3 and self.platEncID == 0

    def _writeCodes(self, codes, writer):
        isUnicode = self.isUnicode()
        for code, name in codes:
            writer.simpletag("map", code=hex(code), name=name)
            if isUnicode:
                writer.comment(Unicode[code])
            writer.newline()

    def __lt__(self, other):
        if not isinstance(other, CmapSubtable):
            return NotImplemented

        # implemented so that list.sort() sorts according to the spec.
        selfTuple = (
            getattr(self, "platformID", None),
            getattr(self, "platEncID", None),
            getattr(self, "language", None),
        )
        otherTuple = (
            getattr(other, "platformID", None),
            getattr(other, "platEncID", None),
            getattr(other, "language", None),
        )
        return selfTuple < otherTuple


class cmap_format_0(CmapSubtable):
    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"
        data = (
            self.data
        )  # decompileHeader assigns the data after the header to self.data
        assert 262 == self.length, "Format 0 cmap subtable not 262 bytes"
        gids = array.array("B")
        gids.frombytes(self.data)
        charCodes = range(len(gids))
        self.cmap = _make_map(self.ttFont, charCodes, gids)

    def compile(self, ttFont):
        if self.data:
            return struct.pack(">HHH", 0, 262, self.language) + self.data

        cmap = self.cmap
        assert set(cmap.keys()).issubset(range(256))
        getGlyphID = ttFont.getGlyphID
        valueList = [getGlyphID(cmap[i]) if i in cmap else 0 for i in range(256)]

        gids = array.array("B", valueList)
        data = struct.pack(">HHH", 0, 262, self.language) + gids.tobytes()
        assert len(data) == 262
        return data

    def fromXML(self, name, attrs, content, ttFont):
        self.language = safeEval(attrs["language"])
        if not hasattr(self, "cmap"):
            self.cmap = {}
        cmap = self.cmap
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name != "map":
                continue
            cmap[safeEval(attrs["code"])] = attrs["name"]


subHeaderFormat = ">HHhH"


class SubHeader(object):
    def __init__(self):
        self.firstCode = None
        self.entryCount = None
        self.idDelta = None
        self.idRangeOffset = None
        self.glyphIndexArray = []


class cmap_format_2(CmapSubtable):
    def setIDDelta(self, subHeader):
        subHeader.idDelta = 0
        # An empty subheader (an all-notdef two-byte range) has no glyphs to
        # adjust, so leave idDelta at 0 and bail before indexing the array.
        if not subHeader.glyphIndexArray:
            return
        # find the minGI which is not zero.
        minGI = subHeader.glyphIndexArray[0]
        for gid in subHeader.glyphIndexArray:
            if (gid != 0) and (gid < minGI):
                minGI = gid
        # The lowest gid in glyphIndexArray, after subtracting idDelta, must be 1.
        # idDelta is a short, and must be between -32K and 32K. minGI can be between 1 and 64K.
        # We would like to pick an idDelta such that the first glyphArray GID is 1,
        # so that we are more likely to be able to combine glypharray GID subranges.
        # This means that we have a problem when minGI is > 32K
        # Since the final gi is reconstructed from the glyphArray GID by:
        #    (short)finalGID = (gid + idDelta) % 0x10000),
        # we can get from a glypharray GID of 1 to a final GID of 65K by subtracting 2, and casting the
        # negative number to an unsigned short.

        if minGI > 1:
            if minGI > 0x7FFF:
                subHeader.idDelta = -(0x10000 - minGI) - 1
            else:
                subHeader.idDelta = minGI - 1
            idDelta = subHeader.idDelta
            for i in range(subHeader.entryCount):
                gid = subHeader.glyphIndexArray[i]
                if gid > 0:
                    subHeader.glyphIndexArray[i] = gid - idDelta

    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"

        data = (
            self.data
        )  # decompileHeader assigns the data after the header to self.data
        subHeaderKeys = []
        maxSubHeaderindex = 0
        # get the key array, and determine the number of subHeaders.
        allKeys = array.array("H")
        allKeys.frombytes(data[:512])
        data = data[512:]
        if sys.byteorder != "big":
            allKeys.byteswap()
        subHeaderKeys = [key // 8 for key in allKeys]
        maxSubHeaderindex = max(subHeaderKeys)

        # Load subHeaders
        subHeaderList = []
        pos = 0
        for i in range(maxSubHeaderindex + 1):
            subHeader = SubHeader()
            (
                subHeader.firstCode,
                subHeader.entryCount,
                subHeader.idDelta,
                subHeader.idRangeOffset,
            ) = struct.unpack(subHeaderFormat, data[pos : pos + 8])
            pos += 8
            giDataPos = pos + subHeader.idRangeOffset - 2
            giList = array.array("H")
            giList.frombytes(data[giDataPos : giDataPos + subHeader.entryCount * 2])
            if sys.byteorder != "big":
                giList.byteswap()
            subHeader.glyphIndexArray = giList
            subHeaderList.append(subHeader)
        # How this gets processed.
        # Charcodes may be one or two bytes.
        # The first byte of a charcode is mapped through the subHeaderKeys, to select
        # a subHeader. For any subheader but 0, the next byte is then mapped through the
        # selected subheader. If subheader Index 0 is selected, then the byte itself is
        # mapped through the subheader, and there is no second byte.
        # Then assume that the subsequent byte is the first byte of the next charcode,and repeat.
        #
        # Each subheader references a range in the glyphIndexArray whose length is entryCount.
        # The range in glyphIndexArray referenced by a sunheader may overlap with the range in glyphIndexArray
        # referenced by another subheader.
        # The only subheader that will be referenced by more than one first-byte value is the subheader
        # that maps the entire range of glyphID values to glyphIndex 0, e.g notdef:
        # 	 {firstChar 0, EntryCount 0,idDelta 0,idRangeOffset xx}
        # A byte being mapped though a subheader is treated as in index into a mapping of array index to font glyphIndex.
        # A subheader specifies a subrange within (0...256) by the
        # firstChar and EntryCount values. If the byte value is outside the subrange, then the glyphIndex is zero
        # (e.g. glyph not in font).
        # If the byte index is in the subrange, then an offset index is calculated as (byteIndex - firstChar).
        # The index to glyphIndex mapping is a subrange of the glyphIndexArray. You find the start of the subrange by
        # counting idRangeOffset bytes from the idRangeOffset word. The first value in this subrange is the
        # glyphIndex for the index firstChar. The offset index should then be used in this array to get the glyphIndex.
        # Example for Logocut-Medium
        # first byte of charcode = 129; selects subheader 1.
        # subheader 1 = {firstChar 64, EntryCount 108,idDelta 42,idRangeOffset 0252}
        # second byte of charCode = 66
        # the index offset = 66-64 = 2.
        # The subrange of the glyphIndexArray starting at 0x0252 bytes from the idRangeOffset word is:
        # [glyphIndexArray index], [subrange array index] = glyphIndex
        # [256], [0]=1 	from charcode [129, 64]
        # [257], [1]=2  	from charcode [129, 65]
        # [258], [2]=3  	from charcode [129, 66]
        # [259], [3]=4  	from charcode [129, 67]
        # So, the glyphIndex = 3 from the array. Then if idDelta is not zero and the glyph ID is not zero,
        # add it to the glyphID to get the final glyphIndex
        # value. In this case the final glyph index = 3+ 42 -> 45 for the final glyphIndex. Whew!

        self.data = b""
        cmap = {}
        notdefGI = 0
        for firstByte in range(256):
            subHeadindex = subHeaderKeys[firstByte]
            subHeader = subHeaderList[subHeadindex]
            if subHeadindex == 0:
                if (firstByte < subHeader.firstCode) or (
                    firstByte >= subHeader.firstCode + subHeader.entryCount
                ):
                    continue  # gi is notdef.
                else:
                    charCode = firstByte
                    offsetIndex = firstByte - subHeader.firstCode
                    gi = subHeader.glyphIndexArray[offsetIndex]
                    if gi != 0:
                        gi = (gi + subHeader.idDelta) % 0x10000
                    else:
                        continue  # gi is notdef.
                cmap[charCode] = gi
            else:
                if subHeader.entryCount:
                    charCodeOffset = firstByte * 256 + subHeader.firstCode
                    for offsetIndex in range(subHeader.entryCount):
                        charCode = charCodeOffset + offsetIndex
                        gi = subHeader.glyphIndexArray[offsetIndex]
                        if gi != 0:
                            gi = (gi + subHeader.idDelta) % 0x10000
                        else:
                            continue
                        cmap[charCode] = gi
                # If not subHeader.entryCount, then all char codes with this first byte are
                # mapped to .notdef. We can skip this subtable, and leave the glyphs un-encoded, which is the
                # same as mapping it to .notdef.

        gids = list(cmap.values())
        charCodes = list(cmap.keys())
        self.cmap = _make_map(self.ttFont, charCodes, gids)

    def compile(self, ttFont):
        if self.data:
            return (
                struct.pack(">HHH", self.format, self.length, self.language) + self.data
            )
        kEmptyTwoCharCodeRange = -1
        notdefGI = 0

        items = sorted(self.cmap.items())
        charCodes = [item[0] for item in items]
        names = [item[1] for item in items]
        nameMap = ttFont.getReverseGlyphMap()
        try:
            gids = [nameMap[name] for name in names]
        except KeyError:
            nameMap = ttFont.getReverseGlyphMap(rebuild=True)
            try:
                gids = [nameMap[name] for name in names]
            except KeyError:
                # allow virtual GIDs in format 2 tables
                gids = []
                for name in names:
                    try:
                        gid = nameMap[name]
                    except KeyError:
                        try:
                            if name[:3] == "gid":
                                gid = int(name[3:])
                            else:
                                gid = ttFont.getGlyphID(name)
                        except:
                            raise KeyError(name)

                    gids.append(gid)

        # Process the (char code to gid) item list in char code order.
        # By definition, all one byte char codes map to subheader 0.
        # For all the two byte char codes, we assume that the first byte maps maps to the empty subhead (with an entry count of 0,
        # which defines all char codes in its range to map to notdef) unless proven otherwise.
        # Note that since the char code items are processed in char code order, all the char codes with the
        # same first byte are in sequential order.

        subHeaderKeys = [
            kEmptyTwoCharCodeRange for x in range(256)
        ]  # list of indices into subHeaderList.
        subHeaderList = []

        # We force this subheader entry 0 to exist in the subHeaderList in the case where some one comes up
        # with a cmap where all the one byte char codes map to notdef,
        # with the result that the subhead 0 would not get created just by processing the item list.
        # The same is true for an entirely empty (all-notdef) cmap, where there
        # are no char codes to process at all.
        if not charCodes or charCodes[0] > 255:
            subHeader = SubHeader()
            subHeader.firstCode = 0
            subHeader.entryCount = 0
            subHeader.idDelta = 0
            subHeader.idRangeOffset = 0
            subHeaderList.append(subHeader)

        lastFirstByte = -1
        items = zip(charCodes, gids)
        for charCode, gid in items:
            if gid == 0:
                continue
            firstbyte = charCode >> 8
            secondByte = charCode & 0x00FF

            if (
                firstbyte != lastFirstByte
            ):  # Need to update the current subhead, and start a new one.
                if lastFirstByte > -1:
                    # fix GI's and iDelta of current subheader.
                    self.setIDDelta(subHeader)

                    # If it was sunheader 0 for one-byte charCodes, then we need to set the subHeaderKeys value to zero
                    # for the indices matching the char codes.
                    if lastFirstByte == 0:
                        for index in range(subHeader.entryCount):
                            charCode = subHeader.firstCode + index
                            subHeaderKeys[charCode] = 0

                    assert subHeader.entryCount == len(
                        subHeader.glyphIndexArray
                    ), "Error - subhead entry count does not match len of glyphID subrange."
                # init new subheader
                subHeader = SubHeader()
                subHeader.firstCode = secondByte
                subHeader.entryCount = 1
                subHeader.glyphIndexArray.append(gid)
                subHeaderList.append(subHeader)
                subHeaderKeys[firstbyte] = len(subHeaderList) - 1
                lastFirstByte = firstbyte
            else:
                # need to fill in with notdefs all the code points between the last charCode and the current charCode.
                codeDiff = secondByte - (subHeader.firstCode + subHeader.entryCount)
                for i in range(codeDiff):
                    subHeader.glyphIndexArray.append(notdefGI)
                subHeader.glyphIndexArray.append(gid)
                subHeader.entryCount = subHeader.entryCount + codeDiff + 1

        # fix GI's and iDelta of last subheader that we we added to the subheader array.
        self.setIDDelta(subHeader)

        # Now we add a final subheader for the subHeaderKeys which maps to empty two byte charcode ranges.
        subHeader = SubHeader()
        subHeader.firstCode = 0
        subHeader.entryCount = 0
        subHeader.idDelta = 0
        subHeader.idRangeOffset = 2
        subHeaderList.append(subHeader)
        emptySubheadIndex = len(subHeaderList) - 1
        for index in range(256):
            if subHeaderKeys[index] == kEmptyTwoCharCodeRange:
                subHeaderKeys[index] = emptySubheadIndex
        # Since this is the last subheader, the GlyphIndex Array starts two bytes after the start of the
        # idRangeOffset word of this subHeader. We can safely point to the first entry in the GlyphIndexArray,
        # since the first subrange of the GlyphIndexArray is for subHeader 0, which always starts with
        # charcode 0 and GID 0.

        idRangeOffset = (
            len(subHeaderList) - 1
        ) * 8 + 2  # offset to beginning of glyphIDArray from first subheader idRangeOffset.
        subheadRangeLen = (
            len(subHeaderList) - 1
        )  # skip last special empty-set subheader; we've already hardocodes its idRangeOffset to 2.
        for index in range(subheadRangeLen):
            subHeader = subHeaderList[index]
            subHeader.idRangeOffset = 0
            for j in range(index):
                prevSubhead = subHeaderList[j]
                if (
                    prevSubhead.glyphIndexArray == subHeader.glyphIndexArray
                ):  # use the glyphIndexArray subarray
                    subHeader.idRangeOffset = (
                        prevSubhead.idRangeOffset - (index - j) * 8
                    )
                    subHeader.glyphIndexArray = []
                    break
            if subHeader.idRangeOffset == 0:  # didn't find one.
                subHeader.idRangeOffset = idRangeOffset
                idRangeOffset = (
                    idRangeOffset - 8
                ) + subHeader.entryCount * 2  # one less subheader, one more subArray.
            else:
                idRangeOffset = idRangeOffset - 8  # one less subheader

        # Now we can write out the data!
        length = (
            6 + 512 + 8 * len(subHeaderList)
        )  # header, 256 subHeaderKeys, and subheader array.
        for subhead in subHeaderList[:-1]:
            length = (
                length + len(subhead.glyphIndexArray) * 2
            )  # We can't use subhead.entryCount, as some of the subhead may share subArrays.
        dataList = [struct.pack(">HHH", 2, length, self.language)]
        for index in subHeaderKeys:
            dataList.append(struct.pack(">H", index * 8))
        for subhead in subHeaderList:
            dataList.append(
                struct.pack(
                    subHeaderFormat,
                    subhead.firstCode,
                    subhead.entryCount,
                    subhead.idDelta,
                    subhead.idRangeOffset,
                )
            )
        for subhead in subHeaderList[:-1]:
            for gi in subhead.glyphIndexArray:
                dataList.append(struct.pack(">H", gi))
        data = bytesjoin(dataList)
        assert len(data) == length, (
            "Error: cmap format 2 is not same length as calculated! actual: "
            + str(len(data))
            + " calc : "
            + str(length)
        )
        return data

    def fromXML(self, name, attrs, content, ttFont):
        self.language = safeEval(attrs["language"])
        if not hasattr(self, "cmap"):
            self.cmap = {}
        cmap = self.cmap

        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name != "map":
                continue
            cmap[safeEval(attrs["code"])] = attrs["name"]


cmap_format_4_format = ">7H"

# uint16  endCode[segCount]          # Ending character code for each segment, last = 0xFFFF.
# uint16  reservedPad                # This value should be zero
# uint16  startCode[segCount]        # Starting character code for each segment
# uint16  idDelta[segCount]          # Delta for all character codes in segment
# uint16  idRangeOffset[segCount]    # Offset in bytes to glyph indexArray, or 0
# uint16  glyphIndexArray[variable]  # Glyph index array


def splitRange(startCode, endCode, cmap):
    # Try to split a range of character codes into subranges with consecutive
    # glyph IDs in such a way that the cmap4 subtable can be stored "most"
    # efficiently. I can't prove I've got the optimal solution, but it seems
    # to do well with the fonts I tested: none became bigger, many became smaller.
    if startCode == endCode:
        return [], [endCode]

    lastID = cmap[startCode]
    lastCode = startCode
    inOrder = None
    orderedBegin = None
    subRanges = []

    # Gather subranges in which the glyph IDs are consecutive.
    for code in range(startCode + 1, endCode + 1):
        glyphID = cmap[code]

        if glyphID - 1 == lastID:
            if inOrder is None or not inOrder:
                inOrder = 1
                orderedBegin = lastCode
        else:
            if inOrder:
                inOrder = 0
                subRanges.append((orderedBegin, lastCode))
                orderedBegin = None

        lastID = glyphID
        lastCode = code

    if inOrder:
        subRanges.append((orderedBegin, lastCode))
    assert lastCode == endCode

    # Now filter out those new subranges that would only make the data bigger.
    # A new segment cost 8 bytes, not using a new segment costs 2 bytes per
    # character.
    newRanges = []
    for b, e in subRanges:
        if b == startCode and e == endCode:
            break  # the whole range, we're fine
        if b == startCode or e == endCode:
            threshold = 4  # split costs one more segment
        else:
            threshold = 8  # split costs two more segments
        if (e - b + 1) > threshold:
            newRanges.append((b, e))
    subRanges = newRanges

    if not subRanges:
        return [], [endCode]

    if subRanges[0][0] != startCode:
        subRanges.insert(0, (startCode, subRanges[0][0] - 1))
    if subRanges[-1][1] != endCode:
        subRanges.append((subRanges[-1][1] + 1, endCode))

    # Fill the "holes" in the segments list -- those are the segments in which
    # the glyph IDs are _not_ consecutive.
    i = 1
    while i < len(subRanges):
        if subRanges[i - 1][1] + 1 != subRanges[i][0]:
            subRanges.insert(i, (subRanges[i - 1][1] + 1, subRanges[i][0] - 1))
            i = i + 1
        i = i + 1

    # Transform the ranges into startCode/endCode lists.
    start = []
    end = []
    for b, e in subRanges:
        start.append(b)
        end.append(e)
    start.pop(0)

    assert len(start) + 1 == len(end)
    return start, end


class cmap_format_4(CmapSubtable):
    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"

        data = (
            self.data
        )  # decompileHeader assigns the data after the header to self.data
        segCountX2, searchRange, entrySelector, rangeShift = struct.unpack(
            ">4H", data[:8]
        )
        data = data[8:]
        segCount = segCountX2 // 2

        allCodes = array.array("H")
        allCodes.frombytes(data)
        self.data = data = None

        if sys.byteorder != "big":
            allCodes.byteswap()

        # divide the data
        endCode = allCodes[:segCount]
        allCodes = allCodes[segCount + 1 :]  # the +1 is skipping the reservedPad field
        startCode = allCodes[:segCount]
        allCodes = allCodes[segCount:]
        idDelta = allCodes[:segCount]
        allCodes = allCodes[segCount:]
        idRangeOffset = allCodes[:segCount]
        glyphIndexArray = allCodes[segCount:]
        lenGIArray = len(glyphIndexArray)

        # build 2-byte character mapping
        charCodes = []
        gids = []
        for i in range(len(startCode) - 1):  # don't do 0xffff!
            start = startCode[i]
            delta = idDelta[i]
            rangeOffset = idRangeOffset[i]
            partial = rangeOffset // 2 - start + i - len(idRangeOffset)

            rangeCharCodes = list(range(startCode[i], endCode[i] + 1))
            charCodes.extend(rangeCharCodes)
            if rangeOffset == 0:
                gids.extend(
                    [(charCode + delta) & 0xFFFF for charCode in rangeCharCodes]
                )
            else:
                for charCode in rangeCharCodes:
                    index = charCode + partial
                    assert index < lenGIArray, (
                        "In format 4 cmap, range (%d), the calculated index (%d) into the glyph index array is not less than the length of the array (%d) !"
                        % (i, index, lenGIArray)
                    )
                    if glyphIndexArray[index] != 0:  # if not missing glyph
                        glyphID = glyphIndexArray[index] + delta
                    else:
                        glyphID = 0  # missing glyph
                    gids.append(glyphID & 0xFFFF)

        self.cmap = _make_map(self.ttFont, charCodes, gids)

    def compile(self, ttFont):
        if self.data:
            return (
                struct.pack(">HHH", self.format, self.length, self.language) + self.data
            )

        charCodes = list(self.cmap.keys())
        if not charCodes:
            startCode = [0xFFFF]
            endCode = [0xFFFF]
        else:
            charCodes.sort()
            names = [self.cmap[code] for code in charCodes]
            nameMap = ttFont.getReverseGlyphMap()
            try:
                gids = [nameMap[name] for name in names]
            except KeyError:
                nameMap = ttFont.getReverseGlyphMap(rebuild=True)
                try:
                    gids = [nameMap[name] for name in names]
                except KeyError:
                    # allow virtual GIDs in format 4 tables
                    gids = []
                    for name in names:
                        try:
                            gid = nameMap[name]
                        except KeyError:
                            try:
                                if name[:3] == "gid":
                                    gid = int(name[3:])
                                else:
                                    gid = ttFont.getGlyphID(name)
                            except:
                                raise KeyError(name)

                        gids.append(gid)
            cmap = {}  # code:glyphID mapping
            for code, gid in zip(charCodes, gids):
                cmap[code] = gid

            # Build startCode and endCode lists.
            # Split the char codes in ranges of consecutive char codes, then split
            # each range in more ranges of consecutive/not consecutive glyph IDs.
            # See splitRange().
            lastCode = charCodes[0]
            endCode = []
            startCode = [lastCode]
            for charCode in charCodes[
                1:
            ]:  # skip the first code, it's the first start code
                if charCode == lastCode + 1:
                    lastCode = charCode
                    continue
                start, end = splitRange(startCode[-1], lastCode, cmap)
                startCode.extend(start)
                endCode.extend(end)
                startCode.append(charCode)
                lastCode = charCode
            start, end = splitRange(startCode[-1], lastCode, cmap)
            startCode.extend(start)
            endCode.extend(end)
            startCode.append(0xFFFF)
            endCode.append(0xFFFF)

        # build up rest of cruft
        idDelta = []
        idRangeOffset = []
        glyphIndexArray = []
        for i in range(len(endCode) - 1):  # skip the closing codes (0xffff)
            indices = []
            for charCode in range(startCode[i], endCode[i] + 1):
                indices.append(cmap[charCode])
            if indices == list(range(indices[0], indices[0] + len(indices))):
                idDelta.append((indices[0] - startCode[i]) % 0x10000)
                idRangeOffset.append(0)
            else:
                idDelta.append(0)
                idRangeOffset.append(2 * (len(endCode) + len(glyphIndexArray) - i))
                glyphIndexArray.extend(indices)
        idDelta.append(1)  # 0xffff + 1 == (tadaa!) 0. So this end code maps to .notdef
        idRangeOffset.append(0)

        # Insane.
        segCount = len(endCode)
        segCountX2 = segCount * 2
        searchRange, entrySelector, rangeShift = getSearchRange(segCount, 2)

        charCodeArray = array.array("H", endCode + [0] + startCode)
        idDeltaArray = array.array("H", idDelta)
        restArray = array.array("H", idRangeOffset + glyphIndexArray)
        if sys.byteorder != "big":
            charCodeArray.byteswap()
        if sys.byteorder != "big":
            idDeltaArray.byteswap()
        if sys.byteorder != "big":
            restArray.byteswap()
        data = charCodeArray.tobytes() + idDeltaArray.tobytes() + restArray.tobytes()

        length = struct.calcsize(cmap_format_4_format) + len(data)
        header = struct.pack(
            cmap_format_4_format,
            self.format,
            length,
            self.language,
            segCountX2,
            searchRange,
            entrySelector,
            rangeShift,
        )
        return header + data

    def fromXML(self, name, attrs, content, ttFont):
        self.language = safeEval(attrs["language"])
        if not hasattr(self, "cmap"):
            self.cmap = {}
        cmap = self.cmap

        for element in content:
            if not isinstance(element, tuple):
                continue
            nameMap, attrsMap, dummyContent = element
            if nameMap != "map":
                assert 0, "Unrecognized keyword in cmap subtable"
            cmap[safeEval(attrsMap["code"])] = attrsMap["name"]


class cmap_format_6(CmapSubtable):
    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"

        data = (
            self.data
        )  # decompileHeader assigns the data after the header to self.data
        firstCode, entryCount = struct.unpack(">HH", data[:4])
        firstCode = int(firstCode)
        data = data[4:]
        # assert len(data) == 2 * entryCount  # XXX not true in Apple's Helvetica!!!
        gids = array.array("H")
        gids.frombytes(data[: 2 * int(entryCount)])
        if sys.byteorder != "big":
            gids.byteswap()
        self.data = data = None

        charCodes = list(range(firstCode, firstCode + len(gids)))
        self.cmap = _make_map(self.ttFont, charCodes, gids)

    def compile(self, ttFont):
        if self.data:
            return (
                struct.pack(">HHH", self.format, self.length, self.language) + self.data
            )
        cmap = self.cmap
        codes = sorted(cmap.keys())
        if codes:  # yes, there are empty cmap tables.
            codes = list(range(codes[0], codes[-1] + 1))
            firstCode = codes[0]
            valueList = [
                ttFont.getGlyphID(cmap[code]) if code in cmap else 0 for code in codes
            ]
            gids = array.array("H", valueList)
            if sys.byteorder != "big":
                gids.byteswap()
            data = gids.tobytes()
        else:
            data = b""
            firstCode = 0
        header = struct.pack(
            ">HHHHH", 6, len(data) + 10, self.language, firstCode, len(codes)
        )
        return header + data

    def fromXML(self, name, attrs, content, ttFont):
        self.language = safeEval(attrs["language"])
        if not hasattr(self, "cmap"):
            self.cmap = {}
        cmap = self.cmap

        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name != "map":
                continue
            cmap[safeEval(attrs["code"])] = attrs["name"]


class cmap_format_12_or_13(CmapSubtable):
    headerFormat = ">HHLLL"

    def __init__(self, format):
        self.format = format
        self.reserved = 0
        self.data = None
        self.ttFont = None

    def decompileHeader(self, data, ttFont):
        headerSize = struct.calcsize(self.headerFormat)
        format, reserved, length, language, nGroups = struct.unpack(
            self.headerFormat, data[:headerSize]
        )
        if len(data) != length:
            raise TTLibError(
                "cmap subtable format %d is truncated: length %d, got %d bytes"
                % (self.format, length, len(data))
            )
        expectedLength = headerSize + nGroups * 12
        if length != expectedLength:
            raise TTLibError(
                "cmap subtable format %d has an inconsistent group count: "
                "length %d, but %d groups need %d bytes"
                % (self.format, length, nGroups, expectedLength)
            )
        self.format = format
        self.reserved = reserved
        self.length = length
        self.language = language
        self.nGroups = nGroups
        self.data = data[headerSize:]
        self.ttFont = ttFont

    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"

        data = (
            self.data
        )  # decompileHeader assigns the data after the header to self.data
        charCodes = []
        gids = []
        pos = 0
        groups = array.array("I", data[: self.nGroups * 12])
        if sys.byteorder != "big":
            groups.byteswap()
        for i in range(self.nGroups):
            startCharCode = groups[i * 3]
            endCharCode = groups[i * 3 + 1]
            glyphID = groups[i * 3 + 2]
            lenGroup = 1 + endCharCode - startCharCode
            charCodes.extend(range(startCharCode, endCharCode + 1))
            gids.extend(self._computeGIDs(glyphID, lenGroup))
        self.data = data = None
        self.cmap = _make_map(self.ttFont, charCodes, gids)

    def compile(self, ttFont):
        if self.data:
            return (
                struct.pack(
                    ">HHLLL",
                    self.format,
                    self.reserved,
                    self.length,
                    self.language,
                    self.nGroups,
                )
                + self.data
            )
        charCodes = list(self.cmap.keys())
        names = list(self.cmap.values())
        nameMap = ttFont.getReverseGlyphMap()
        try:
            gids = [nameMap[name] for name in names]
        except KeyError:
            nameMap = ttFont.getReverseGlyphMap(rebuild=True)
            try:
                gids = [nameMap[name] for name in names]
            except KeyError:
                # allow virtual GIDs in format 12 tables
                gids = []
                for name in names:
                    try:
                        gid = nameMap[name]
                    except KeyError:
                        try:
                            if name[:3] == "gid":
                                gid = int(name[3:])
                            else:
                                gid = ttFont.getGlyphID(name)
                        except:
                            raise KeyError(name)

                    gids.append(gid)

        cmap = {}  # code:glyphID mapping
        for code, gid in zip(charCodes, gids):
            cmap[code] = gid

        charCodes.sort()
        index = 0
        startCharCode = charCodes[0]
        startGlyphID = cmap[startCharCode]
        lastGlyphID = startGlyphID - self._format_step
        lastCharCode = startCharCode - 1
        nGroups = 0
        dataList = []
        maxIndex = len(charCodes)
        for index in range(maxIndex):
            charCode = charCodes[index]
            glyphID = cmap[charCode]
            if not self._IsInSameRun(glyphID, lastGlyphID, charCode, lastCharCode):
                dataList.append(
                    struct.pack(">LLL", startCharCode, lastCharCode, startGlyphID)
                )
                startCharCode = charCode
                startGlyphID = glyphID
                nGroups = nGroups + 1
            lastGlyphID = glyphID
            lastCharCode = charCode
        dataList.append(struct.pack(">LLL", startCharCode, lastCharCode, startGlyphID))
        nGroups = nGroups + 1
        data = bytesjoin(dataList)
        lengthSubtable = len(data) + 16
        assert len(data) == (nGroups * 12) == (lengthSubtable - 16)
        return (
            struct.pack(
                ">HHLLL",
                self.format,
                self.reserved,
                lengthSubtable,
                self.language,
                nGroups,
            )
            + data
        )

    def toXML(self, writer, ttFont):
        writer.begintag(
            self.__class__.__name__,
            [
                ("platformID", self.platformID),
                ("platEncID", self.platEncID),
                ("format", self.format),
                ("reserved", self.reserved),
                ("length", self.length),
                ("language", self.language),
                ("nGroups", self.nGroups),
            ],
        )
        writer.newline()
        codes = sorted(self.cmap.items())
        self._writeCodes(codes, writer)
        writer.endtag(self.__class__.__name__)
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        self.format = safeEval(attrs["format"])
        self.reserved = safeEval(attrs["reserved"])
        self.length = safeEval(attrs["length"])
        self.language = safeEval(attrs["language"])
        self.nGroups = safeEval(attrs["nGroups"])
        if not hasattr(self, "cmap"):
            self.cmap = {}
        cmap = self.cmap

        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name != "map":
                continue
            cmap[safeEval(attrs["code"])] = attrs["name"]


class cmap_format_12(cmap_format_12_or_13):
    _format_step = 1

    def __init__(self, format=12):
        cmap_format_12_or_13.__init__(self, format)

    def _computeGIDs(self, startingGlyph, numberOfGlyphs):
        return range(startingGlyph, startingGlyph + numberOfGlyphs)

    def _IsInSameRun(self, glyphID, lastGlyphID, charCode, lastCharCode):
        return (glyphID == 1 + lastGlyphID) and (charCode == 1 + lastCharCode)


class cmap_format_13(cmap_format_12_or_13):
    _format_step = 0

    def __init__(self, format=13):
        cmap_format_12_or_13.__init__(self, format)

    def _computeGIDs(self, startingGlyph, numberOfGlyphs):
        return [startingGlyph] * numberOfGlyphs

    def _IsInSameRun(self, glyphID, lastGlyphID, charCode, lastCharCode):
        return (glyphID == lastGlyphID) and (charCode == 1 + lastCharCode)


def cvtToUVS(threeByteString):
    data = b"\0" + threeByteString
    (val,) = struct.unpack(">L", data)
    return val


def cvtFromUVS(val):
    assert 0 <= val < 0x1000000
    fourByteString = struct.pack(">L", val)
    return fourByteString[1:]


class cmap_format_14(CmapSubtable):
    headerFormat = ">HLL"

    def decompileHeader(self, data, ttFont):
        headerSize = struct.calcsize(self.headerFormat)
        format, length, numVarSelectorRecords = struct.unpack(
            self.headerFormat, data[:headerSize]
        )
        self.data = data[headerSize:]
        self.length = length
        self.numVarSelectorRecords = numVarSelectorRecords
        self.ttFont = ttFont
        self.language = 0xFF  # has no language.

    def decompile(self, data, ttFont):
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"
        data = self.data

        self.cmap = (
            {}
        )  # so that clients that expect this to exist in a cmap table won't fail.
        uvsDict = {}
        recOffset = 0
        for n in range(self.numVarSelectorRecords):
            uvs, defOVSOffset, nonDefUVSOffset = struct.unpack(
                ">3sLL", data[recOffset : recOffset + 11]
            )
            recOffset += 11
            varUVS = cvtToUVS(uvs)
            if defOVSOffset:
                startOffset = defOVSOffset - 10
                (numValues,) = struct.unpack(">L", data[startOffset : startOffset + 4])
                startOffset += 4
                for r in range(numValues):
                    uv, addtlCnt = struct.unpack(
                        ">3sB", data[startOffset : startOffset + 4]
                    )
                    startOffset += 4
                    firstBaseUV = cvtToUVS(uv)
                    cnt = addtlCnt + 1
                    baseUVList = list(range(firstBaseUV, firstBaseUV + cnt))
                    glyphList = [None] * cnt
                    localUVList = zip(baseUVList, glyphList)
                    try:
                        uvsDict[varUVS].extend(localUVList)
                    except KeyError:
                        uvsDict[varUVS] = list(localUVList)

            if nonDefUVSOffset:
                startOffset = nonDefUVSOffset - 10
                (numRecs,) = struct.unpack(">L", data[startOffset : startOffset + 4])
                startOffset += 4
                localUVList = []
                for r in range(numRecs):
                    uv, gid = struct.unpack(">3sH", data[startOffset : startOffset + 5])
                    startOffset += 5
                    uv = cvtToUVS(uv)
                    glyphName = self.ttFont.getGlyphName(gid)
                    localUVList.append((uv, glyphName))
                try:
                    uvsDict[varUVS].extend(localUVList)
                except KeyError:
                    uvsDict[varUVS] = localUVList

        self.uvsDict = uvsDict

    def toXML(self, writer, ttFont):
        writer.begintag(
            self.__class__.__name__,
            [
                ("platformID", self.platformID),
                ("platEncID", self.platEncID),
            ],
        )
        writer.newline()
        uvsDict = self.uvsDict
        uvsList = sorted(uvsDict.keys())
        for uvs in uvsList:
            uvList = uvsDict[uvs]
            uvList.sort(key=lambda item: (item[1] is not None, item[0], item[1]))
            for uv, gname in uvList:
                attrs = [("uv", hex(uv)), ("uvs", hex(uvs))]
                if gname is not None:
                    attrs.append(("name", gname))
                writer.simpletag("map", attrs)
                writer.newline()
        writer.endtag(self.__class__.__name__)
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        self.language = 0xFF  # provide a value so that CmapSubtable.__lt__() won't fail
        if not hasattr(self, "cmap"):
            self.cmap = (
                {}
            )  # so that clients that expect this to exist in a cmap table won't fail.
        if not hasattr(self, "uvsDict"):
            self.uvsDict = {}
            uvsDict = self.uvsDict

        # For backwards compatibility reasons we accept "None" as an indicator
        # for "default mapping", unless the font actually has a glyph named
        # "None".
        _hasGlyphNamedNone = None

        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name != "map":
                continue
            uvs = safeEval(attrs["uvs"])
            uv = safeEval(attrs["uv"])
            gname = attrs.get("name")
            if gname == "None":
                if _hasGlyphNamedNone is None:
                    _hasGlyphNamedNone = "None" in ttFont.getGlyphOrder()
                if not _hasGlyphNamedNone:
                    gname = None
            try:
                uvsDict[uvs].append((uv, gname))
            except KeyError:
                uvsDict[uvs] = [(uv, gname)]

    def compile(self, ttFont):
        if self.data:
            return (
                struct.pack(
                    ">HLL", self.format, self.length, self.numVarSelectorRecords
                )
                + self.data
            )

        uvsDict = self.uvsDict
        uvsList = sorted(uvsDict.keys())
        self.numVarSelectorRecords = len(uvsList)
        offset = (
            10 + self.numVarSelectorRecords * 11
        )  # current value is end of VarSelectorRecords block.
        data = []
        varSelectorRecords = []
        for uvs in uvsList:
            entryList = uvsDict[uvs]

            defList = [entry for entry in entryList if entry[1] is None]
            if defList:
                defList = [entry[0] for entry in defList]
                defOVSOffset = offset
                defList.sort()

                lastUV = defList[0]
                cnt = -1
                defRecs = []
                for defEntry in defList:
                    cnt += 1
                    if (lastUV + cnt) != defEntry:
                        rec = struct.pack(">3sB", cvtFromUVS(lastUV), cnt - 1)
                        lastUV = defEntry
                        defRecs.append(rec)
                        cnt = 0

                rec = struct.pack(">3sB", cvtFromUVS(lastUV), cnt)
                defRecs.append(rec)

                numDefRecs = len(defRecs)
                data.append(struct.pack(">L", numDefRecs))
                data.extend(defRecs)
                offset += 4 + numDefRecs * 4
            else:
                defOVSOffset = 0

            ndefList = [entry for entry in entryList if entry[1] is not None]
            if ndefList:
                nonDefUVSOffset = offset
                ndefList.sort()
                numNonDefRecs = len(ndefList)
                data.append(struct.pack(">L", numNonDefRecs))
                offset += 4 + numNonDefRecs * 5

                for uv, gname in ndefList:
                    gid = ttFont.getGlyphID(gname)
                    ndrec = struct.pack(">3sH", cvtFromUVS(uv), gid)
                    data.append(ndrec)
            else:
                nonDefUVSOffset = 0

            vrec = struct.pack(">3sLL", cvtFromUVS(uvs), defOVSOffset, nonDefUVSOffset)
            varSelectorRecords.append(vrec)

        data = bytesjoin(varSelectorRecords) + bytesjoin(data)
        self.length = 10 + len(data)
        headerdata = struct.pack(
            ">HLL", self.format, self.length, self.numVarSelectorRecords
        )

        return headerdata + data


class cmap_format_unknown(CmapSubtable):
    headerFormat = ""  # the body is kept verbatim, nothing is unpacked

    def __init__(self, format):
        CmapSubtable.__init__(self, format)
        # We can't read the subtable, so there are no mappings to offer. The raw
        # data is kept instead, and written back out as-is. fromXML() does the
        # same.
        self.cmap = {}

    def ensureDecompiled(self, recurse=False):
        # There is nothing to decompile, so hold on to the data; the base class
        # would drop it.
        pass

    def __getattr__(self, attr):
        # The base class lazily decompiles and retries, relying on
        # ensureDecompiled() clearing self.data to terminate. We keep the data,
        # so retrying would recurse forever.
        raise AttributeError(attr)

    def toXML(self, writer, ttFont):
        cmapName = self.__class__.__name__[:12] + str(self.format)
        writer.begintag(
            cmapName,
            [
                ("platformID", self.platformID),
                ("platEncID", self.platEncID),
            ],
        )
        writer.newline()
        writer.dumphex(self.data)
        writer.endtag(cmapName)
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        self.data = readHex(content)
        self.cmap = {}

    def decompileHeader(self, data, ttFont):
        self.language = 0  # dummy value
        self.data = data

    def decompile(self, data, ttFont):
        # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
        # If not, someone is calling the subtable decompile() directly, and must provide both args.
        if data is not None and ttFont is not None:
            self.decompileHeader(data, ttFont)
        else:
            assert (
                data is None and ttFont is None
            ), "Need both data and ttFont arguments"

    def compile(self, ttFont):
        if self.data:
            return self.data
        else:
            return None


cmap_classes = {
    0: cmap_format_0,
    2: cmap_format_2,
    4: cmap_format_4,
    6: cmap_format_6,
    12: cmap_format_12,
    13: cmap_format_13,
    14: cmap_format_14,
}
