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"
}

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

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

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

local texattribute       = tex.attribute
local unsetvalue         = attributes.unsetvalue

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

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 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 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 v_max              = interfaces.variables.max

typesetters              = typesetters or { }
local typesetters        = typesetters

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

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

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

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)

-- 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 = g.spec
        s.stretch = kern
        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)
        s.stretch_order = 1
        return s
    else
        return new_gluespec(width,stretch,shrink)
    end
end

-- needs checking ... base mode / node mode

local function do_process(namespace,attribute,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 start[attribute]
        if attr and attr > 0 then
            start[attribute] = 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 = start.id
                if id == glyph_code then
                    lastfont = start.font
                    local c = start.components
                    if c then
                        if keepligature and keepligature(start) then
                            -- keep 'm
                        else
                            c = do_process(namespace,attribute,c,attr)
                            local s = start
                            local p, n = s.prev, s.next
                            local tail = find_node_tail(c)
                            if p then
                                p.next = c
                                c.prev = p
                            else
                                head = c
                            end
                            if n then
                                n.prev = tail
                            end
                            tail.next = n
                            start = c
                            s.components = nil
                            -- we now leak nodes !
                        --  free_node(s)
                            done = true
                        end
                    end
                    local prev = start.prev
                    if not prev then
                        -- skip
                    elseif markdata[lastfont][start.char] then
                            -- skip
                    else
                        local pid = prev.id
                        if not pid then
                            -- nothing
                        elseif pid == kern_code then
                            if prev.subtype == kerning_code or prev[a_fontkern] then
                                if keeptogether and prev.prev.id == glyph_code and keeptogether(prev.prev,start) then -- we could also pass start
                                    -- keep 'm
                                else
                                    -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                                    prev.subtype = userkern_code
                                    prev.kern = prev.kern + quaddata[lastfont]*krn -- here
                                    done = true
                                end
                            end
                        elseif pid == glyph_code then
                            if prev.font == lastfont then
                                local prevchar, lastchar = prev.char, start.char
                                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 pre, post, replace = disc.pre, disc.post, disc.replace
                            local prv, nxt = disc.prev, disc.next
                            if pre and prv then -- must pair with start.prev
                                -- this one happens in most cases
                                local before = copy_node(prv)
                                pre.prev = before
                                before.next = pre
                                before.prev = nil
                                pre = do_process(namespace,attribute,before,attr)
                                pre = pre.next
                                pre.prev = nil
                                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)
                                tail.next = after
                                after.prev = tail
                                after.next = nil
                                post = do_process(namespace,attribute,post,attr)
                                tail.next = nil
                                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)
                                replace.prev = before
                                before.next = replace
                                before.prev = nil
                                tail.next = after
                                after.prev = tail
                                after.next = nil
                                replace = do_process(namespace,attribute,before,attr)
                                replace = replace.next
                                replace.prev = nil
                                after.prev.next = nil
                                disc.replace = replace
                                free_node(after)
                                free_node(before)
                            else
                                if prv and prv.id == glyph_code and prv.font == lastfont then
                                    local prevchar, lastchar = prv.char, start.char
                                    local kerns = chardata[lastfont][prevchar].kerns
                                    local kern = kerns and kerns[lastchar] or 0
                                    krn = kern + quaddata[lastfont]*krn -- here
                                else
                                    krn = quaddata[lastfont]*krn -- here
                                end
                                disc.replace = kern_injector(false,krn) -- only kerns permitted, no glue
                            end
                        end
                    end
                elseif id == glue_code then
                    local subtype = start.subtype
                    if subtype == userskip_code or subtype == xspaceskip_code or subtype == spaceskip_code then
                        local s = start.spec
                        local w = s.width
                        if w > 0 then
                            local width, stretch, shrink = w+gluefactor*w*krn, s.stretch, s.shrink
                            start.spec = spec_injector(fillup,width,stretch*width/w,shrink*width/w)
                            done = true
                        end
                    end
                elseif id == kern_code then
                 -- if start.subtype == kerning_code then -- handle with glyphs
                 --     local sk = start.kern
                 --     if sk > 0 then
                 --         start.kern = sk*krn
                 --         done = true
                 --     end
                 -- end
                elseif lastfont and (id == hlist_code or id == vlist_code) then -- todo: lookahead
                    local p = start.prev
                    if p and p.id ~= glue_code then
                        insert_node_before(head,start,kern_injector(fillup,quaddata[lastfont]*krn))
                        done = true
                    end
                    local n = start.next
                    if n and n.id ~= 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 = start.next
        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
    texattribute[a_kerns] = factor
    return factor
end

local function process(namespace,attribute,head)
    return do_process(namespace,attribute,head)  -- no direct map, because else fourth argument is tail == true
end

kerns.handler = nodes.installattributehandler {
    name     = "kern",
    namespace = kerns,
    processor = process,
}

-- interface

commands.setcharacterkerning = kerns.set
