if not modules then modules = { } end modules ['typo-krn'] = {
    version   = 1.001,
    comment   = "companion to typo-krn.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- glue is still somewhat suboptimal

local next, type, tonumber = next, type, tonumber
local utfchar = utf.char

local nodes, node, fonts = nodes, node, fonts

local tasks              = nodes.tasks
local nuts               = nodes.nuts
local nodepool           = nuts.pool

local tonode             = nuts.tonode
local tonut              = nuts.tonut

local find_node_tail     = nuts.tail
local free_node          = nuts.free
local free_nodelist      = nuts.flush_list
local copy_node          = nuts.copy
local copy_nodelist      = nuts.copy_list
local insert_node_before = nuts.insert_before
local insert_node_after  = nuts.insert_after
local end_of_math        = nuts.end_of_math

local getfield           = nuts.getfield
local setfield           = nuts.setfield
local getnext            = nuts.getnext
local getprev            = nuts.getprev
local getid              = nuts.getid
local getattr            = nuts.getattr
local setattr            = nuts.setattr
local getfont            = nuts.getfont
local getsubtype         = nuts.getsubtype
local getchar            = nuts.getchar

local texsetattribute    = tex.setattribute
local unsetvalue         = attributes.unsetvalue

local new_gluespec       = nodepool.gluespec
local new_kern           = nodepool.kern
local new_glue           = nodepool.glue

local nodecodes          = nodes.nodecodes
local kerncodes          = nodes.kerncodes
local skipcodes          = nodes.skipcodes
local disccodes          = nodes.disccodes

local glyph_code         = nodecodes.glyph
local kern_code          = nodecodes.kern
local disc_code          = nodecodes.disc
local glue_code          = nodecodes.glue
local hlist_code         = nodecodes.hlist
local vlist_code         = nodecodes.vlist
local math_code          = nodecodes.math

local discretionary_code = disccodes.discretionary
local kerning_code       = kerncodes.kerning
local userkern_code      = kerncodes.userkern
local userskip_code      = skipcodes.userskip
local spaceskip_code     = skipcodes.spaceskip
local xspaceskip_code    = skipcodes.xspaceskip

local fonthashes         = fonts.hashes
local fontdata           = fonthashes.identifiers
local chardata           = fonthashes.characters
local quaddata           = fonthashes.quads
local markdata           = fonthashes.marks
local fontproperties     = fonthashes.properties
local fontdescriptions   = fonthashes.descriptions
local fontfeatures       = fonthashes.features

local tracers            = nodes.tracers
local setcolor           = tracers.colors.set
local resetcolor         = tracers.colors.reset

local v_max              = interfaces.variables.max
local v_auto             = interfaces.variables.auto

typesetters              = typesetters or { }
local typesetters        = typesetters

typesetters.kerns        = typesetters.kerns or { }
local kerns              = typesetters.kerns

local report             = logs.reporter("kerns")
local trace_ligatures    = false  trackers.register("typesetters.kerns.ligatures",function(v) trace_ligatures = v end)

kerns.mapping            = kerns.mapping or { }
kerns.factors            = kerns.factors or { }
local a_kerns            = attributes.private("kern")
local a_fontkern         = attributes.private('fontkern')

local contextsetups      = fonts.specifiers.contextsetups

storage.register("typesetters/kerns/mapping", kerns.mapping, "typesetters.kerns.mapping")
storage.register("typesetters/kerns/factors", kerns.factors, "typesetters.kerns.factors")

local mapping = kerns.mapping
local factors = kerns.factors

-- one must use liga=no and mode=base and kern=yes
-- use more helpers
-- make sure it runs after all others
-- there will be a width adaptor field in nodes so this will change
-- todo: interchar kerns / disc nodes / can be made faster
-- todo: use insert_before etc

local gluefactor = 4 -- assumes quad = .5 enspace

kerns.keepligature = false -- just for fun (todo: control setting with key/value)
kerns.keeptogether = false -- just for fun (todo: control setting with key/value)

-- red   : kept by dynamic feature
-- green : kept by static feature
-- blue  : keep by goodie

function kerns.keepligature(n) -- might become default
    local f = getfont(n)
    local a = getattr(n,0) or 0
    if trace_ligatures then
        local c = getchar(n)
        local d = fontdescriptions[f][c].name
        if a > 0 and contextsetups[a].keepligatures == v_auto then
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","dynamic","keepligatures")
            setcolor(n,"darkred")
            return true
        end
        local k = fontfeatures[f].keepligatures
        if k == v_auto then
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","static","keepligatures")
            setcolor(n,"darkgreen")
            return true
        end
        if not k then
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"split","static","keepligatures")
            resetcolor(n)
            return false
        end
        local k = fontproperties[f].keptligatures
        if not k then
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"split","no")
            resetcolor(n)
            return false
        end
        if k and k[c] then
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"kept","by")
            setcolor(n,"darkblue")
            return true
        else
            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"split","by")
            resetcolor(n)
            return false
        end
    else
        if a > 0 and contextsetups[a].keepligatures == v_auto then
            return true
        end
        local k = fontfeatures[f].keepligatures
        if k == v_auto then
            return true
        end
        if not k then
            return false
        end
        local k = fontproperties[f].keptligatures
        if not k then
            return false
        end
        if k and k[c] then
            return true
        end
    end
