from fontTools.misc import sstruct
from . import DefaultTable
from fontTools.misc.textTools import bytesjoin, safeEval
from .BitmapGlyphMetrics import (
    BigGlyphMetrics,
    bigGlyphMetricsFormat,
    SmallGlyphMetrics,
    smallGlyphMetricsFormat,
)
import struct
import itertools
from collections import deque
import logging

log = logging.getLogger(__name__)

eblcHeaderFormat = """
	> # big endian
	version:  16.16F
	numSizes: I
"""
# The table format string is split to handle sbitLineMetrics simply.
bitmapSizeTableFormatPart1 = """
	> # big endian
	indexSubTableArrayOffset: I
	indexTablesSize:          I
	numberOfIndexSubTables:   I
	colorRef:                 I
"""
# The compound type for hori and vert.
sbitLineMetricsFormat = """
	> # big endian
	ascender:              b
	descender:             b
	widthMax:              B
	caretSlopeNumerator:   b
	caretSlopeDenominator: b
	caretOffset:           b
	minOriginSB:           b
	minAdvanceSB:          b
	maxBeforeBL:           b
	minAfterBL:            b
	pad1:                  b
	pad2:                  b
"""
# hori and vert go between the two parts.
bitmapSizeTableFormatPart2 = """
	> # big endian
	startGlyphIndex: H
	endGlyphIndex:   H
	ppemX:           B
	ppemY:           B
	bitDepth:        B
	flags:           b
"""

indexSubTableArrayFormat = ">HHL"
indexSubTableArraySize = struct.calcsize(indexSubTableArrayFormat)

indexSubHeaderFormat = ">HHL"
indexSubHeaderSize = struct.calcsize(indexSubHeaderFormat)

codeOffsetPairFormat = ">HH"
codeOffsetPairSize = struct.calcsize(codeOffsetPairFormat)


