local vstruct = require("vstruct")
local hb = require("justenoughharfbuzz")
local zlib = require("zlib")

local parseName = function (str)
   if str:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(str)

   local names = {}
   local MacintoshLanguages = {
      [0] = "en",
      [1] = "fr",
      [2] = "de",
      [3] = "it",
      [4] = "nl",
      [5] = "sv",
      [6] = "es",
      [7] = "da",
      [8] = "pt",
      [9] = "no",
      [10] = "he",
      [11] = "ja",
      [12] = "arb",
      [13] = "fi",
      [14] = "el",
      [15] = "is",
      [16] = "mt",
      [17] = "tr",
      [18] = "hr",
      [19] = "zh-Hant",
      [20] = "ur",
      [21] = "hi",
      [22] = "th",
      [23] = "ko",
      [24] = "lt",
      [25] = "pl",
      [26] = "hu",
      [27] = "et",
      [28] = "lv",
      [29] = "smi",
      [30] = "fo",
      [31] = "fa",
      [32] = "ru",
      [33] = "zh-Hans",
      [34] = "nl-BE",
      [35] = "gle",
      [36] = "sq",
      [37] = "ro",
      [38] = "cs",
      [39] = "sk",
      [40] = "sl",
      [41] = "yi",
      [42] = "sr",
      [43] = "mk",
      [44] = "bg",
      [45] = "uk",
      [46] = "be",
      [47] = "uz",
      [48] = "kk",
      [49] = "az-Cyrl",
      [50] = "az-Arab",
      [51] = "hy",
      [52] = "ka",
      [53] = "mo",
      [54] = "ky",
      [55] = "tg",
      [56] = "tk",
      [57] = "mn-Mong",
      [58] = "mn-Cyrl",
      [59] = "ps",
      [60] = "ku",
      [61] = "ks",
      [62] = "sd",
      [63] = "bo",
      [64] = "ne",
      [65] = "sa",
      [66] = "mr",
      [67] = "bn",
      [68] = "as",
      [69] = "gu",
      [70] = "pa",
      [71] = "or",
      [72] = "ml",
      [73] = "kn",
      [74] = "ta",
      [75] = "te",
      [76] = "se",
      [77] = "my",
      [78] = "km",
      [79] = "lo",
      [80] = "vi",
      [81] = "id",
      [82] = "tl",
      [83] = "ms",
      [84] = "ms",
      [85] = "am",
      [86] = "ti",
      [87] = "gax",
      [88] = "so",
      [89] = "sw",
      [90] = "rw",
      [91] = "rn",
      [92] = "ny",
      [93] = "mg",
      [94] = "eo",
      [128] = "cy",
      [129] = "eu",
      [130] = "ca",
      [131] = "la",
      [132] = "qu",
      [133] = "gn",
      [134] = "ay",
      [135] = "tt",
      [136] = "ug",
      [137] = "dz",
      [138] = "jv",
      [139] = "su",
      [140] = "gl",
      [141] = "af",
      [142] = "br",
      [143] = "iu",
      [144] = "gd",
      [145] = "gv",
      [146] = "gle",
      [147] = "to",
      [148] = "el-polyton",
      [149] = "kl",
      [150] = "az",
   }

   local WindowsLanguages = {
      [4] = "zh-CHS",
      [1025] = "ar-SA",
      [1026] = "bg-BG",
      [1027] = "ca-ES",
      [1028] = "zh-TW",
      [1029] = "cs-CZ",
      [1030] = "da-DK",
      [1031] = "de-DE",
      [1032] = "el-GR",
      [1033] = "en-US",
      [1035] = "fi-FI",
      [1036] = "fr-FR",
      [1037] = "he-IL",
      [1038] = "hu-HU",
      [1039] = "is-IS",
      [1040] = "it-IT",
      [1041] = "ja-JP",
      [1042] = "ko-KR",
      [1043] = "nl-NL",
      [1044] = "nb-NO",
      [1045] = "pl-PL",
      [1046] = "pt-BR",
      [1048] = "ro-RO",
      [1049] = "ru-RU",
      [1050] = "hr-HR",
      [1051] = "sk-SK",
      [1052] = "sq-AL",
      [1053] = "sv-SE",
      [1054] = "th-TH",
      [1055] = "tr-TR",
      [1056] = "ur-PK",
      [1057] = "id-ID",
      [1058] = "uk-UA",
      [1059] = "be-BY",
      [1060] = "sl-SI",
      [1061] = "et-EE",
      [1062] = "lv-LV",
      [1063] = "lt-LT",
      [1065] = "fa-IR",
      [1066] = "vi-VN",
      [1067] = "hy-AM",
      [1068] = "Lt-az-AZ",
      [1069] = "eu-ES",
      [1071] = "mk-MK",
      [1078] = "af-ZA",
      [1079] = "ka-GE",
      [1080] = "fo-FO",
      [1081] = "hi-IN",
      [1086] = "ms-MY",
      [1087] = "kk-KZ",
      [1088] = "ky-KZ",
      [1089] = "sw-KE",
      [1091] = "Lt-uz-UZ",
      [1092] = "tt-RU",
      [1094] = "pa-IN",
      [1095] = "gu-IN",
      [1097] = "ta-IN",
      [1098] = "te-IN",
      [1099] = "kn-IN",
      [1102] = "mr-IN",
      [1103] = "sa-IN",
      [1104] = "mn-MN",
      [1110] = "gl-ES",
      [1111] = "kok-IN",
      [1114] = "syr-SY",
      [1125] = "div-MV",
      [2049] = "ar-IQ",
      [2052] = "zh-CN",
      [2055] = "de-CH",
      [2057] = "en-GB",
      [2058] = "es-MX",
      [2060] = "fr-BE",
      [2064] = "it-CH",
      [2067] = "nl-BE",
      [2068] = "nn-NO",
      [2070] = "pt-PT",
      [2074] = "Lt-sr-SP",
      [2077] = "sv-FI",
      [2092] = "Cy-az-AZ",
      [2110] = "ms-BN",
      [2115] = "Cy-uz-UZ",
      [3073] = "ar-EG",
      [3076] = "zh-HK",
      [3079] = "de-AT",
      [3081] = "en-AU",
      [3082] = "es-ES",
      [3084] = "fr-CA",
      [3098] = "Cy-sr-SP",
      [4097] = "ar-LY",
      [4100] = "zh-SG",
      [4103] = "de-LU",
      [4105] = "en-CA",
      [4106] = "es-GT",
      [4108] = "fr-CH",
      [5121] = "ar-DZ",
      [5124] = "zh-MO",
      [5127] = "de-LI",
      [5129] = "en-NZ",
      [5130] = "es-CR",
      [5132] = "fr-LU",
      [6145] = "ar-MA",
      [6153] = "en-IE",
      [6154] = "es-PA",
      [6156] = "fr-MC",
      [7169] = "ar-TN",
      [7177] = "en-ZA",
      [7178] = "es-DO",
      [8193] = "ar-OM",
      [8201] = "en-JM",
      [8202] = "es-VE",
      [9217] = "ar-YE",
      [9225] = "en-CB",
      [9226] = "es-CO",
      [10241] = "ar-SY",
      [10249] = "en-BZ",
      [10250] = "es-PE",
      [11265] = "ar-JO",
      [11273] = "en-TT",
      [11274] = "es-AR",
      [12289] = "ar-LB",
      [12297] = "en-ZW",
      [12298] = "es-EC",
      [13313] = "ar-KW",
      [13321] = "en-PH",
      [13322] = "es-CL",
      [14337] = "ar-AE",
      [14346] = "es-UY",
      [15361] = "ar-BH",
      [15370] = "es-PY",
      [16385] = "ar-QA",
      [16394] = "es-BO",
      [17418] = "es-SV",
      [18442] = "es-HN",
      [19466] = "es-NI",
      [20490] = "es-PR",
      [31748] = "zh-CHT",
   }
   local name = vstruct.read(">format:u2 count:u2 sOffset:u2", fd)
   name.records = {}
   if name.format == 1 then
      return
   end
   for i = 1, name.count do
      local record = vstruct.read(">platform:u2 encoding:u2 language:u2 name:u2 length:u2 offset:u2", fd)
      name.records[i] = record
      local language
      if (record.platform == 1 and record.encoding == 0) or (record.platform == 3 and record.encoding == 1) then
         if record.language < 0x8000 and record.platform == 1 then
            language = MacintoshLanguages[record.language]
         elseif record.language < 0x8000 and record.platform == 3 then
            language = WindowsLanguages[record.language]
         end
      end
      name.records[i].language = language
   end
   for i = 1, name.count do
      local record = name.records[i]
      local language = record.language
      if language then
         if not names[record.name] then
            names[record.name] = {}
         end
         if record.length > 0 then
            names[record.name][language] =
               vstruct.read(">@" .. name.sOffset + record.offset .. "s" .. record.length, fd)
            if record.platform == 3 then
               names[record.name][language] = { SU.utf16be_to_utf8(names[record.name][language][1]) }
            end
         end
      end
   end

   return names