end

-- can be optimized .. the prev thing .. but hardly worth the effort

local function kern_injector(fillup,kern)
    if fillup then
        local g = new_glue(kern)
        local s = getfield(g,"spec")
        setfield(s,"stretch",kern)
        setfield(s,"stretch_order",1)
        return g
    else
        return new_kern(kern)
    end
end

local function spec_injector(fillup,width,stretch,shrink)
    if fillup then
        local s = new_gluespec(width,2*stretch,2*shrink)
        setfield(s,"stretch_order",1)
        return s
    else
        return new_gluespec(width,stretch,shrink)
    end
end

-- needs checking ... base mode / node mode -- also use insert_before/after etc

local function do_process(head,force) -- todo: glue so that we can fully stretch
    local start, done, lastfont = head, false, nil
    local keepligature = kerns.keepligature
    local keeptogether = kerns.keeptogether
    local fillup = false
    while start do
        -- faster to test for attr first
        local attr = force or getattr(start,a_kerns)
        if attr and attr > 0 then
            setattr(start,a_kerns,unsetvalue)
            local krn = mapping[attr]
            if krn == v_max then
                krn = .25
                fillup = true
            else
                fillup = false
            end
            if krn and krn ~= 0 then
                local id = getid(start)
                if id == glyph_code then -- we could use the subtype ligature
                    lastfont = getfont(start)
                    local c = getfield(start,"components")
                    if not c then
                        -- fine
                    elseif keepligature and keepligature(start) then
                        -- keep 'm
                    else
                        c = do_process(c,attr)
                        local s = start
                        local p, n = getprev(s), getnext(s)
                        local tail = find_node_tail(c)
                        if p then
                            setfield(p,"next",c)
                            setfield(c,"prev",p)
                        else
                            head = c
                        end
                        if n then
                            setfield(n,"prev",tail)
                        end
                        setfield(tail,"next",n)
                        start = c
                        setfield(s,"components",nil)
                        -- we now leak nodes !
                    --  free_node(s)
                        done = true
                    end
                    local prev = getprev(start)
                    if not prev then
                        -- skip
                    elseif markdata[lastfont][getchar(start)] then
                            -- skip
                    else
                        local pid = getid(prev)
                        if not pid then
                            -- nothing
                        elseif pid == kern_code then
                            if getsubtype(prev) == kerning_code or getattr(prev,a_fontkern) then
                                if keeptogether and getid(getprev(prev)) == glyph_code and keeptogether(getprev(prev),start) then -- we could also pass start
                                    -- keep 'm
                                else
                                    -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                                    setfield(prev,"subtype",userkern_code)
                                    setfield(prev,"kern",getfield(prev,"kern") + quaddata[lastfont]*krn) -- here
                                    done = true
                                end
                            end
                        elseif pid == glyph_code then
                            if getfont(prev) == lastfont then
                                local prevchar, lastchar = getchar(prev), getchar(start)
                                if keeptogether and keeptogether(prev,start) then
                                    -- keep 'm
                                else
                                    local kerns = chardata[lastfont][prevchar].kerns
                                    local kern = kerns and kerns[lastchar] or 0
                                    krn = kern + quaddata[lastfont]*krn -- here
                                    insert_node_before(head,start,kern_injector(fillup,krn))
                                    done = true
                                end
                            else
                                krn = quaddata[lastfont]*krn -- here
                                insert_node_before(head,start,kern_injector(fillup,krn))
                                done = true
                            end
                        elseif pid == disc_code then
                            -- a bit too complicated, we can best not copy and just calculate
                            -- but we could have multiple glyphs involved so ...
                            local disc = prev -- disc
                            local prv, nxt = getprev(disc), getnext(disc)
                            if getsubtype(disc) == discretionary_code then
                                -- maybe we should forget about this variant as there is no glue
                                -- possible
                                local pre, post, replace = getfield(disc,"pre"), getfield(disc,"post"), getfield(disc,"replace")
                                if pre and prv then -- must pair with getprev(start)
                                    local before = copy_node(prv)
                                    setfield(pre,"prev",before)
                                    setfield(before,"next",pre)
                                    setfield(before,"prev",nil)
                                    pre = do_process(before,attr)
                                    pre = getnext(pre)
                                    setfield(pre,"prev",nil)
                                    setfield(disc,"pre",pre)
                                    free_node(before)
                                end
                                if post and nxt then  -- must pair with start
                                    local after = copy_node(nxt)
                                    local tail = find_node_tail(post)
                                    setfield(tail,"next",after)
                                    setfield(after,"prev",tail)
                                    setfield(after,"next",nil)
                                    post = do_process(post,attr)
                                    setfield(tail,"next",nil)
                                    setfield(disc,"post",post)
                                    free_node(after)
                                end
                                if replace and prv and nxt then -- must pair with start and start.prev
                                    local before = copy_node(prv)
                                    local after = copy_node(nxt)
                                    local tail = find_node_tail(replace)
                                    setfield(replace,"prev",before)
                                    setfield(before,"next",replace)
                                    setfield(before,"prev",nil)
                                    setfield(tail,"next",after)
                                    setfield(after,"prev",tail)
                                    setfield(after,"next",nil)
                                    replace = do_process(before,attr)
                                    replace = getnext(replace)
                                    setfield(replace,"prev",nil)
                                    setfield(getfield(after,"prev"),"next",nil)
                                    setfield(disc,"replace",replace)
                                    free_node(after)
                                    free_node(before)
                                elseif prv and getid(prv) == glyph_code and getfont(prv) == lastfont then
                                    local prevchar, lastchar = getchar(prv), getchar(start)
                                    local kerns = chardata[lastfont][prevchar].kerns
                                    local kern = kerns and kerns[lastchar] or 0
                                    krn = kern + quaddata[lastfont]*krn -- here
                                    setfield(disc,"replace",kern_injector(false,krn)) -- only kerns permitted, no glue
                                else
                                    krn = quaddata[lastfont]*krn -- here
                                    setfield(disc,"replace",kern_injector(false,krn)) -- only kerns permitted, no glue
                                end
                            else
                                -- this one happens in most cases: automatic (-), explicit (\-), regular (patterns)
                                if prv and getid(prv) == glyph_code and getfont(prv) == lastfont then
                                    -- the normal case
                                    local prevchar, lastchar = getchar(prv), getchar(start)
                                    local kerns = chardata[lastfont][prevchar].kerns
                                    local kern = kerns and kerns[lastchar] or 0
                                    krn = kern + quaddata[lastfont]*krn
                                else
                                    krn = quaddata[lastfont]*krn
                                end
                                insert_node_before(head,start,kern_injector(fillup,krn))
                            end
                        end
                    end
                elseif id == glue_code then
                    local subtype = getsubtype(start)
                    if subtype == userskip_code or subtype == xspaceskip_code or subtype == spaceskip_code then
                        local s = getfield(start,"spec")
                        local w = getfield(s,"width")
                        if w > 0 then
                            local width, stretch, shrink = w+gluefactor*w*krn, getfield(s,"stretch"), getfield(s,"shrink")
                            setfield(start,"spec",spec_injector(fillup,width,stretch*width/w,shrink*width/w))
                            done = true
                        end
                    end
                elseif id == kern_code then
                 -- if getsubtype(start) == kerning_code then -- handle with glyphs
                 --     local sk = getfield(start,"kern")
                 --     if sk > 0 then
                 --         setfield(start,"kern",sk*krn)
                 --         done = true
                 --     end
                 -- end
                elseif lastfont and (id == hlist_code or id == vlist_code) then -- todo: lookahead
                    local p = getprev(start)
                    if p and getid(p) ~= glue_code then
                        insert_node_before(head,start,kern_injector(fillup,quaddata[lastfont]*krn))
                        done = true
                    end
                    local n = getnext(start)
                    if n and getid(n) ~= glue_code then
                        insert_node_after(head,start,kern_injector(fillup,quaddata[lastfont]*krn))
                        done = true
                    end
                elseif id == math_code then
                    start = end_of_math(start)
                end
            end
        end
        if start then
            start = getnext(start)
        end
    end
    return head, done
end

local enabled = false

function kerns.set(factor)
    if factor ~= v_max then
        factor = tonumber(factor) or 0
    end
    if factor == v_max or factor ~= 0 then
        if not enabled then
            tasks.enableaction("processors","typesetters.kerns.handler")
            enabled = true
        end
        local a = factors[factor]
        if not a then
            a = #mapping + 1
            factors[factors], mapping[a] = a, factor
        end
        factor = a
    else
        factor = unsetvalue
    end
    texsetattribute(a_kerns,factor)
    return factor
end

function kerns.handler(head)
    local head, done = do_process(tonut(head))  -- no direct map, because else fourth argument is tail == true
    return tonode(head), done
end

-- interface

commands.setcharacterkerning = kerns.set