class table_E_B_L_C_(DefaultTable.DefaultTable):
    """Embedded Bitmap Location table

    The ``EBLC`` table contains the locations of monochrome or grayscale
    bitmaps for glyphs. It must be used in concert with the ``EBDT`` table.

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

    dependencies = ["EBDT"]

    # This method can be overridden in subclasses to support new formats
    # without changing the other implementation. Also can be used as a
    # convenience method for coverting a font file to an alternative format.
    def getIndexFormatClass(self, indexFormat):
        return eblc_sub_table_classes[indexFormat]

    def decompile(self, data, ttFont):
        # Save the original data because offsets are from the start of the table.
        origData = data
        i = 0

        dummy = sstruct.unpack(eblcHeaderFormat, data[:8], self)
        i += 8

        self.strikes = []
        for curStrikeIndex in range(self.numSizes):
            curStrike = Strike()
            self.strikes.append(curStrike)
            curTable = curStrike.bitmapSizeTable
            dummy = sstruct.unpack2(
                bitmapSizeTableFormatPart1, data[i : i + 16], curTable
            )
            i += 16
            for metric in ("hori", "vert"):
                metricObj = SbitLineMetrics()
                vars(curTable)[metric] = metricObj
                dummy = sstruct.unpack2(
                    sbitLineMetricsFormat, data[i : i + 12], metricObj
                )
                i += 12
            dummy = sstruct.unpack(
                bitmapSizeTableFormatPart2, data[i : i + 8], curTable
            )
            i += 8

        for curStrike in self.strikes:
            curTable = curStrike.bitmapSizeTable
            for subtableIndex in range(curTable.numberOfIndexSubTables):
                i = (
                    curTable.indexSubTableArrayOffset
                    + subtableIndex * indexSubTableArraySize
                )

                tup = struct.unpack(
                    indexSubTableArrayFormat, data[i : i + indexSubTableArraySize]
                )
                firstGlyphIndex, lastGlyphIndex, additionalOffsetToIndexSubtable = tup
                i = curTable.indexSubTableArrayOffset + additionalOffsetToIndexSubtable

                tup = struct.unpack(
                    indexSubHeaderFormat, data[i : i + indexSubHeaderSize]
                )
                indexFormat, imageFormat, imageDataOffset = tup

                indexFormatClass = self.getIndexFormatClass(indexFormat)
                indexSubTable = indexFormatClass(data[i + indexSubHeaderSize :], ttFont)
                indexSubTable.firstGlyphIndex = firstGlyphIndex
                indexSubTable.lastGlyphIndex = lastGlyphIndex
                indexSubTable.additionalOffsetToIndexSubtable = (
                    additionalOffsetToIndexSubtable
                )
                indexSubTable.indexFormat = indexFormat
                indexSubTable.imageFormat = imageFormat
                indexSubTable.imageDataOffset = imageDataOffset
                indexSubTable.decompile()  # https://github.com/fonttools/fonttools/issues/317
                curStrike.indexSubTables.append(indexSubTable)

    def compile(self, ttFont):
        dataList = []
        self.numSizes = len(self.strikes)
        dataList.append(sstruct.pack(eblcHeaderFormat, self))

        # Data size of the header + bitmapSizeTable needs to be calculated
        # in order to form offsets. This value will hold the size of the data
        # in dataList after all the data is consolidated in dataList.
        dataSize = len(dataList[0])

        # The table will be structured in the following order:
        # (0) header
        # (1) Each bitmapSizeTable [1 ... self.numSizes]
        # (2) Alternate between indexSubTableArray and indexSubTable
        #     for each bitmapSizeTable present.
        #
        # The issue is maintaining the proper offsets when table information
        # gets moved around. All offsets and size information must be recalculated
        # when building the table to allow editing within ttLib and also allow easy
        # import/export to and from XML. All of this offset information is lost
        # when exporting to XML so everything must be calculated fresh so importing
        # from XML will work cleanly. Only byte offset and size information is
        # calculated fresh. Count information like numberOfIndexSubTables is
        # checked through assertions. If the information in this table was not
        # touched or was changed properly then these types of values should match.
        #
        # The table will be rebuilt the following way:
        # (0) Precompute the size of all the bitmapSizeTables. This is needed to
        #     compute the offsets properly.
        # (1) For each bitmapSizeTable compute the indexSubTable and
        #    	indexSubTableArray pair. The indexSubTable must be computed first
        #     so that the offset information in indexSubTableArray can be
        #     calculated. Update the data size after each pairing.
        # (2) Build each bitmapSizeTable.
        # (3) Consolidate all the data into the main dataList in the correct order.

        for _ in self.strikes:
            dataSize += sstruct.calcsize(bitmapSizeTableFormatPart1)
            dataSize += len(("hori", "vert")) * sstruct.calcsize(sbitLineMetricsFormat)
            dataSize += sstruct.calcsize(bitmapSizeTableFormatPart2)

        indexSubTablePairDataList = []
        for curStrike in self.strikes:
            curTable = curStrike.bitmapSizeTable
            curTable.numberOfIndexSubTables = len(curStrike.indexSubTables)
            curTable.indexSubTableArrayOffset = dataSize

            # Precompute the size of the indexSubTableArray. This information
            # is important for correctly calculating the new value for
            # additionalOffsetToIndexSubtable.
            sizeOfSubTableArray = (
                curTable.numberOfIndexSubTables * indexSubTableArraySize
            )
            lowerBound = dataSize
            dataSize += sizeOfSubTableArray
            upperBound = dataSize

            indexSubTableDataList = []
            for indexSubTable in curStrike.indexSubTables:
                indexSubTable.additionalOffsetToIndexSubtable = (
                    dataSize - curTable.indexSubTableArrayOffset
                )
                glyphIds = list(map(ttFont.getGlyphID, indexSubTable.names))
                indexSubTable.firstGlyphIndex = min(glyphIds)
                indexSubTable.lastGlyphIndex = max(glyphIds)
                data = indexSubTable.compile(ttFont)
                indexSubTableDataList.append(data)
                dataSize += len(data)
            curTable.startGlyphIndex = min(
                ist.firstGlyphIndex for ist in curStrike.indexSubTables
            )
            curTable.endGlyphIndex = max(
                ist.lastGlyphIndex for ist in curStrike.indexSubTables
            )

            for i in curStrike.indexSubTables:
                data = struct.pack(
                    indexSubHeaderFormat,
                    i.firstGlyphIndex,
                    i.lastGlyphIndex,
                    i.additionalOffsetToIndexSubtable,
                )
                indexSubTablePairDataList.append(data)
            indexSubTablePairDataList.extend(indexSubTableDataList)
            curTable.indexTablesSize = dataSize - curTable.indexSubTableArrayOffset

        for curStrike in self.strikes:
            curTable = curStrike.bitmapSizeTable
            data = sstruct.pack(bitmapSizeTableFormatPart1, curTable)
            dataList.append(data)
            for metric in ("hori", "vert"):
                metricObj = vars(curTable)[metric]
                data = sstruct.pack(sbitLineMetricsFormat, metricObj)
                dataList.append(data)
            data = sstruct.pack(bitmapSizeTableFormatPart2, curTable)
            dataList.append(data)
        dataList.extend(indexSubTablePairDataList)

        return bytesjoin(dataList)

    def toXML(self, writer, ttFont):
        writer.simpletag("header", [("version", self.version)])
        writer.newline()
        for curIndex, curStrike in enumerate(self.strikes):
            curStrike.toXML(curIndex, writer, ttFont)

    def fromXML(self, name, attrs, content, ttFont):
        if name == "header":
            self.version = safeEval(attrs["version"])
        elif name == "strike":
            if not hasattr(self, "strikes"):
                self.strikes = []
            strikeIndex = safeEval(attrs["index"])
            curStrike = Strike()
            curStrike.fromXML(name, attrs, content, ttFont, self)

            # Grow the strike array to the appropriate size. The XML format
            # allows for the strike index value to be out of order.
            if strikeIndex >= len(self.strikes):
                self.strikes += [None] * (strikeIndex + 1 - len(self.strikes))
            assert self.strikes[strikeIndex] is None, "Duplicate strike EBLC indices."
            self.strikes[strikeIndex] = curStrike


class Strike(object):
    def __init__(self):
        self.bitmapSizeTable = BitmapSizeTable()
        self.indexSubTables = []

    def toXML(self, strikeIndex, writer, ttFont):
        writer.begintag("strike", [("index", strikeIndex)])
        writer.newline()
        self.bitmapSizeTable.toXML(writer, ttFont)
        writer.comment(
            "GlyphIds are written but not read. The firstGlyphIndex and\nlastGlyphIndex values will be recalculated by the compiler."
        )
        writer.newline()
        for indexSubTable in self.indexSubTables:
            indexSubTable.toXML(writer, ttFont)
        writer.endtag("strike")
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont, locator):
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name == "bitmapSizeTable":
                self.bitmapSizeTable.fromXML(name, attrs, content, ttFont)
            elif name.startswith(_indexSubTableSubclassPrefix):
                indexFormat = safeEval(name[len(_indexSubTableSubclassPrefix) :])
                indexFormatClass = locator.getIndexFormatClass(indexFormat)
                indexSubTable = indexFormatClass(None, None)
                indexSubTable.indexFormat = indexFormat
                indexSubTable.fromXML(name, attrs, content, ttFont)
                self.indexSubTables.append(indexSubTable)


class BitmapSizeTable(object):
    # Returns all the simple metric names that bitmap size table
    # cares about in terms of XML creation.
    def _getXMLMetricNames(self):
        dataNames = sstruct.getformat(bitmapSizeTableFormatPart1)[1]
        dataNames = {**dataNames, **sstruct.getformat(bitmapSizeTableFormatPart2)[1]}
        # Skip the first 3 data names because they are byte offsets and counts.
        return list(dataNames.keys())[3:]

    def toXML(self, writer, ttFont):
        writer.begintag("bitmapSizeTable")
        writer.newline()
        for metric in ("hori", "vert"):
            getattr(self, metric).toXML(metric, writer, ttFont)
        for metricName in self._getXMLMetricNames():
            writer.simpletag(metricName, value=getattr(self, metricName))
            writer.newline()
        writer.endtag("bitmapSizeTable")
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        # Create a lookup for all the simple names that make sense to
        # bitmap size table. Only read the information from these names.
        dataNames = set(self._getXMLMetricNames())
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name == "sbitLineMetrics":
                direction = attrs["direction"]
                assert direction in (
                    "hori",
                    "vert",
                ), "SbitLineMetrics direction specified invalid."
                metricObj = SbitLineMetrics()
                metricObj.fromXML(name, attrs, content, ttFont)
                vars(self)[direction] = metricObj
            elif name in dataNames:
                vars(self)[name] = safeEval(attrs["value"])
            else:
                log.warning("unknown name '%s' being ignored in BitmapSizeTable.", name)


class SbitLineMetrics(object):
    def toXML(self, name, writer, ttFont):
        writer.begintag("sbitLineMetrics", [("direction", name)])
        writer.newline()
        for metricName in sstruct.getformat(sbitLineMetricsFormat)[1]:
            writer.simpletag(metricName, value=getattr(self, metricName))
            writer.newline()
        writer.endtag("sbitLineMetrics")
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        metricNames = set(sstruct.getformat(sbitLineMetricsFormat)[1])
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name in metricNames:
                vars(self)[name] = safeEval(attrs["value"])


# Important information about the naming scheme. Used for identifying subtables.
_indexSubTableSubclassPrefix = "eblc_index_sub_table_"


class EblcIndexSubTable(object):
    def __init__(self, data, ttFont):
        self.data = data
        self.ttFont = ttFont
        # TODO Currently non-lazy decompiling doesn't work for this class...
        # if not ttFont.lazy:
        # 	self.decompile()
        # 	del self.data, self.ttFont

    def __getattr__(self, attr):
        # Allow lazy decompile.
        if attr[:2] == "__":
            raise AttributeError(attr)
        if attr == "data":
            raise AttributeError(attr)
        self.decompile()
        return getattr(self, attr)

    def ensureDecompiled(self, recurse=False):
        if hasattr(self, "data"):
            self.decompile()

    # This method just takes care of the indexSubHeader. Implementing subclasses
    # should call it to compile the indexSubHeader and then continue compiling
    # the remainder of their unique format.
    def compile(self, ttFont):
        return struct.pack(
            indexSubHeaderFormat,
            self.indexFormat,
            self.imageFormat,
            self.imageDataOffset,
        )

    # Creates the XML for bitmap glyphs. Each index sub table basically makes
    # the same XML except for specific metric information that is written
    # out via a method call that a subclass implements optionally.
    def toXML(self, writer, ttFont):
        writer.begintag(
            self.__class__.__name__,
            [
                ("imageFormat", self.imageFormat),
                ("firstGlyphIndex", self.firstGlyphIndex),
                ("lastGlyphIndex", self.lastGlyphIndex),
            ],
        )
        writer.newline()
        self.writeMetrics(writer, ttFont)
        # Write out the names as thats all thats needed to rebuild etc.
        # For font debugging of consecutive formats the ids are also written.
        # The ids are not read when moving from the XML format.
        glyphIds = map(ttFont.getGlyphID, self.names)
        for glyphName, glyphId in zip(self.names, glyphIds):
            writer.simpletag("glyphLoc", name=glyphName, id=glyphId)
            writer.newline()
        writer.endtag(self.__class__.__name__)
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        # Read all the attributes. Even though the glyph indices are
        # recalculated, they are still read in case there needs to
        # be an immediate export of the data.
        self.imageFormat = safeEval(attrs["imageFormat"])
        self.firstGlyphIndex = safeEval(attrs["firstGlyphIndex"])
        self.lastGlyphIndex = safeEval(attrs["lastGlyphIndex"])

        self.readMetrics(name, attrs, content, ttFont)

        self.names = []
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name == "glyphLoc":
                self.names.append(attrs["name"])

    # A helper method that writes the metrics for the index sub table. It also
    # is responsible for writing the image size for fixed size data since fixed
    # size is not recalculated on compile. Default behavior is to do nothing.
    def writeMetrics(self, writer, ttFont):
        pass

    # A helper method that is the inverse of writeMetrics.
    def readMetrics(self, name, attrs, content, ttFont):
        pass

    # This method is for fixed glyph data sizes. There are formats where
    # the glyph data is fixed but are actually composite glyphs. To handle
    # this the font spec in indexSubTable makes the data the size of the
    # fixed size by padding the component arrays. This function abstracts
    # out this padding process. Input is data unpadded. Output is data
    # padded only in fixed formats. Default behavior is to return the data.
    def padBitmapData(self, data):
        return data

    # Remove any of the glyph locations and names that are flagged as skipped.
    # This only occurs in formats {1,3}.
    def removeSkipGlyphs(self):
        # Determines if a name, location pair is a valid data location.
        # Skip glyphs are marked when the size is equal to zero.
        def isValidLocation(args):
            name, (startByte, endByte) = args
            return startByte < endByte

        # Remove all skip glyphs.
        dataPairs = list(filter(isValidLocation, zip(self.names, self.locations)))
        self.names, self.locations = list(map(list, zip(*dataPairs)))


# A closure for creating a custom mixin. This is done because formats 1 and 3
# are very similar. The only difference between them is the size per offset
# value. Code put in here should handle both cases generally.
def _createOffsetArrayIndexSubTableMixin(formatStringForDataType):
    # Prep the data size for the offset array data format.
    dataFormat = ">" + formatStringForDataType
    offsetDataSize = struct.calcsize(dataFormat)

    class OffsetArrayIndexSubTableMixin(object):
        def decompile(self):
            numGlyphs = self.lastGlyphIndex - self.firstGlyphIndex + 1
            indexingOffsets = [
                glyphIndex * offsetDataSize for glyphIndex in range(numGlyphs + 2)
            ]
            indexingLocations = zip(indexingOffsets, indexingOffsets[1:])
            offsetArray = [
                struct.unpack(dataFormat, self.data[slice(*loc)])[0]
                for loc in indexingLocations
            ]

            glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
            modifiedOffsets = [offset + self.imageDataOffset for offset in offsetArray]
            self.locations = list(zip(modifiedOffsets, modifiedOffsets[1:]))

            self.names = list(map(self.ttFont.getGlyphName, glyphIds))
            self.removeSkipGlyphs()
            del self.data, self.ttFont

        def compile(self, ttFont):
            # First make sure that all the data lines up properly. Formats 1 and 3
            # must have all its data lined up consecutively. If not this will fail.
            for curLoc, nxtLoc in zip(self.locations, self.locations[1:]):
                assert (
                    curLoc[1] == nxtLoc[0]
                ), "Data must be consecutive in indexSubTable offset formats"

            glyphIds = list(map(ttFont.getGlyphID, self.names))
            # Make sure that all ids are sorted strictly increasing.
            assert all(glyphIds[i] < glyphIds[i + 1] for i in range(len(glyphIds) - 1))

            # Run a simple algorithm to add skip glyphs to the data locations at
            # the places where an id is not present.
            idQueue = deque(glyphIds)
            locQueue = deque(self.locations)
            allGlyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
            allLocations = []
            for curId in allGlyphIds:
                if curId != idQueue[0]:
                    allLocations.append((locQueue[0][0], locQueue[0][0]))
                else:
                    idQueue.popleft()
                    allLocations.append(locQueue.popleft())

            # Now that all the locations are collected, pack them appropriately into
            # offsets. This is the form where offset[i] is the location and
            # offset[i+1]-offset[i] is the size of the data location.
            offsets = list(allLocations[0]) + [loc[1] for loc in allLocations[1:]]
            # Image data offset must be less than or equal to the minimum of locations.
            # This offset may change the value for round tripping but is safer and
            # allows imageDataOffset to not be required to be in the XML version.
            self.imageDataOffset = min(offsets)
            offsetArray = [offset - self.imageDataOffset for offset in offsets]

            dataList = [EblcIndexSubTable.compile(self, ttFont)]
            dataList += [
                struct.pack(dataFormat, offsetValue) for offsetValue in offsetArray
            ]
            # Take care of any padding issues. Only occurs in format 3.
            if offsetDataSize * len(offsetArray) % 4 != 0:
                dataList.append(struct.pack(dataFormat, 0))
            return bytesjoin(dataList)

    return OffsetArrayIndexSubTableMixin


# A Mixin for functionality shared between the different kinds
# of fixed sized data handling. Both kinds have big metrics so
# that kind of special processing is also handled in this mixin.
class FixedSizeIndexSubTableMixin(object):
    def writeMetrics(self, writer, ttFont):
        writer.simpletag("imageSize", value=self.imageSize)
        writer.newline()
        self.metrics.toXML(writer, ttFont)

    def readMetrics(self, name, attrs, content, ttFont):
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, content = element
            if name == "imageSize":
                self.imageSize = safeEval(attrs["value"])
            elif name == BigGlyphMetrics.__name__:
                self.metrics = BigGlyphMetrics()
                self.metrics.fromXML(name, attrs, content, ttFont)
            elif name == SmallGlyphMetrics.__name__:
                log.warning(
                    "SmallGlyphMetrics being ignored in format %d.", self.indexFormat
                )

    def padBitmapData(self, data):
        # Make sure that the data isn't bigger than the fixed size.
        assert len(data) <= self.imageSize, (
            "Data in indexSubTable format %d must be less than the fixed size."
            % self.indexFormat
        )
        # Pad the data so that it matches the fixed size.
        pad = (self.imageSize - len(data)) * b"\0"
        return data + pad


class eblc_index_sub_table_1(
    _createOffsetArrayIndexSubTableMixin("L"), EblcIndexSubTable
):
    pass


class eblc_index_sub_table_2(FixedSizeIndexSubTableMixin, EblcIndexSubTable):
    def decompile(self):
        (self.imageSize,) = struct.unpack(">L", self.data[:4])
        self.metrics = BigGlyphMetrics()
        sstruct.unpack2(bigGlyphMetricsFormat, self.data[4:], self.metrics)
        glyphIds = list(range(self.firstGlyphIndex, self.lastGlyphIndex + 1))
        offsets = [
            self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds) + 1)
        ]
        self.locations = list(zip(offsets, offsets[1:]))
        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
        del self.data, self.ttFont

    def compile(self, ttFont):
        glyphIds = list(map(ttFont.getGlyphID, self.names))
        # Make sure all the ids are consecutive. This is required by Format 2.
        assert glyphIds == list(
            range(self.firstGlyphIndex, self.lastGlyphIndex + 1)
        ), "Format 2 ids must be consecutive."
        self.imageDataOffset = min(next(iter(zip(*self.locations))))

        dataList = [EblcIndexSubTable.compile(self, ttFont)]
        dataList.append(struct.pack(">L", self.imageSize))
        dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
        return bytesjoin(dataList)


class eblc_index_sub_table_3(
    _createOffsetArrayIndexSubTableMixin("H"), EblcIndexSubTable
):
    pass


class eblc_index_sub_table_4(EblcIndexSubTable):
    def decompile(self):
        (numGlyphs,) = struct.unpack(">L", self.data[:4])
        data = self.data[4:]
        indexingOffsets = [
            glyphIndex * codeOffsetPairSize for glyphIndex in range(numGlyphs + 2)
        ]
        indexingLocations = zip(indexingOffsets, indexingOffsets[1:])
        glyphArray = [
            struct.unpack(codeOffsetPairFormat, data[slice(*loc)])
            for loc in indexingLocations
        ]
        glyphIds, offsets = list(map(list, zip(*glyphArray)))
        # There are one too many glyph ids. Get rid of the last one.
        glyphIds.pop()

        offsets = [offset + self.imageDataOffset for offset in offsets]
        self.locations = list(zip(offsets, offsets[1:]))
        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
        del self.data, self.ttFont

    def compile(self, ttFont):
        # First make sure that all the data lines up properly. Format 4
        # must have all its data lined up consecutively. If not this will fail.
        for curLoc, nxtLoc in zip(self.locations, self.locations[1:]):
            assert (
                curLoc[1] == nxtLoc[0]
            ), "Data must be consecutive in indexSubTable format 4"

        offsets = list(self.locations[0]) + [loc[1] for loc in self.locations[1:]]
        # Image data offset must be less than or equal to the minimum of locations.
        # Resetting this offset may change the value for round tripping but is safer
        # and allows imageDataOffset to not be required to be in the XML version.
        self.imageDataOffset = min(offsets)
        offsets = [offset - self.imageDataOffset for offset in offsets]
        glyphIds = list(map(ttFont.getGlyphID, self.names))
        # Create an iterator over the ids plus a padding value.
        idsPlusPad = list(itertools.chain(glyphIds, [0]))

        dataList = [EblcIndexSubTable.compile(self, ttFont)]
        dataList.append(struct.pack(">L", len(glyphIds)))
        tmp = [
            struct.pack(codeOffsetPairFormat, *cop) for cop in zip(idsPlusPad, offsets)
        ]
        dataList += tmp
        data = bytesjoin(dataList)
        return data


class eblc_index_sub_table_5(FixedSizeIndexSubTableMixin, EblcIndexSubTable):
    def decompile(self):
        self.origDataLen = 0
        (self.imageSize,) = struct.unpack(">L", self.data[:4])
        data = self.data[4:]
        self.metrics, data = sstruct.unpack2(
            bigGlyphMetricsFormat, data, BigGlyphMetrics()
        )
        (numGlyphs,) = struct.unpack(">L", data[:4])
        data = data[4:]
        glyphIds = [
            struct.unpack(">H", data[2 * i : 2 * (i + 1)])[0] for i in range(numGlyphs)
        ]

        offsets = [
            self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds) + 1)
        ]
        self.locations = list(zip(offsets, offsets[1:]))
        self.names = list(map(self.ttFont.getGlyphName, glyphIds))
        del self.data, self.ttFont

    def compile(self, ttFont):
        self.imageDataOffset = min(next(iter(zip(*self.locations))))
        dataList = [EblcIndexSubTable.compile(self, ttFont)]
        dataList.append(struct.pack(">L", self.imageSize))
        dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics))
        glyphIds = list(map(ttFont.getGlyphID, self.names))
        dataList.append(struct.pack(">L", len(glyphIds)))
        dataList += [struct.pack(">H", curId) for curId in glyphIds]
        if len(glyphIds) % 2 == 1:
            dataList.append(struct.pack(">H", 0))
        return bytesjoin(dataList)


# Dictionary of indexFormat to the class representing that format.
eblc_sub_table_classes = {
    1: eblc_index_sub_table_1,
    2: eblc_index_sub_table_2,
    3: eblc_index_sub_table_3,
    4: eblc_index_sub_table_4,
    5: eblc_index_sub_table_5,
}