end

local parseMaxp = function (str)
   if str:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(str)

   return vstruct.read(">version:u4 numGlyphs:u2", fd)
end

local function parseColr (str)
   if str:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(str)

   local version = vstruct.readvals(">u2", fd)
   if version ~= 0 then
      return
   end

   local colr = {}

   local header = vstruct.read(">nBases:u2 oBases:u4 oLayers:u4 nLayers:u2", fd)
   local bases = vstruct.read(">@" .. header.oBases .. " " .. header.nBases .. "*{gid:u2 firstLayer:u2 nLayers:u2}", fd)
   local layers = vstruct.read(">@" .. header.oLayers .. " " .. header.nLayers .. "*{gid:u2 paletteIndex:u2}", fd)

   for i = 1, #bases do
      local base = bases[i]
      local glyphLayers = {}
      for j = base.firstLayer + 1, base.firstLayer + base.nLayers do
         local layer = layers[j]
         layer.paletteIndex = layer.paletteIndex + 1
         glyphLayers[#glyphLayers + 1] = layer
      end
      colr[base.gid] = glyphLayers
   end

   return colr
end

local function parseCpal (str)
   if str:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(str)

   local version = vstruct.readvals(">u2", fd)
   if version > 1 then
      return
   end

   local cpal = {}

   local header = vstruct.read(">nPalettesEntries:u2 nPalettes:u2 nColors:u2 oFirstColor:u4", fd)
   -- local colorIndices = vstruct.read("> " .. header.nPalettes .. "*u2", fd)
   local colors = vstruct.read(">@" .. header.oFirstColor .. " " .. header.nColors .. "*{b:u1 g:u1 r:u1 a:u1}", fd)

   for _ = 1, header.nPalettes do
      local palette = {}
      for j = 1, header.nPalettesEntries do
         local color = colors[j]
         for k, v in pairs(color) do
            color[k] = v / 255
         end
         palette[#palette + 1] = color
      end
      cpal[#cpal + 1] = palette
   end

   return cpal
end

local function parseSvg (str)
   if str:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(str)

   local offsets = {}
   local header = vstruct.read(">version:u2 oDocIndex:u4", fd)
   if header.version > 0 then
      return
   end
   local numEntries = vstruct.read(">@" .. header.oDocIndex .. " u2", fd)
   local outlines =
      vstruct.read("> " .. numEntries[1] .. "*{startGlyphID:u2 endGlyphID:u2 svgDocOffset:u4 svgDocLength:u4}", fd)
   for i = 1, numEntries[1] do
      local outline = outlines[i]
      for j = outline.startGlyphID, outline.endGlyphID do
         offsets[j] = {
            svgDocLength = outline.svgDocLength,
            svgDocOffset = outline.svgDocOffset + header.oDocIndex,
            -- Note that now we are an offset from the beginning of the table
         }
      end
   end
   return offsets
end

local function parseHead (s)
   if s:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(s)
   return vstruct.read(
      ">majorVersion:u2 minorVersion:u2 fontRevision:u4 checkSumAdjustment:u4 magicNumber:u4 flags:u2 unitsPerEm:u2 created:u8 modified:u8 xMin:i2 yMin:i2 xMax:i2 yMax:i2 macStyle:u2 lowestRecPPEM:u2 fontDirectionHint:i2 indexToLocFormat:i2 glyphDataFormat:i2",
      fd
   )
end

local parseDeviceTable = function (offset, fd)
   local header = vstruct.read(">@" .. offset .. " startSize:u2 endSize:u2 deltaFormat:u2", fd)
   local size = header.endSize - header.startSize + 1
   local buf
   if header.deltaFormat == 0x0001 then
      buf = vstruct.read("> " .. math.ceil(size + 7 / 8) .. "*[2| i2 i2 i2 i2 i2 i2 i2 i2 ]", fd)
   elseif header.deltaFormat == 0x0002 then
      buf = vstruct.read("> " .. math.ceil(size / 4) .. "*[2| i4 i4 i4 i4 ]", fd)
   elseif header.deltaFormat == 0x0003 then
      buf = vstruct.read("> " .. math.ceil(size / 2) .. "*[2| i8 i8 ]", fd)
   else
      SU.warn("DeltaFormat " .. header.deltaFormat .. " in Device Table is not supported; ignore the table")
      return nil
   end
   local deviceTable = {}
   for i = 1, size do
      deviceTable[header.startSize + i - 1] = buf[i]
   end
   return deviceTable
end

local parseCoverage = function (offset, fd)
   local coverageFormat = vstruct.readvals(">@" .. offset .. " u2", fd)
   if coverageFormat == 1 then
      local glyphCount = vstruct.readvals("> u2", fd)
      return vstruct.read("> " .. glyphCount .. "*u2", fd)
   elseif coverageFormat == 2 then
      local rangeCount = vstruct.readvals("> u2", fd)
      local ranges = vstruct.read("> " .. rangeCount .. "*{ &RangeRecord }", fd)
      local coverage = {}
      for i = 1, #ranges do
         for glyphID = ranges[i].startGlyphID, ranges[i].endGlyphID do
            local index = ranges[i].startCoverageIndex + glyphID - ranges[i].startGlyphID + 1 -- array in lua is one-based
            if coverage[index] then
               SU.error(glyphID .. " already exist in coverage when processing " .. ranges[i])
            end
            coverage[index] = glyphID
         end
      end
      return coverage
   else
      SU.error("Unsupported coverage table format " .. coverageFormat)
   end
end

-- Removes the indirection in a MathValueRecord by replacing the
-- deviceTableOffset field by an actual device table in the deviceTable field.
local fetchMathValueRecord = function (record, parent_offset, fd)
   local newRecord = { value = record.value }
   if record.deviceTableOffset ~= 0 then
      newRecord.deviceTable = parseDeviceTable(parent_offset + record.deviceTableOffset, fd)
   end
   return newRecord
end

local parseConstants = function (offset, fd)
   local mathConstantNames = {
      "scriptPercentScaleDown",
      "scriptScriptPercentScaleDown",
      "delimitedSubFormulaMinHeight",
      "displayOperatorMinHeight",
      "mathLeading",
      "axisHeight",
      "accentBaseHeight",
      "flattenedAccentBaseHeight",
      "subscriptShiftDown",
      "subscriptTopMax",
      "subscriptBaselineDropMin",
      "superscriptShiftUp",
      "superscriptShiftUpCramped",
      "superscriptBottomMin",
      "superscriptBaselineDropMax",
      "subSuperscriptGapMin",
      "superscriptBottomMaxWithSubscript",
      "spaceAfterScript",
      "upperLimitGapMin",
      "upperLimitBaselineRiseMin",
      "lowerLimitGapMin",
      "lowerLimitBaselineDropMin",
      "stackTopShiftUp",
      "stackTopDisplayStyleShiftUp",
      "stackBottomShiftDown",
      "stackBottomDisplayStyleShiftDown",
      "stackGapMin",
      "stackDisplayStyleGapMin",
      "stretchStackTopShiftUp",
      "stretchStackBottomShiftDown",
      "stretchStackGapAboveMin",
      "stretchStackGapBelowMin",
      "fractionNumeratorShiftUp",
      "fractionNumeratorDisplayStyleShiftUp",
      "fractionDenominatorShiftDown",
      "fractionDenominatorDisplayStyleShiftDown",
      "fractionNumeratorGapMin",
      "fractionNumDisplayStyleGapMin",
      "fractionRuleThickness",
      "fractionDenominatorGapMin",
      "fractionDenomDisplayStyleGapMin",
      "skewedFractionHorizontalGap",
      "skewedFractionVerticalGap",
      "overbarVerticalGap",
      "overbarRuleThickness",
      "overbarExtraAscender",
      "underbarVerticalGap",
      "underbarRuleThickness",
      "underbarExtraDescender",
      "radicalVerticalGap",
      "radicalDisplayStyleVerticalGap",
      "radicalRuleThickness",
      "radicalExtraAscender",
      "radicalKernBeforeDegree",
      "radicalKernAfterDegree",
      "radicalDegreeBottomRaisePercent",
   }
   local mathConstantTypes = {
      "i2",
      "i2",
      "u2",
      "u2",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "{ &MathValueRecord }",
      "i2",
   }
   local mathConstantFormat = ">@" .. offset
   for i = 1, #mathConstantNames do
      mathConstantFormat = mathConstantFormat .. " " .. mathConstantNames[i] .. ":" .. mathConstantTypes[i]
   end
   local mathConstants = vstruct.read(mathConstantFormat, fd)
   for k, v in pairs(mathConstants) do
      if v and type(v) == "table" then
         mathConstants[k] = fetchMathValueRecord(v, offset, fd)
      end
   end
   return mathConstants
end

local parseMathKern = function (offset, fd)
   local heightCount = vstruct.readvals(">@" .. offset .. " u2", fd)
   local mathKern = vstruct.read(
      "> correctionHeight:{ "
         .. heightCount
         .. "*{ &MathValueRecord } } kernValues:{ "
         .. (heightCount + 1)
         .. "*{ &MathValueRecord } }",
      fd
   )
   for i = 1, #mathKern.correctionHeight do
      mathKern.correctionHeight[i] = fetchMathValueRecord(mathKern.correctionHeight[i], offset, fd)
   end
   for i = 1, #mathKern.kernValues do
      mathKern.kernValues[i] = fetchMathValueRecord(mathKern.kernValues[i], offset, fd)
   end
   return mathKern
end

local parsePerGlyphTable = function (offset, type, fd)
   local coverageOffset = vstruct.readvals(">@" .. offset .. " u2", fd)
   local coverageTable = parseCoverage(offset + coverageOffset, fd)
   local count = vstruct.readvals(">@" .. (offset + 2) .. " u2", fd)
   if count ~= #coverageTable then
      SU.error("Coverage table corrupted")
   end
   local table = vstruct.read("> " .. count .. "*{ " .. type .. " }", fd)
   local result = {}
   for i = 1, count do
      if type == "&MathValueRecord" then
         result[coverageTable[i]] = fetchMathValueRecord(table[i], offset, fd)
      elseif type == "&MathKernInfoRecord" then
         result[coverageTable[i]] = {
            topRightMathKern = table[i].topRightMathKernOffset ~= 0
                  and parseMathKern(offset + table[i].topRightMathKernOffset, fd)
               or nil,
            topLeftMathKern = table[i].topLeftMathKernOffset ~= 0
                  and parseMathKern(offset + table[i].topLeftMathKernOffset, fd)
               or nil,
            bottomRightMathKern = table[i].bottomRightMathKernOffset ~= 0
                  and parseMathKern(offset + table[i].bottomRightMathKernOffset, fd)
               or nil,
            bottomLeftMathKern = table[i].bottomLeftMathKernOffset ~= 0
                  and parseMathKern(offset + table[i].bottomLeftMathKernOffset, fd)
               or nil,
         }
      else
         result[coverageTable[i]] = table[i]
      end
   end
   return result
end

local parseMathVariants = function (offset, fd)
   local parseGlyphAssembly = function (inner_offset, inner_fd)
      local assembly =
         vstruct.read(">@" .. inner_offset .. " italicsCorrection:{ &MathValueRecord } partCount:u2", inner_fd)
      assembly.italicsCorrection = fetchMathValueRecord(assembly.italicsCorrection, inner_offset, inner_fd)
      assembly.partRecords = vstruct.read("> " .. assembly.partCount .. "*{ &GlyphPartRecord }", inner_fd)
      assembly.partCount = nil
      return assembly
   end
   local parseMathGlyphConstruction = function (inner_offset, inner_fd)
      local construction = vstruct.read(">@" .. inner_offset .. " glyphAssemblyOffset:u2 variantCount:u2", inner_fd)
      local mathGlyphVariantRecord =
         vstruct.read("> " .. construction.variantCount .. "*{ &MathGlyphVariantRecord }", inner_fd)
      return {
         glyphAssembly = construction.glyphAssemblyOffset ~= 0
               and parseGlyphAssembly(inner_offset + construction.glyphAssemblyOffset, inner_fd)
            or nil,
         mathGlyphVariantRecord = mathGlyphVariantRecord,
      }
   end
   local variants = vstruct.read(
      ">@"
         .. offset
         .. " minConnectorOverlap:u2 vertGlyphCoverageOffset:u2 horizGlyphCoverageOffset:u2 vertGlyphCount:u2 horizGlyphCount:u2",
      fd
   )
   local vertGlyphConstructionOffsets = vstruct.read("> " .. variants.vertGlyphCount .. "*u2", fd)
   local horizGlyphConstructionOffsets = vstruct.read("> " .. variants.horizGlyphCount .. "*u2", fd)
   local vertGlyphCoverage = {}
   if variants.vertGlyphCoverageOffset > 0 then
      vertGlyphCoverage = parseCoverage(offset + variants.vertGlyphCoverageOffset, fd)
   end
   local horizGlyphCoverage = {}
   if variants.horizGlyphCoverageOffset > 0 then
      horizGlyphCoverage = parseCoverage(offset + variants.horizGlyphCoverageOffset, fd)
   end
   if variants.vertGlyphCount ~= #vertGlyphCoverage or variants.horizGlyphCount ~= #horizGlyphCoverage then
      SU.error("MathVariants Table corrupted")
   end
   local vertGlyphConstructions = {}
   local horizGlyphConstructions = {}
   for i = 1, variants.vertGlyphCount do
      vertGlyphConstructions[vertGlyphCoverage[i]] =
         parseMathGlyphConstruction(offset + vertGlyphConstructionOffsets[i], fd)
   end
   for i = 1, variants.horizGlyphCount do
      horizGlyphConstructions[horizGlyphCoverage[i]] =
         parseMathGlyphConstruction(offset + horizGlyphConstructionOffsets[i], fd)
   end
   return {
      minConnectorOverlap = variants.minConnectorOverlap,
      vertGlyphConstructions = vertGlyphConstructions,
      horizGlyphConstructions = horizGlyphConstructions,
   }
end

local parseIfPresent = function (baseOffset, subtableOffset, f)
   if subtableOffset == 0 then
      return nil
   else
      return f(baseOffset + subtableOffset)
   end
end

local function parseMath (s)
   if s:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(s)
   local header = vstruct.read(
      ">majorVersion:u2 minorVersion:u2 mathConstantsOffset:u2 mathGlyphInfoOffset:u2 mathVariantsOffset:u2",
      fd
   )
   SU.debug("opentype-parser", "header =", header)
   if header.majorVersion > 1 then
      return
   end
   vstruct.compile("MathValueRecord", "value:i2 deviceTableOffset:u2")
   vstruct.compile("RangeRecord", "startGlyphID:u2 endGlyphID:u2 startCoverageIndex:u2")
   vstruct.compile(
      "MathKernInfoRecord",
      "topRightMathKernOffset:u2 topLeftMathKernOffset:u2 bottomRightMathKernOffset:u2 bottomLeftMathKernOffset:u2"
   )
   vstruct.compile("MathGlyphVariantRecord", "variantGlyph:u2 advanceMeasurement:u2")
   vstruct.compile(
      "GlyphPartRecord",
      "glyphID:u2 startConnectorLength:u2 endConnectorLength:u2 fullAdvance:u2 partFlags:u2"
   )
   local mathConstants = parseConstants(header.mathConstantsOffset, fd)
   local mathGlyphInfo = vstruct.read(
      ">@"
         .. header.mathGlyphInfoOffset
         .. " mathItalicsCorrectionInfoOffset:u2"
         .. " mathTopAccentAttachmentOffset:u2"
         .. " extendedShapeCoverageOffset:u2"
         .. " mathKernInfoOffset:u2",
      fd
   )
   SU.debug("opentype-parser", "mathGlyphInfoOffset =", header.mathGlyphInfoOffset)
   SU.debug("opentype-parser", "mathGlyphInfo =", mathGlyphInfo)
   local mathItalicsCorrection = parseIfPresent(
      header.mathGlyphInfoOffset,
      mathGlyphInfo.mathItalicsCorrectionInfoOffset,
      function (offset)
         return parsePerGlyphTable(offset, "&MathValueRecord", fd)
      end
   )
   local mathTopAccentAttachment = parseIfPresent(
      header.mathGlyphInfoOffset,
      mathGlyphInfo.mathTopAccentAttachmentOffset,
      function (offset)
         return parsePerGlyphTable(offset, "&MathValueRecord", fd)
      end
   )
   local extendedShapeCoverage = parseIfPresent(
      header.mathGlyphInfoOffset,
      mathGlyphInfo.extendedShapeCoverageOffset,
      function (offset)
         return parseCoverage(offset, fd)
      end
   )
   local mathKernInfo = parseIfPresent(header.mathGlyphInfoOffset, mathGlyphInfo.mathKernInfoOffset, function (offset)
      return parsePerGlyphTable(offset, "&MathKernInfoRecord", fd)
   end)
   local mathVariants = parseMathVariants(header.mathVariantsOffset, fd)
   return {
      mathConstants = mathConstants,
      mathItalicsCorrection = mathItalicsCorrection,
      mathTopAccentAttachment = mathTopAccentAttachment,
      extendedShapeCoverage = extendedShapeCoverage,
      mathKernInfo = mathKernInfo,
      mathVariants = mathVariants,
   }
end

local function parsePost (s)
   if s:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(s)
   local header = vstruct.read(
      ">majorVersion:u2 minorVersion:u2 italicAngle:i4 underlinePosition:i2 underlineThickness:i2 isFixedPitch:u4",
      fd
   )
   local italicAngle = header.italicAngle / 65536 -- 1 << 16
   return {
      italicAngle = italicAngle,
      underlinePosition = header.underlinePosition,
      underlineThickness = header.underlineThickness,
   }
end

local function parseOs2 (s)
   if s:len() <= 0 then
      return
   end
   local fd = vstruct.cursor(s)
   local header = vstruct.read(
      ">version:u2 xAvgCharWidth:i2 usWeightClass:u2 usWidthClass:u2 fsType:u2 ySubscriptXSize:i2 ySubscriptYSize:i2 ySubscriptXOffset:i2 ySubscriptYOffset:i2 ySuperscriptXSize:i2 ySuperscriptYSize:i2 ySuperscriptXOffset:i2 ySuperscriptYOffset:i2, yStrikeoutSize:i2 yStrikeoutPosition:i2",
      fd
   )
   return {
      yStrikeoutPosition = header.yStrikeoutPosition,
      yStrikeoutSize = header.yStrikeoutSize,
   }
end

local parseFont = function (face)
   if not face.font then
      local font = {}
      font.head = parseHead(hb.get_table(face, "head"))
      font.names = parseName(hb.get_table(face, "name"))
      font.maxp = parseMaxp(hb.get_table(face, "maxp"))
      font.colr = parseColr(hb.get_table(face, "COLR"))
      font.cpal = parseCpal(hb.get_table(face, "CPAL"))
      font.svg = parseSvg(hb.get_table(face, "SVG"))
      font.math = parseMath(hb.get_table(face, "MATH"))
      font.post = parsePost(hb.get_table(face, "post"))
      font.os2 = parseOs2(hb.get_table(face, "OS/2"))
      face.font = font
   end
   return face.font
end

local decompress = function (str)
   local decompressed = {}
   while true do
      local chunk, eof = zlib.inflate(str)
      decompressed[#decompressed + 1] = chunk
      if eof then
         break
      end
   end
   return table.concat(decompressed, "")
end

local getSVG = function (face, gid)
   if not face.font then
      parseFont(face)
   end
   if not face.font.svg then
      return
   end
   local item = face.font.svg[gid]
   if not item then
      return
   end
   local str = hb.get_table(face, "SVG")
   local start = item.svgDocOffset + 1
   local svg = str:sub(start, start + item.svgDocLength - 1)
   if svg[1] == "\x1f" and svg[2] == "\x8b" then
      svg = decompress(svg)
   end
   return svg
end

return { parseHead = parseHead, parseMath = parseMath, parseFont = parseFont, getSVG = getSVG }
