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
-- components: better split on tounicode

local next, type, tonumber = next, type, tonumber

local nodes              = nodes
local fonts              = fonts

local enableaction       = nodes.tasks.enableaction

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

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

-- check what is used

local find_node_tail     = nuts.tail
local flush_node         = nuts.flush_node
local insert_node_before = nuts.insert_before
local insert_node_after  = nuts.insert_after
local end_of_math        = nuts.end_of_math
local use_components     = nuts.use_components

local getfield           = nuts.getfield
local getnext            = nuts.getnext
local getprev            = nuts.getprev
local getid              = nuts.getid
local getfont            = nuts.getfont
local getsubtype         = nuts.getsubtype
local getchar            = nuts.getchar
local getdisc            = nuts.getdisc
local getglue            = nuts.getglue
local getkern            = nuts.getkern
local isglyph            = nuts.isglyph

local setfield           = nuts.setfield
local getattr            = nuts.getattr
local takeattr           = nuts.takeattr
local setlink            = nuts.setlink
local setdisc            = nuts.setdisc
local setglue            = nuts.setglue
local setkern            = nuts.setkern

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

local setglue            = nuts.setglue -- todo

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 listcodes          = nodes.listcodes

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 box_list_code      = listcodes.box
local user_list_code     = listcodes.unknown

local discretionary_code = disccodes.discretionary
local automatic_code     = disccodes.automatic

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 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)
local trace_ligatures_d  = false  trackers.register("typesetters.kerns.ligatures.detail",function(v) trace_ligatures_d = 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
            if trace_ligatures_d then
                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","dynamic","keepligatures")
            end
            setcolor(n,"darkred")
            return true
        end
        local k = fontfeatures[f].keepligatures
        if k == v_auto then
            if trace_ligatures_d then
                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","static","keepligatures")
            end
            setcolor(n,"darkgreen")
            return true
        end
        if not k then
            if trace_ligatures_d then
                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"split","static","keepligatures")
            end
            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)
        setfield(g,"stretch",kern)
        setfield(g,"stretch_order",1)
        return g
    else
        return new_kern(kern)
    end
end

-- a simple list injector, no components and such .. just disable ligatures in
-- kern mode .. maybe not even hyphenate ... anyway, the next one is for simple
-- sublists .. beware: we can have char -1

local function inject_begin(boundary,prev,keeptogether,krn,ok) -- prev is a glyph
    local char, id = isglyph(boundary)
    if id == kern_code then
        if getsubtype(boundary) == kerning_code or getattr(boundary,a_fontkern) then
            local inject = true
            if keeptogether then
                local next = getnext(boundary)
                if not next or (getid(next) == glyph_code and keeptogether(prev,next)) then
                    inject = false
                end
            end
            if inject then
                -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                setkern(boundary,getkern(boundary) + quaddata[getfont(prev)]*krn,userkern_code)
                return boundary, true
            end
        end
    elseif char then
        if keeptogether and keeptogether(boundary,prev) then
            -- keep 'm
        else
            local prevchar = isglyph(prev)
            if prevchar and prevchar > 0 then
                local font  = getfont(boundary)
                local data  = chardata[font][prevchar]
                local kerns = data and data.kerns
                local kern  = new_kern((kerns and kerns[char] or 0) + quaddata[font]*krn)
                setlink(kern,boundary)
                return kern, true
            end
        end
    end
    return boundary, ok
end

local function inject_end(boundary,next,keeptogether,krn,ok)
    local tail = find_node_tail(boundary)
    local char, id = getid(tail)
    if id == kern_code then
        if getsubtype(tail) == kerning_code or getattr(tail,a_fontkern) then
            local inject = true
            if keeptogether then
                local prev = getprev(tail)
                if getid(prev) == glyph_code and keeptogether(prev,two) then
                    inject = false
                end
            end
            if inject then
                -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                setkern(tail,getkern(tail) + quaddata[getfont(next)]*krn,userkern_code)
                return boundary, true
            end
        end
    elseif char then
        if keeptogether and keeptogether(tail,two) then
            -- keep 'm
        else
            local nextchar = isglyph(tail)
            if nextchar and nextchar > 0 then
                local font  = getfont(tail)
                local data  = chardata[font][nextchar]
                local kerns = data and data.kerns
                local kern  = (kerns and kerns[char] or 0) + quaddata[font]*krn
                setlink(tail,new_kern(kern))
                return boundary, true
            end
        end
    end
    return boundary, ok
