local pack = string.pack
local strings = require'luametalatex-font-cff-data'

local function getstring(cff, str)
  local i = strings[str] or cff.strings[str]
  if not i then
    i = #strings + #cff.strings + 1
    cff.strings[str] = i
    cff.strings[i - #strings] = str
  end
  return i
end
local function serialize_index(index, element_serializer)
  local sizes = {1}
  local length = 1
  local data = {}
  for i=1,#index do
    data[i] = element_serializer(index[i])
    length = length + #data[i]
    sizes[#sizes+1] = length
  end
  data = table.concat(data)
  if data == "" then return "\0\0" end
  local offSize = length < 2^8 and 1 or length < 2^16 and 2 or length < 2^24 and 3 or 4
  local offsetfmt = string.format(">I%i", offSize)
  local offsets = ""
  for i = #sizes,1,-1 do
    sizes[i+1] = pack(offsetfmt, sizes[i])
  end
  sizes[1] = pack(">I2B", #index, offSize)
  sizes[#sizes+1] = data
  return table.concat(sizes)
end
local function ident(...)
  return ...
end
local real_lookup = {
  ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
  ['.'] = 0xa, ['-'] = 0xe
}
local function dictInt(n)
  local num = math.floor(n)
  if num ~= n then
    num = tostring(n)
    local i, result, tmp = 1, string.char(0x1e)
    while i <= #num do
      local c = real_lookup[num:sub(i, i)]
      if not c then -- We got an 'e'
        c = num:sub(i+1, i+1) == '+' and 0xb or 0xc
        repeat
          i = i + 1
        until num:sub(i+1, i+1) ~= '0'
      end
      if tmp then
        result = result .. string.char(tmp * 16 + c)
        tmp = nil
      else
        tmp = c
      end
      i = i + 1
    end
    return result .. string.char((tmp or 0xf) * 16 + 0xf)
  elseif num >= -107 and num <= 107 then
    return string.char(num + 139)
  elseif num >= 108 and num <= 1131 then
    num = num - 108
    return string.char(247 + ((num >> 8) & 0xFF), num & 0xFF)
  elseif num >= -1131 and num <= -108 then
    num = -num - 108
    return string.char(251 + ((num >> 8) & 0xFF), num & 0xFF)
  elseif num >= -32768 and num <= 32767 then
    return string.char(28, (num >> 8) & 0xFF, num & 0xFF)
  else
    return string.char(29, (num >> 24) & 0xFF, (num >> 16) & 0xFF,
                           (num >> 8) & 0xFF, num & 0xFF)
  end
end
local function serialize_top(cff)
  local data = dictInt(getstring(cff, cff.registry or 'Adobe'))
            .. dictInt(getstring(cff, cff.ordering or 'Identity'))
            .. dictInt(               cff.supplement or 0)
            .. string.char(12, 30)
  if cff.version then
    data = data .. dictInt(getstring(cff, cff.version)) .. string.char(0)
  end
  if cff.Notice then
    data = data .. dictInt(getstring(cff, cff.Notice)) .. string.char(1)
  end
  if cff.FullName then
    data = data .. dictInt(getstring(cff, cff.FullName)) .. string.char(2)
  end
  if cff.FamilyName then
    data = data .. dictInt(getstring(cff, cff.FamilyName)) .. string.char(3)
  end
  if cff.Weight then
    data = data .. dictInt(getstring(cff, cff.Weight)) .. string.char(4)
  end
  if cff.isFixedPitch then
    data = data .. dictInt(1) .. string.char(12, 1)
  end
  if cff.ItalicAngle and cff.ItalicAngle ~= 0 then
    data = data .. dictInt(cff.ItalicAngle) .. string.char(12, 2)
  end
  if cff.UnderlinePosition then
    data = data .. dictInt(cff.UnderlinePosition) .. string.char(12, 3)
  end
  if cff.UnderlineThickness then
    data = data .. dictInt(cff.UnderlineThickness) .. string.char(12, 4)
  end
  if cff.FontMatrix then
    data = data .. dictInt(cff.FontMatrix[1]) .. dictInt(cff.FontMatrix[2])
                .. dictInt(cff.FontMatrix[3]) .. dictInt(cff.FontMatrix[4])
                .. dictInt(cff.FontMatrix[5]) .. dictInt(cff.FontMatrix[6])
                .. string.char(12, 7)
  end
  if cff.FontBBox then
    data = data .. dictInt(cff.FontBBox[1]) .. dictInt(cff.FontBBox[2])
                .. dictInt(cff.FontBBox[3]) .. dictInt(cff.FontBBox[4])
                .. string.char(5)
  end
  if cff.PostScript then
    data = data .. dictInt(getstring(cff, cff.PostScript)) .. string.char(12, 21)
  end
  data = data .. dictInt(cff.charset_offset) .. string.char(15)
  data = data .. dictInt(cff.charstrings_offset) .. string.char(17)
  data = data .. dictInt(cff.fdarray_offset) .. string.char(12, 36)
  data = data .. dictInt(cff.fdselect_offset) .. string.char(12, 37)
  -- data = data .. dictInt(cff.private_size) .. dictInt(cff.private_offset) .. string.char(18)
  return data
end
local function serialize_font(cff, offset0) return function(private)
    local data = dictInt(private[3]) .. string.char(12, 38)
    data = data .. dictInt(private[2]) .. dictInt(offset0 + private[1]) .. string.char(18)
    return data
end end
local function va_minone(...)
  if select('#', ...) ~= 0 then
    return (...+1), select(2, ...)
  end
end
-- local function serialize_fdselect(cff)
--   local fdselect = cff.FDSelect or {format=3, {0,1}}
--   if not fdselect then
--     return '\3\0\1\0\0\0' .. string.pack('>I2', #cff.glyphs)
--   end
--   if fdselect.format == 0 then
--     return string.char(0, va_minone(table.unpack(fdselect)))
--   elseif fdselect.format == 3 then
--     local fdparts = {string.pack(">BI2", 3, #fdselect)}
--     for i=1,#fdselect do
--       fdparts[i+1] = string.pack(">I2B", fdselect[i][1], fdselect[i][2]-1)
--     end
--     fdparts[#fdselect+2] = string.pack(">I2", #cff.glyphs)
--     return table.concat(fdparts)
--   else
--     error[[Confusion]]
--   end
-- end
local function serialize_fdselect(cff)
  local fdselect = {""}
  local lastfont = -1
  for i, g in ipairs(cff.glyphs) do
    local font = g.cidfont or 1
    if font ~= lastfont then
      fdselect[#fdselect+1] = string.pack(">I2B", i-1, font-1)
      lastfont = font
    end
  end
  if #fdselect*3+2 > #cff.glyphs+1 then
    fdselect[1] = string.pack("B", 0)
    for i, g in ipairs(cff.glyphs) do
      local font = g.cidfont or 1
      fdselect[i+1] = string.pack("B", font-1)
    end
  else
    fdselect[1] = string.pack(">BI2", 3, #fdselect-1)
    fdselect[#fdselect+1] = string.pack(">I2", #cff.glyphs)
  end
  return table.concat(fdselect)
end
local function serialize_private(private, subrsoffset)
  local data = ""
  if not private.BlueValues then
    private.BlueValues = { }
  end
  local last = 0
  for _, v in ipairs(private.BlueValues) do
    data = data .. dictInt(v - last)
    last = v
  end
  data = data .. '\6'
  if private.OtherBlues and #private.OtherBlues ~= 0 then
    last = 0
    for _, v in ipairs(private.OtherBlues) do
      data = data .. dictInt(v - last)
      last = v
    end
    data = data .. '\7'
  end
  if private.BlueScale then
    data = data .. dictInt(private.BlueScale) .. '\12\9'
  end
  if private.BlueShift then
    data = data .. dictInt(private.BlueShift) .. '\12\10'
  end
  if private.BlueFuzz then
    data = data .. dictInt(private.BlueFuzz) .. '\12\11'
  end
  if private.ForceBold then
    data = data .. dictInt(1) .. '\12\14'
  end
  if private.StdHW then
    data = data .. dictInt(private.StdHW) .. '\10'
  end
  if private.StdVW then
    data = data .. dictInt(private.StdVW) .. '\11'
  end
  if private.StemSnapH and #private.StemSnapH ~= 0 then
    last = 0
    for _, v in ipairs(private.StemSnapH) do
      data = data .. dictInt(v - last)
      last = v
    end
    data = data .. '\12\12'
  end
  if private.StemSnapV and #private.StemSnapV ~= 0 then
    last = 0
    for _, v in ipairs(private.StemSnapV) do
      data = data .. dictInt(v - last)
      last = v
    end
    data = data .. '\12\13'
  end
  if subrsoffset and subrsoffset ~= 0 then
    data = data .. dictInt(subrsoffset) .. '\19'
  end
  if private.defaultWidthX then
    data = data .. dictInt(private.defaultWidthX) .. '\20'
  end
  if private.nominalWidthX then
    data = data .. dictInt(private.nominalWidthX) .. '\21'
  end
  return data
end
local function serialize_charset(cff)
  local data = string.char(2)
  local last, count = -42, -1
  for _, glyph in ipairs(cff.glyphs) do
    if not glyph.cid then glyph.cid = glyph.index end
    if glyph.cid ~= 0 then
      if glyph.cid ~= last + 1 or count == 0xFFFF then
        if count >= 0 then
          data = data .. string.pack(">I2", count)
          count = -1
        end
        data = data .. string.pack(">I2", glyph.cid)
      end
      last = glyph.cid
      count = count + 1
    end
  end
  if count >= 0 then
    data = data .. string.pack(">I2", count)
    count = -1
  end
  return data
end
return function(cff)
  cff.strings = {}
  cff.private_offset = 0
  cff.charstrings_offset = 0
  cff.fdarray_offset = 0
  cff.fdselect_offset = 0
  cff.charset_offset = 0
  local top = serialize_index({serialize_top(cff)}, ident)
  local data = string.char(1, 0, 4, 2) -- Assuming 16Bit offsets (Where are they used?)
  local name = serialize_index({cff.FontName}, ident)
  local globalsubrs = serialize_index(cff.GlobalSubrs or {}, ident)
  local private_offsets, privates = {}, {}
  local privates_size = 0 -- These include the localsubrs sizes
  for i, p in ipairs(cff.Privates or {cff}) do
    local subrs = p.Subrs
    if not subrs or subrs and #subrs == 0 then
      subrs = ""
    else
      subrs = serialize_index(subrs, ident)
    end
    local serialized = ""
    if subrs ~= "" then
      local last_size = 0
      repeat
        last_size = #serialized
        serialized = serialize_private(p, last_size)
      until last_size == #serialized
    else
      serialized = serialize_private(p)
    end
        -- serialized = serialize_private(p, -#subrs)
    privates[i] = serialized .. subrs
    private_offsets[i] = {privates_size + 0*#subrs, #serialized, getstring(cff, p.FontName or (cff.FontName .. '-' .. i))}
    privates_size = privates_size + #subrs + #serialized
  end
  if cff.glyphs[1].index ~= 0 then
    table.insert(cff.glyphs, 1, {index = 0, cs = string.char(14), cidfont = cff.glyphs[1].cidfont})
  end
  local strings = serialize_index(cff.strings, ident)
  local charset = serialize_charset(cff)
  local fdselect = serialize_fdselect(cff)
  local charstrings = serialize_index(cff.glyphs, function(g) return g.cs end)
  local pre_private, top_size = #data + #name + #strings + #globalsubrs
  repeat
    top_size = #top
    cff.charstrings_offset = pre_private + top_size + privates_size
    cff.charset_offset = cff.charstrings_offset + #charstrings
    cff.fdselect_offset = cff.charset_offset + #charset
    cff.fdarray_offset = cff.fdselect_offset + #fdselect
    top = serialize_index({serialize_top(cff)}, ident)
  until top_size == #top
  local fdarray = serialize_index(private_offsets, serialize_font(cff, top_size + pre_private), ident)
  return data .. name .. top .. strings .. globalsubrs .. table.concat(privates) .. charstrings .. charset .. fdselect .. fdarray
end
