from fontTools.misc import sstruct
from fontTools.misc.fixedTools import (
    fixedToFloat as fi2fl,
    floatToFixed as fl2fi,
    floatToFixedToStr as fl2str,
    strToFixedToFloat as str2fl,
)
from fontTools.misc.textTools import bytesjoin, safeEval
from fontTools.ttLib import TTLibError
from . import DefaultTable
import struct
from collections.abc import MutableMapping


# Apple's documentation of 'trak':
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html

TRAK_HEADER_FORMAT = """
	> # big endian
	version:     16.16F
	format:      H
	horizOffset: H
	vertOffset:  H
	reserved:    H
"""

TRAK_HEADER_FORMAT_SIZE = sstruct.calcsize(TRAK_HEADER_FORMAT)


TRACK_DATA_FORMAT = """
	> # big endian
	nTracks:         H
	nSizes:          H
	sizeTableOffset: L
"""

TRACK_DATA_FORMAT_SIZE = sstruct.calcsize(TRACK_DATA_FORMAT)


TRACK_TABLE_ENTRY_FORMAT = """
	> # big endian
	track:      16.16F
	nameIndex:       H
	offset:          H
"""

TRACK_TABLE_ENTRY_FORMAT_SIZE = sstruct.calcsize(TRACK_TABLE_ENTRY_FORMAT)


# size values are actually '16.16F' fixed-point values, but here I do the
# fixedToFloat conversion manually instead of relying on sstruct
SIZE_VALUE_FORMAT = ">l"
SIZE_VALUE_FORMAT_SIZE = struct.calcsize(SIZE_VALUE_FORMAT)

# per-Size values are in 'FUnits', i.e. 16-bit signed integers
PER_SIZE_VALUE_FORMAT = ">h"
PER_SIZE_VALUE_FORMAT_SIZE = struct.calcsize(PER_SIZE_VALUE_FORMAT)


class table__t_r_a_k(DefaultTable.DefaultTable):
    """The AAT ``trak`` table can store per-size adjustments to each glyph's
    sidebearings to make when tracking is enabled, which applications can
    use to provide more visually balanced line spacing.

    See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
    """

    def compile(self, ttFont):
        dataList = []
        offset = TRAK_HEADER_FORMAT_SIZE
        for direction in ("horiz", "vert"):
            trackData = getattr(self, direction + "Data", TrackData())
            offsetName = direction + "Offset"
            # set offset to 0 if None or empty
            if not trackData:
                setattr(self, offsetName, 0)
                continue
            # TrackData table format must be longword aligned
            alignedOffset = (offset + 3) & ~3
            padding, offset = b"\x00" * (alignedOffset - offset), alignedOffset
            setattr(self, offsetName, offset)

            data = trackData.compile(offset)
            offset += len(data)
            dataList.append(padding + data)

        self.reserved = 0
        tableData = bytesjoin([sstruct.pack(TRAK_HEADER_FORMAT, self)] + dataList)
        return tableData

    def decompile(self, data, ttFont):
        sstruct.unpack(TRAK_HEADER_FORMAT, data[:TRAK_HEADER_FORMAT_SIZE], self)
        for direction in ("horiz", "vert"):
            trackData = TrackData()
            offset = getattr(self, direction + "Offset")
            if offset != 0:
                trackData.decompile(data, offset)
            setattr(self, direction + "Data", trackData)

    def toXML(self, writer, ttFont):
        writer.simpletag("version", value=self.version)
        writer.newline()
        writer.simpletag("format", value=self.format)
        writer.newline()
        for direction in ("horiz", "vert"):
            dataName = direction + "Data"
            writer.begintag(dataName)
            writer.newline()
            trackData = getattr(self, dataName, TrackData())
            trackData.toXML(writer, ttFont)
            writer.endtag(dataName)
            writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        if name == "version":
            self.version = safeEval(attrs["value"])
        elif name == "format":
            self.format = safeEval(attrs["value"])
        elif name in ("horizData", "vertData"):
            trackData = TrackData()
            setattr(self, name, trackData)
            for element in content:
                if not isinstance(element, tuple):
                    continue
                name, attrs, content_ = element
                trackData.fromXML(name, attrs, content_, ttFont)