end

local function process_list(head,keeptogether,krn,font,okay)
    local start = head
    local prev  = nil
    local pid   = nil
    local kern  = 0
    local mark  = font and markdata[font]
    while start  do
        local char, id = isglyph(start)
        if char then
            if not font then
                font = getfont(start)
                mark = markdata[font]
                kern = quaddata[font]*krn
            end
            if prev then
                if mark[char] then
                    -- skip
                elseif pid == kern_code then
                    if getsubtype(prev) == kerning_code or getattr(prev,a_fontkern) then
                        local inject = true
                        if keeptogether then
                            local prevprev = getprev(prev)
                            if getid(prevprev) == glyph_code and keeptogether(prevprev,start) then
                                inject = false
                            end
                        end
                        if inject then
                            -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                            setkern(prev,getkern(prev) + kern,userkern_code)
                            okay = true
                        end
                    end
                elseif pid == glyph_code then
                    if keeptogether and keeptogether(prev,start) then
                        -- keep 'm
                    else
                        local prevchar = getchar(prev)
                        local data     = chardata[font][prevchar]
                        local kerns    = data and data.kerns
                     -- if kerns then
                     --     print("it happens indeed, basemode kerns not yet injected")
                     -- end
                        insert_node_before(head,start,new_kern((kerns and kerns[char] or 0) + kern))
                        okay = true
                    end
                end
            end
        end
        if start then
            prev  = start
            pid   = id
            start = getnext(start)
        end
    end
    return head, okay, prev
end

local function closest_bound(b,get)
    b = get(b)
    if b and getid(b) ~= glue_code then
        while b do
            if not getattr(b,a_kerns) then
                break
            elseif getid(b) == glyph_code then
                return b, getfont(b)
            else
                b = get(b)
            end
        end
    end
end

