if not modules then modules = { } end modules ['font-otp'] = {
    version   = 1.001,
    comment   = "companion to font-otf.lua (packing)",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- todo: pack math (but not that much to share)
--
-- pitfall 5.2: hashed tables can suddenly become indexed with nil slots
--
-- unless we sort all hashes we can get a different pack order (no big deal but size can differ)

local next, type = next, type
local sort, concat = table.sort, table.concat
local sortedhash = table.sortedhash

local trace_packing = false  trackers.register("otf.packing", function(v) trace_packing = v end)
local trace_loading = false  trackers.register("otf.loading", function(v) trace_loading = v end)

local report_otf    = logs.reporter("fonts","otf loading")

-- also used in other scripts so we need to check some tables:

fonts               = fonts or { }

local handlers      = fonts.handlers or { }
fonts.handlers      = handlers

local otf           = handlers.otf or { }
handlers.otf        = otf

local enhancers     = otf.enhancers or { }
otf.enhancers       = enhancers

local glists        = otf.glists or { "gsub", "gpos" }
otf.glists          = glists

local criterium     = 1
local threshold     = 0

local function tabstr_normal(t)
    local s = { }
    local n = 0
    for k, v in next, t do
        n = n + 1
        if type(v) == "table" then
            s[n] = k .. ">" .. tabstr_normal(v)
        elseif v == true then
            s[n] = k .. "+" -- "=true"
        elseif v then
            s[n] = k .. "=" .. v
        else
            s[n] = k .. "-" -- "=false"
        end
    end
    if n == 0 then
        return ""
    elseif n == 1 then
        return s[1]
    else
        sort(s) -- costly but needed (occasional wrong hit otherwise)
        return concat(s,",")
    end
end

local function tabstr_flat(t)
    local s = { }
    local n = 0
    for k, v in next, t do
        n = n + 1
        s[n] = k .. "=" .. v
    end
    if n == 0 then
        return ""
    elseif n == 1 then
        return s[1]
    else
        sort(s) -- costly but needed (occasional wrong hit otherwise)
        return concat(s,",")
    end
end

local function tabstr_mixed(t) -- indexed
    local s = { }
    local n = #t
    if n == 0 then
        return ""
    elseif n == 1 then
        local k = t[1]
        if k == true then
            return "++" -- we need to distinguish from "true"
        elseif k == false then
            return "--" -- we need to distinguish from "false"
        else
            return tostring(k) -- number or string
        end
    else
        for i=1,n do
            local k = t[i]
            if k == true then
                s[i] = "++" -- we need to distinguish from "true"
            elseif k == false then
                s[i] = "--" -- we need to distinguish from "false"
            else
                s[i] = k -- number or string
            end
        end
        return concat(s,",")
    end
end

local function tabstr_boolean(t)
    local s = { }
    local n = 0
    for k, v in next, t do
        n = n + 1
        if v then
            s[n] = k .. "+"
        else
            s[n] = k .. "-"
        end
    end
    if n == 0 then
        return ""
    elseif n == 1 then
        return s[1]
    else
        sort(s) -- costly but needed (occasional wrong hit otherwise)
        return concat(s,",")
    end
end

-- tabstr_boolean_x = tabstr_boolean

-- tabstr_boolean = function(t)
--     local a = tabstr_normal(t)
--     local b = tabstr_boolean_x(t)
--     print(a)
--     print(b)
--     return b
-- end

-- beware: we cannot unpack and repack the same table because then sharing
-- interferes (we could catch this if needed) .. so for now: save, reload
-- and repack in such cases (never needed anyway) .. a tricky aspect is that
-- we then need to sort more thanks to random hashing

local function packdata(data)
    if data then
     -- stripdata(data)
        local h, t, c = { }, { }, { }
        local hh, tt, cc = { }, { }, { }
        local nt, ntt = 0, 0
        local function pack_normal(v)
            local tag = tabstr_normal(v)
            local ht = h[tag]
            if ht then
                c[ht] = c[ht] + 1
                return ht
            else
                nt = nt + 1
                t[nt] = v
                h[tag] = nt
                c[nt] = 1
                return nt
            end
        end
        local function pack_flat(v)
            local tag = tabstr_flat(v)
            local ht = h[tag]
            if ht then
                c[ht] = c[ht] + 1
                return ht
            else
                nt = nt + 1
                t[nt] = v
                h[tag] = nt
                c[nt] = 1
                return nt
            end
        end
        local function pack_boolean(v)
            local tag = tabstr_boolean(v)
            local ht = h[tag]
            if ht then
                c[ht] = c[ht] + 1
                return ht
            else
                nt = nt + 1
                t[nt] = v
                h[tag] = nt
                c[nt] = 1
                return nt
            end
        end
        local function pack_indexed(v)
            local tag = concat(v," ")
            local ht = h[tag]
            if ht then
                c[ht] = c[ht] + 1
                return ht
            else
                nt = nt + 1
                t[nt] = v
                h[tag] = nt
                c[nt] = 1
                return nt
            end
        end
        local function pack_mixed(v)
            local tag = tabstr_mixed(v)
            local ht = h[tag]
            if ht then
                c[ht] = c[ht] + 1
                return ht
            else
                nt = nt + 1
                t[nt] = v
                h[tag] = nt
                c[nt] = 1
                return nt
            end
        end
        local function pack_final(v)
            -- v == number
            if c[v] <= criterium then
                return t[v]
            else
                -- compact hash
                local hv = hh[v]
                if hv then
                    return hv
                else
                    ntt = ntt + 1
                    tt[ntt] = t[v]
                    hh[v] = ntt
                    cc[ntt] = c[v]
                    return ntt
                end
            end
        end
        local function success(stage,pass)
            if nt == 0 then
                if trace_loading or trace_packing then
                    report_otf("pack quality: nothing to pack")
                end
                return false
            elseif nt >= threshold then
                local one, two, rest = 0, 0, 0
                if pass == 1 then
                    for k,v in next, c do
                        if v == 1 then
                            one = one + 1
                        elseif v == 2 then
                            two = two + 1
                        else
                            rest = rest + 1
                        end
                    end
                else
                    for k,v in next, cc do
                        if v > 20 then
                            rest = rest + 1
                        elseif v > 10 then
                            two = two + 1
                        else
                            one = one + 1
                        end
                    end
                    data.tables = tt
                end
                if trace_loading or trace_packing then
                    report_otf("pack quality: stage %s, pass %s, %s packed, 1-10:%s, 11-20:%s, rest:%s (criterium: %s)", stage, pass, one+two+rest, one, two, rest, criterium)
                end
                return true
            else
                if trace_loading or trace_packing then
                    report_otf("pack quality: stage %s, pass %s, %s packed, aborting pack (threshold: %s)", stage, pass, nt, threshold)
                end
                return false
            end
        end
        local function packers(pass)
            if pass == 1 then
                return pack_normal, pack_indexed, pack_flat, pack_boolean, pack_mixed
            else
                return pack_final, pack_final, pack_final, pack_final, pack_final
            end
        end
        local resources = data.resources
        local lookuptypes = resources.lookuptypes
        for pass=1,2 do
            if trace_packing then
                report_otf("start packing: stage 1, pass %s",pass)
            end
            local pack_normal, pack_indexed, pack_flat, pack_boolean, pack_mixed = packers(pass)
            for unicode, description in next, data.descriptions do
                local boundingbox = description.boundingbox
                if boundingbox then
                    description.boundingbox = pack_indexed(boundingbox)
                end
                local slookups = description.slookups
                if slookups then
                    for tag, slookup in next, slookups do
                        local what = lookuptypes[tag]
                        if what == "pair" then
                            local t = slookup[2] if t then slookup[2] = pack_indexed(t) end
                            local t = slookup[3] if t then slookup[3] = pack_indexed(t) end
                        elseif what ~= "substitution" then
                            slookups[tag] = pack_indexed(slookup) -- true is new
                        end
                    end
                end
                local mlookups = description.mlookups
                if mlookups then
                    for tag, mlookup in next, mlookups do
                        local what = lookuptypes[tag]
                        if what == "pair" then
                            for i=1,#mlookup do
                                local lookup = mlookup[i]
                                local t = lookup[2] if t then lookup[2] = pack_indexed(t) end
                                local t = lookup[3] if t then lookup[3] = pack_indexed(t) end
                            end
                        elseif what ~= "substitution" then
                            for i=1,#mlookup do
                                mlookup[i] = pack_indexed(mlookup[i]) -- true is new
                            end
                        end
                    end
                end
                local kerns = description.kerns
                if kerns then
                    for tag, kern in next, kerns do
                        kerns[tag] = pack_flat(kern)
                    end
                end
                local math = description.math
                if math then
                    local kerns = math.kerns
                    if kerns then
                        for tag, kern in next, kerns do
                            kerns[tag] = pack_normal(kern)
                        end
                    end
                end
                local anchors = description.anchors
                if anchors then
                    for what, anchor in next, anchors do
                        if what == "baselig" then
                            for _, a in next, anchor do
                                for k=1,#a do
                                    a[k] = pack_indexed(a[k])
                                end
                            end
                        else
                            for k, v in next, anchor do
                                anchor[k] = pack_indexed(v)
                            end
                        end
                    end
                end
                local altuni = description.altuni
                if altuni then
                    for i=1,#altuni do
                        altuni[i] = pack_flat(altuni[i])
                    end
                end
            end
            local lookups = data.lookups
            if lookups then
                for _, lookup in next, lookups do
                    local rules = lookup.rules
                    if rules then
                        for i=1,#rules do
                            local rule = rules[i]
                            local r = rule.before       if r then for i=1,#r do r[i] = pack_boolean(r[i]) end end
                            local r = rule.after        if r then for i=1,#r do r[i] = pack_boolean(r[i]) end end
                            local r = rule.current      if r then for i=1,#r do r[i] = pack_boolean(r[i]) end end
                            local r = rule.replacements if r then rule.replacements  = pack_flat   (r)    end -- can have holes
                            local r = rule.lookups      if r then rule.lookups       = pack_indexed(r)    end -- can have ""
                         -- local r = rule.lookups      if r then rule.lookups       = pack_flat(r)       end -- can have holes (already taken care of some cases)
                        end
                    end
                end
            end
            local anchor_to_lookup  = resources.anchor_to_lookup
            if anchor_to_lookup then
                for anchor, lookup in next, anchor_to_lookup do
                    anchor_to_lookup[anchor] = pack_normal(lookup)
                end
            end
            local lookup_to_anchor = resources.lookup_to_anchor
            if lookup_to_anchor then
                for lookup, anchor in next, lookup_to_anchor do
                    lookup_to_anchor[lookup] = pack_normal(anchor)
                end
            end
            local sequences = resources.sequences
            if sequences then
                for feature, sequence in next, sequences do
                    local flags = sequence.flags
                    if flags then
                        sequence.flags = pack_normal(flags)
                    end
                    local subtables = sequence.subtables
                    if subtables then
                        sequence.subtables = pack_normal(subtables)
                    end
                    local features = sequence.features
                    if features then
                        for script, feature in next, features do
                            features[script] = pack_normal(feature)
                        end
                    end
                    local order = sequence.order
                    if order then
                        sequence.order = pack_indexed(order)
                    end
                    local markclass = sequence.markclass
                    if markclass then
                        sequence.markclass = pack_boolean(markclass)
                    end
                end
            end
            local lookups = resources.lookups
            if lookups then
                for name, lookup in next, lookups do
                    local flags = lookup.flags
                    if flags then
                        lookup.flags = pack_normal(flags)
                    end
                    local subtables = lookup.subtables
                    if subtables then
                        lookup.subtables = pack_normal(subtables)
                    end
                end
            end
            local features = resources.features
            if features then
                for _, what in next, glists do
                    local list = features[what]
                    if list then
                        for feature, spec in next, list do
                            list[feature] = pack_normal(spec)
                        end
                    end
                end
            end
            if not success(1,pass) then
                return
            end
        end
        if nt > 0 then
            for pass=1,2 do
                if trace_packing then
                    report_otf("start packing: stage 2, pass %s",pass)
                end
                local pack_normal, pack_indexed, pack_flat, pack_boolean, pack_mixed = packers(pass)
                for unicode, description in next, data.descriptions do
                    local kerns = description.kerns
                    if kerns then
                        description.kerns = pack_normal(kerns)
                    end
                    local math = description.math
                    if math then
                        local kerns = math.kerns
                        if kerns then
                            math.kerns = pack_normal(kerns)
                        end
                    end
                    local anchors = description.anchors
                    if anchors then
                        description.anchors = pack_normal(anchors)
                    end
                    local mlookups = description.mlookups
                    if mlookups then
                        for tag, mlookup in next, mlookups do
                            mlookups[tag] = pack_normal(mlookup)
                        end
                    end
                    local altuni = description.altuni
                    if altuni then
                        description.altuni = pack_normal(altuni)
                    end
                end
                local lookups = data.lookups
                if lookups then
                    for _, lookup in next, lookups do
                        local rules = lookup.rules
                        if rules then
                            for i=1,#rules do -- was next loop
                                local rule = rules[i]
                                local r = rule.before  if r then rule.before  = pack_normal(r) end
                                local r = rule.after   if r then rule.after   = pack_normal(r) end
                                local r = rule.current if r then rule.current = pack_normal(r) end
                            end
                        end
                    end
                end
                local sequences = resources.sequences
                if sequences then
                    for feature, sequence in next, sequences do
                        sequence.features = pack_normal(sequence.features)
                    end
                end
                if not success(2,pass) then
                 -- return
                end
            end

            for pass=1,2 do
                local pack_normal, pack_indexed, pack_flat, pack_boolean, pack_mixed = packers(pass)
                for unicode, description in next, data.descriptions do
                    local slookups = description.slookups
                    if slookups then
                        description.slookups = pack_normal(slookups)
                    end
                    local mlookups = description.mlookups
                    if mlookups then
                        description.mlookups = pack_normal(mlookups)
                    end
                end
            end

        end
    end
end

local unpacked_mt = {
    __index =
        function(t,k)
            t[k] = false
            return k -- next time true
        end
}

local function unpackdata(data)
    if data then
        local tables = data.tables
        if tables then
            local resources = data.resources
            local lookuptypes = resources.lookuptypes
            local unpacked = { }
            setmetatable(unpacked,unpacked_mt)
            for unicode, description in next, data.descriptions do
                local tv = tables[description.boundingbox]
                if tv then
                    description.boundingbox = tv
                end
                local slookups = description.slookups
                if slookups then
                    local tv = tables[slookups]
                    if tv then
                        description.slookups = tv
                        slookups = unpacked[tv]
                    end
                    if slookups then
                        for tag, lookup in next, slookups do
                            local what = lookuptypes[tag]
                            if what == "pair" then
                                local tv = tables[lookup[2]]
                                if tv then
                                    lookup[2] = tv
                                end
                                local tv = tables[lookup[3]]
                                if tv then
                                    lookup[3] = tv
                                end
                            elseif what ~= "substitution" then
                                local tv = tables[lookup]
                                if tv then
                                    slookups[tag] = tv
                                end
                            end
                        end
                    end
                end
                local mlookups = description.mlookups
                if mlookups then
                    local tv = tables[mlookups]
                    if tv then
                        description.mlookups = tv
                        mlookups = unpacked[tv]
                    end
                    if mlookups then
                        for tag, list in next, mlookups do
                            local tv = tables[list]
                            if tv then
                                mlookups[tag] = tv
                                list = unpacked[tv]
                            end
                            if list then
                                local what = lookuptypes[tag]
                                if what == "pair" then
                                    for i=1,#list do
                                        local lookup = list[i]
                                        local tv = tables[lookup[2]]
                                        if tv then
                                            lookup[2] = tv
                                        end
                                        local tv = tables[lookup[3]]
                                        if tv then
                                            lookup[3] = tv
                                        end
                                    end
                                elseif what ~= "substitution" then
                                    for i=1,#list do
                                        local tv = tables[list[i]]
                                        if tv then
                                            list[i] = tv
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
                local kerns = description.kerns
                if kerns then
                    local tm = tables[kerns]
                    if tm then
                        description.kerns = tm
                        kerns = unpacked[tm]
                    end
                    if kerns then
                        for k, kern in next, kerns do
                            local tv = tables[kern]
                            if tv then
                                kerns[k] = tv
                            end
                        end
                    end
                end
                local math = description.math
                if math then
                    local kerns = math.kerns
                    if kerns then
                        local tm = tables[kerns]
                        if tm then
                            math.kerns = tm
                            kerns = unpacked[tm]
                        end
                        if kerns then
                            for k, kern in next, kerns do
                                local tv = tables[kern]
                                if tv then
                                    kerns[k] = tv
                                end
                            end
                        end
                    end
                end
                local anchors = description.anchors
                if anchors then
                    local ta = tables[anchors]
                    if ta then
                        description.anchors = ta
                        anchors = unpacked[ta]
                    end
                    if anchors then
                        for tag, anchor in next, anchors do
                            if tag == "baselig" then
                                for _, list in next, anchor do
                                    for i=1,#list do
                                        local tv = tables[list[i]]
                                        if tv then
                                            list[i] = tv
                                        end
                                    end
                                end
                            else
                                for a, data in next, anchor do
                                    local tv = tables[data]
                                    if tv then
                                        anchor[a] = tv
                                    end
                                end
                            end
                        end
                    end
                end
                local altuni = description.altuni
                if altuni then
                    local altuni = tables[altuni]
                    if altuni then
                        description.altuni = altuni
                        for i=1,#altuni do
                            local tv = tables[altuni[i]]
                            if tv then
                                altuni[i] = tv
                            end
                        end
                    end
                end
            end
            local lookups = data.lookups
            if lookups then
                for _, lookup in next, lookups do
                    local rules = lookup.rules
                    if rules then
                        for i=1,#rules do -- was next loop
                            local rule = rules[i]
                            local before = rule.before
                            if before then
                                local tv = tables[before]
                                if tv then
                                    rule.before = tv
                                    before = unpacked[tv]
                                end
                                if before then
                                    for i=1,#before do
                                        local tv = tables[before[i]]
                                        if tv then
                                            before[i] = tv
                                        end
                                    end
                                end
                            end
                            local after = rule.after
                            if after then
                                local tv = tables[after]
                                if tv then
                                    rule.after = tv
                                    after = unpacked[tv]
                                end
                                if after then
                                    for i=1,#after do
                                        local tv = tables[after[i]]
                                        if tv then
                                            after[i] = tv
                                        end
                                    end
                                end
                            end
                            local current = rule.current
                            if current then
                                local tv = tables[current]
                                if tv then
                                    rule.current = tv
                                    current = unpacked[tv]
                                end
                                if current then
                                    for i=1,#current do
                                        local tv = tables[current[i]]
                                        if tv then
                                            current[i] = tv
                                        end
                                    end
                                end
                            end
                            local replacements = rule.replacements
                            if replacements then
                                local tv = tables[replacements]
                                if tv then
                                    rule.replacements = tv
                                end
                            end
                         -- local fore = rule.fore
                         -- if fore then
                         --     local tv = tables[fore]
                         --     if tv then
                         --         rule.fore = tv
                         --     end
                         -- end
                         -- local back = rule.back
                         -- if back then
                         --     local tv = tables[back]
                         --     if tv then
                         --         rule.back = tv
                         --     end
                         -- end
                         -- local names = rule.names
                         -- if names then
                         --     local tv = tables[names]
                         --     if tv then
                         --         rule.names = tv
                         --     end
                         -- end
                            --
                            local lookups = rule.lookups
                            if lookups then
                                local tv = tables[lookups]
                                if tv then
                                    rule.lookups = tv
                                end
                            end
                        end
                    end
                end
            end
            local anchor_to_lookup = resources.anchor_to_lookup
            if anchor_to_lookup then
                for anchor, lookup in next, anchor_to_lookup do
                    local tv = tables[lookup]
                    if tv then
                        anchor_to_lookup[anchor] = tv
                    end
                end
            end
            local lookup_to_anchor = resources.lookup_to_anchor
            if lookup_to_anchor then
                for lookup, anchor in next, lookup_to_anchor do
                    local tv = tables[anchor]
                    if tv then
                        lookup_to_anchor[lookup] = tv
                    end
                end
            end
            local ls = resources.sequences
            if ls then
                for _, feature in next, ls do
                    local flags = feature.flags
                    if flags then
                        local tv = tables[flags]
                        if tv then
                            feature.flags = tv
                        end
                    end
                    local subtables = feature.subtables
                    if subtables then
                        local tv = tables[subtables]
                        if tv then
                            feature.subtables = tv
                        end
                    end
                    local features = feature.features
                    if features then
                        local tv = tables[features]
                        if tv then
                            feature.features = tv
                            features = unpacked[tv]
                        end
                        if features then
                            for script, data in next, features do
                                local tv = tables[data]
                                if tv then
                                    features[script] = tv
                                end
                            end
                        end
                    end
                    local order = feature.order
                    if order then
                        local tv = tables[order]
                        if tv then
                            feature.order = tv
                        end
                    end
                    local markclass = feature.markclass
                    if markclass then
                        local tv = tables[markclass]
                        if tv then
                            feature.markclass = tv
                        end
                    end
                end
            end
            local lookups = resources.lookups
            if lookups then
                for _, lookup in next, lookups do
                    local flags = lookup.flags
                    if flags then
                        local tv = tables[flags]
                        if tv then
                            lookup.flags = tv
                        end
                    end
                    local subtables = lookup.subtables
                    if subtables then
                        local tv = tables[subtables]
                        if tv then
                            lookup.subtables = tv
                        end
                    end
                end
            end
            local features = resources.features
            if features then
                for _, what in next, glists do
                    local feature = features[what]
                    if feature then
                        for tag, spec in next, feature do
                            local tv = tables[spec]
                            if tv then
                                feature[tag] = tv
                            end
                        end
                    end
                end
            end
            data.tables = nil
        end
    end
end

if otf.enhancers.register then

    otf.enhancers.register(  "pack",  packdata)
    otf.enhancers.register("unpack",unpackdata)

-- todo: directive

end

otf.enhancers.unpack = unpackdata -- used elsewhere
otf.enhancers.pack   = packdata   -- used elsewhere
