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

local next, type = next, type

local trace_spacing = false  trackers.register("typesetters.spacing", function(v) trace_spacing = v end)

local report_spacing = logs.reporter("typesetting","spacing")

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

local fonthashes         = fonts.hashes
local chardata           = fonthashes.characters
local quaddata           = fonthashes.quads

local texsetattribute    = tex.setattribute

local unsetvalue         <const> = attributes.unsetvalue

local xscaled            = font.xscaled

local v_reset            <const> = interfaces.variables.reset

local nuts               = nodes.nuts

local getnext            = nuts.getnext
local getprev            = nuts.getprev
local getattr            = nuts.getattr
local isglyph            = nuts.isglyph
local getlanguage        = nuts.getlanguage
local setattrlist        = nuts.setattrlist
local setoptions         = nuts.setoptions

local insertnodebefore   = nuts.insertbefore
local insertnodeafter    = nuts.insertafter
local remove_node        = nuts.remove
local endofmath          = nuts.endofmath
local unsetattributes    = nuts.unsetattributes
local findattribute      = nuts.findattribute

local nodepool           = nuts.pool
local new_penalty        = nodepool.penalty
local new_glue           = nodepool.glue

local math_code          <const> = nodes.nodecodes.math
local noautobreak_code   <const> = tex.glueoptioncodes.noautobreak

local somespace          = nuts.somespace
local somepenalty        = nuts.somepenalty

local enableaction       = nodes.tasks.enableaction

typesetters              = typesetters or { }
local typesetters        = typesetters

typesetters.spacings     = typesetters.spacings or { }
local spacings           = typesetters.spacings

spacings.mapping         = spacings.mapping or { }
spacings.numbers         = spacings.numbers or { }

local a_spacings         <const> = attributes.private("spacing")

storage.register("typesetters/spacings/mapping", spacings.mapping, "typesetters.spacings.mapping")

local mapping = spacings.mapping
local numbers = spacings.numbers

for i=1,#mapping do
    local m = mapping[i]
    numbers[m.name] = m
end

-- todo cache lastattr

-- traverseattr
-- traverseattrmethod

function spacings.handler(head)
    local _, start = findattribute(head, a_spacings)
    if start then
        -- head is always begin of par (whatsit), so we have at least two prev nodes
        -- penalty followed by glue
        while start do
            local char, id = isglyph(start)
            if char then
                local attr = getattr(start,a_spacings)
                if attr and attr > 0 then
                    local data = mapping[attr]
                    if data then
                        local map = data.characters[char]
                        if map then
                            local language = data.language or 0
--                             if language == 0 or languages.registered[language].number == getlanguage(start) then
                            if language == 0 or languages.numbers[language] == getlanguage(start) then
                                local font = id
                                local left = map.left
                                local right = map.right
                                local alternative = map.alternative
                                local quad = xscaled(quaddata[font])
                                local prev = getprev(start)
                                if not chardata[font][char] then
                                    report_spacing("missing character %C in font %i",char,font)
                                end
                                if left and left ~= 0 and prev then
                                    local ok = false
                                    local prevprev = getprev(prev)
                                    if alternative == 1 then
                                        if somespace(prev,true) then
                                            if somepenalty(prevprev,10000) then
                                                if trace_spacing then
                                                    report_spacing("removing penalty and space before %C (left)",char)
                                                end
                                                head = remove_node(head,prev,true)
                                                head = remove_node(head,prevprev,true)
                                            else
                                                if trace_spacing then
                                                    report_spacing("removing space before %C (left)",char)
                                                end
                                                head = remove_node(head,prev,true)
                                            end
                                        end
                                        ok = true
                                    else
                                        ok = not (somespace(prev,true) and somepenalty(prevprev,true)) or somespace(prev,true)
                                    end
                                    if ok then
                                        if trace_spacing then
                                            report_spacing("inserting penalty and space before %C (left)",char)
                                        end
                                        local g = new_glue(left*quad)
--                                         local p = new_penalty(10000)
                                        setattrlist(g,start)
--                                         setattrlist(p,start)
--                                         insertnodebefore(head,start,p)
                                        insertnodebefore(head,start,g)
                                        setoptions(g,noautobreak_code)
                                    end
                                end
                                local next = getnext(start)
                                if right and right ~= 0 and next then
                                    local ok = false
                                    local nextnext = getnext(next)
                                    if alternative == 1 then
                                        if somepenalty(next,10000) then
                                            if somespace(nextnext,true) then
                                                if trace_spacing then
                                                    report_spacing("removing penalty and space after %C right",char)
                                                end
                                                head = remove_node(head,next,true)
                                                head = remove_node(head,nextnext,true)
                                            end
                                        elseif somespace(next,true) then
                                            if trace_spacing then
                                                report_spacing("removing space after %C (right)", char)
                                            end
                                            head = remove_node(head,next,true)
                                        end
                                        ok = true
                                    else
                                        ok = not (somepenalty(next,10000) and somespace(nextnext,true)) or somespace(next,true)
                                    end
                                    if ok then
                                        if trace_spacing then
                                            report_spacing("inserting penalty and space after %C (right)",char)
                                        end
                                        local g = new_glue(right*quad)
--                                         local p = new_penalty(10000)
                                        setattrlist(g,start)
--                                         setattrlist(p,start)
                                        insertnodeafter(head,start,g)
--                                         insertnodeafter(head,start,p)
                                        setoptions(g,noautobreak_code)
                                    end
                                end
                            end
                        end
                    end
                end
            elseif id == math_code then
                start = endofmath(start) -- weird, can return nil .. no math end?
            end
            if start then
                start = getnext(start)
            end
        end
    end
    return head
end

local enabled = false

function spacings.define(name,parent)
    local data = numbers[name]
    if data then
        -- error, already defined
    else
        local number = #mapping + 1
        local data = {
            name       = name,
            number     = number,
            characters = { },
        }
        mapping[number] = data
        numbers[name]   = data
        if parent and parent ~= "" then
            -- could be dynamic i.e. metatable inheritance but maybe only as option
            table.merge(data.characters,unpack(utilities.parsers.settings_to_array(parent)))
        end
    end
end

function spacings.setup(name,char,settings)
    local data = numbers[name]
    if not data then
        -- error
    elseif char > 0 then
        data.characters[char] = settings
    else
        for k, v in next, settings do
            data[k] = v
        end
    end
end

function spacings.set(name)
    local n = unsetvalue
    if name ~= v_reset then
        local data = numbers[name]
        if data then
            if not enabled then
                enableaction("processors","typesetters.spacings.handler")
                enabled = true
            end
            n = data.number or unsetvalue
        end
    end
    texsetattribute(a_spacings,n)
end

function spacings.reset()
    texsetattribute(a_spacings,unsetvalue)
end

-- interface

local implement = interfaces.implement

implement {
    name      = "definecharacterspacing",
    actions   = spacings.define,
    arguments = "2 strings"
}

implement {
    name      = "setupcharacterspacing",
    actions   = spacings.setup,
    arguments = {
        "string",
        "integer",
        {
            { "left",        "number" },
            { "right",       "number" },
            { "alternative", "integer" },
            { "language",    "string" },
        }
    }
}

implement {
    name      = "setcharacterspacing",
    actions   = spacings.set,
    arguments = "string"
}