function kerns.handler(head)
    local head         = tonut(head)
    local start        = head
    local done         = false
    local lastfont     = nil
    local keepligature = kerns.keepligature
    local keeptogether = kerns.keeptogether
    local fillup       = false
    local bound        = false
    local prev         = nil
    local previd       = nil
    local prevchar     = nil
    local prevfont     = nil
    local prevmark     = nil
    while start do
        -- fontkerns don't get the attribute but they always sit between glyphs so
        -- are always valid bound .. disc nodes also somtimes don't get them
        local id   = getid(start)
        local attr = takeattr(start,a_kerns)
        if attr and attr > 0 then
            local krn = mapping[attr]
            if krn == v_max then
                krn    = .25
                fillup = true
            else
                fillup = false
            end
            if not krn or krn == 0 then
                bound = false
            elseif id == glyph_code then
                if keepligature and keepligature(start) then
                    -- keep 'm
                else
                    -- we could use the subtype ligature but that's also a call
                    -- todo: check tounicode and use that information to split
                    head, start = use_components(head,start)
                end
                local char = getchar(start)
                local font = getfont(start)
                local mark = markdata[font]
                if not bound then
                    -- yet
                elseif mark[char] then
                    -- skip
                elseif previd == kern_code then
                    if getsubtype(prev) == kerning_code or getattr(prev,a_fontkern) then
                        local inject = true
                        if keeptogether then
                            if previd == glyph_code and keeptogether(prev,start) then
                                inject = false
                            end
                        end
                        if inject then
                            -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
                            setkern(prev,getkern(prev) + quaddata[font]*krn,userkern_code)
                            done = true
                        end
                    end
                elseif previd == glyph_code then
                    if prevfont == font then
                        if keeptogether and keeptogether(prev,start) then
                            -- keep 'm
                        else
                            local data  = chardata[font][prevchar]
                            local kerns = data and data.kerns
                            local kern  = (kerns and kerns[char] or 0) + quaddata[font]*krn
                            insert_node_before(head,start,kern_injector(fillup,kern))
                            done = true
                        end
                    else
                        insert_node_before(head,start,kern_injector(fillup,quaddata[font]*krn))
                        done = true
                    end
                end
                prev     = start
                prevchar = char
                prevfont = font
                prevmark = mark
                previd   = id
                bound    = true
            elseif id == disc_code then
                local prev, next, pglyph, nglyph -- delayed till needed
                local subtype = getsubtype(start)
                if subtype == automatic_code then
                    -- this is kind of special, as we have already injected the
                    -- previous kern
                    local prev   = getprev(start)
                    local pglyph = prev and getid(prev) == glyph_code
                    languages.expand(start,pglyph and prev)
                    -- we can have a different start now
                elseif subtype ~= discretionary_code then
                    prev    = getprev(start)
                    pglyph  = prev and getid(prev) == glyph_code
                    languages.expand(start,pglyph and prev)
                end
                local pre, post, replace = getdisc(start)
                local indeed = false
                if pre then
                    local okay = false
                    if not prev then
                        prev   = prev or getprev(start)
                        pglyph = prev and getid(prev) == glyph_code
                    end
                    if pglyph then
                        pre, okay = inject_begin(pre,prev,keeptogether,krn,okay)
                    end
                    pre, okay = process_list(pre,keeptogether,krn,false,okay)
                    if okay then
                        indeed = true
                    end
                end
                if post then
                    local okay = false
                    if not next then
                        next   = getnext(start)
                        nglyph = next and getid(next) == glyph_code
                    end
                    if nglyph then
                        post, okay = inject_end(post,next,keeptogether,krn,okay)
                    end
                    post, okay = process_list(post,keeptogether,krn,false,okay)
                    if okay then
                        indeed = true
                    end
                end
                if replace then
                    local okay = false
                    if not prev then
                        prev    = prev or getprev(start)
                        pglyph  = prev and getid(prev) == glyph_code
                    end
                    if pglyph then
                        replace, okay = inject_begin(replace,prev,keeptogether,krn,okay)
                    end
                    if not next then
                        next   = getnext(start)
                        nglyph = next and getid(next) == glyph_code
                    end
                    if nglyph then
                        replace, okay = inject_end(replace,next,keeptogether,krn,okay)
                    end
                    replace, okay = process_list(replace,keeptogether,krn,false,okay)
                    if okay then
                        indeed = true
                    end
                elseif prevfont then
                    replace = new_kern(quaddata[prevfont]*krn)
                    indeed  = true
                end
                if indeed then
                    setdisc(start,pre,post,replace)
                    done = true
                end
                bound = false
            elseif id == kern_code then
                bound  = getsubtype(start) == kerning_code or getattr(start,a_fontkern)
                prev   = start
                previd = id
            elseif id == glue_code then
                local subtype = getsubtype(start)
                if subtype == userskip_code or subtype == xspaceskip_code or subtype == spaceskip_code then
                    local width, stretch, shrink, stretch_order, shrink_order = getglue(start)
                    if width > 0 then
                        local w = width + gluefactor * width * krn
                        stretch = stretch * w / width
                        shrink  = shrink  * w / width
                        if fillup then
                            stretch = 2 * stretch
                            shrink  = 2 * shrink
                            stretch_order = 1
                         -- shrink_order  = 1 ?
                        end
                        setglue(start,w,stretch,shrink,stretch_order,shrink_order)
                        done = true
                    end
                end
                bound = false
            elseif id == hlist_code or id == vlist_code then
                local subtype = getsubtype(start)
                if subtype == user_list_code or subtype == box_list_code then
                    -- special case
                    local b, f = closest_bound(start,getprev)
                    if b then
                        insert_node_before(head,start,kern_injector(fillup,quaddata[f]*krn))
                        done = true
                    end
                    local b, f = closest_bound(start,getnext)
                    if b then
                        insert_node_after(head,start,kern_injector(fillup,quaddata[f]*krn))
                        done = true
                    end
                end
                bound = false
            elseif id == math_code then
                start = end_of_math(start)
                bound = false
            end
            if start then
                start = getnext(start)
            end
        elseif id == kern_code then
            bound  = getsubtype(start) == kerning_code or getattr(start,a_fontkern)
            prev   = start
            previd = id
            start  = getnext(start)
        else
            bound = false
            start = getnext(start)
        end
    end
    return tonode(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
            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

-- interface

interfaces.implement {
    name      = "setcharacterkerning",
    actions   = kerns.set,
    arguments = "string"
}