class TrackData(MutableMapping):
    def __init__(self, initialdata={}):
        self._map = dict(initialdata)

    def compile(self, offset):
        nTracks = len(self)
        sizes = self.sizes()
        nSizes = len(sizes)

        # offset to the start of the size subtable
        offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE * nTracks
        trackDataHeader = sstruct.pack(
            TRACK_DATA_FORMAT,
            {"nTracks": nTracks, "nSizes": nSizes, "sizeTableOffset": offset},
        )

        entryDataList = []
        perSizeDataList = []
        # offset to per-size tracking values
        offset += SIZE_VALUE_FORMAT_SIZE * nSizes
        # sort track table entries by track value
        for track, entry in sorted(self.items()):
            assert entry.nameIndex is not None
            entry.track = track
            entry.offset = offset
            entryDataList += [sstruct.pack(TRACK_TABLE_ENTRY_FORMAT, entry)]
            # sort per-size values by size
            for size, value in sorted(entry.items()):
                perSizeDataList += [struct.pack(PER_SIZE_VALUE_FORMAT, value)]
            offset += PER_SIZE_VALUE_FORMAT_SIZE * nSizes
        # sort size values
        sizeDataList = [
            struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)
        ]

        data = bytesjoin(
            [trackDataHeader] + entryDataList + sizeDataList + perSizeDataList
        )
        return data

    def decompile(self, data, offset):
        # initial offset is from the start of trak table to the current TrackData
        trackDataHeader = data[offset : offset + TRACK_DATA_FORMAT_SIZE]
        if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE:
            raise TTLibError("not enough data to decompile TrackData header")
        sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self)
        offset += TRACK_DATA_FORMAT_SIZE

        nSizes = self.nSizes
        sizeTableOffset = self.sizeTableOffset
        sizeTable = []
        for i in range(nSizes):
            sizeValueData = data[
                sizeTableOffset : sizeTableOffset + SIZE_VALUE_FORMAT_SIZE
            ]
            if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE:
                raise TTLibError("not enough data to decompile TrackData size subtable")
            (sizeValue,) = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData)
            sizeTable.append(fi2fl(sizeValue, 16))
            sizeTableOffset += SIZE_VALUE_FORMAT_SIZE

        for i in range(self.nTracks):
            entry = TrackTableEntry()
            entryData = data[offset : offset + TRACK_TABLE_ENTRY_FORMAT_SIZE]
            if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE:
                raise TTLibError("not enough data to decompile TrackTableEntry record")
            sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry)
            perSizeOffset = entry.offset
            for j in range(nSizes):
                size = sizeTable[j]
                perSizeValueData = data[
                    perSizeOffset : perSizeOffset + PER_SIZE_VALUE_FORMAT_SIZE
                ]
                if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE:
                    raise TTLibError(
                        "not enough data to decompile per-size track values"
                    )
                (perSizeValue,) = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData)
                entry[size] = perSizeValue
                perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE
            self[entry.track] = entry
            offset += TRACK_TABLE_ENTRY_FORMAT_SIZE

    def toXML(self, writer, ttFont):
        nTracks = len(self)
        nSizes = len(self.sizes())
        writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes))
        writer.newline()
        for track, entry in sorted(self.items()):
            assert entry.nameIndex is not None
            entry.track = track
            entry.toXML(writer, ttFont)

    def fromXML(self, name, attrs, content, ttFont):
        if name != "trackEntry":
            return
        entry = TrackTableEntry()
        entry.fromXML(name, attrs, content, ttFont)
        self[entry.track] = entry

    def sizes(self):
        if not self:
            return frozenset()
        tracks = list(self.tracks())
        sizes = self[tracks.pop(0)].sizes()
        for track in tracks:
            entrySizes = self[track].sizes()
            if sizes != entrySizes:
                raise TTLibError(
                    "'trak' table entries must specify the same sizes: "
                    "%s != %s" % (sorted(sizes), sorted(entrySizes))
                )
        return frozenset(sizes)

    def __getitem__(self, track):
        return self._map[track]

    def __delitem__(self, track):
        del self._map[track]

    def __setitem__(self, track, entry):
        self._map[track] = entry

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

    def __iter__(self):
        return iter(self._map)

    def keys(self):
        return self._map.keys()

    tracks = keys

    def __repr__(self):
        return "TrackData({})".format(self._map if self else "")


class TrackTableEntry(MutableMapping):
    def __init__(self, values={}, nameIndex=None):
        self.nameIndex = nameIndex
        self._map = dict(values)

    def toXML(self, writer, ttFont):
        name = ttFont["name"].getDebugName(self.nameIndex)
        writer.begintag(
            "trackEntry",
            (("value", fl2str(self.track, 16)), ("nameIndex", self.nameIndex)),
        )
        writer.newline()
        if name:
            writer.comment(name)
            writer.newline()
        for size, perSizeValue in sorted(self.items()):
            writer.simpletag("track", size=fl2str(size, 16), value=perSizeValue)
            writer.newline()
        writer.endtag("trackEntry")
        writer.newline()

    def fromXML(self, name, attrs, content, ttFont):
        self.track = str2fl(attrs["value"], 16)
        self.nameIndex = safeEval(attrs["nameIndex"])
        for element in content:
            if not isinstance(element, tuple):
                continue
            name, attrs, _ = element
            if name != "track":
                continue
            size = str2fl(attrs["size"], 16)
            self[size] = safeEval(attrs["value"])

    def __getitem__(self, size):
        return self._map[size]

    def __delitem__(self, size):
        del self._map[size]

    def __setitem__(self, size, value):
        self._map[size] = value

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

    def __iter__(self):
        return iter(self._map)

    def keys(self):
        return self._map.keys()

    sizes = keys

    def __repr__(self):
        return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self.nameIndex == other.nameIndex and dict(self) == dict(other)

    def __ne__(self, other):
        result = self.__eq__(other)
        return result if result is NotImplemented else not result
