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

--[[ldx--
<p>This is rather experimental. We need more control and some of this
might become a runtime module instead. This module will be cleaned up!</p>
--ldx]]--

local utfchar = utf.char
local concat = table.concat
local format, match, gmatch, concat, rep = string.format, string.match, string.gmatch, table.concat, string.rep
local lpegmatch = lpeg.match
local write_nl = texio.write_nl

local report_nodes = logs.reporter("nodes","tracing")

fonts = fonts or { }
nodes = nodes or { }

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

nodes.tracers         = nodes.tracers or { }
local tracers         = nodes.tracers

nodes.tasks           = nodes.tasks or { }
local tasks           = nodes.tasks

nodes.handlers        = nodes.handlers or { }
local handlers        = nodes.handlers

nodes.injections      = nodes.injections or { }
local injections      = nodes.injections

tracers.characters    = tracers.characters or { }
tracers.steppers      = tracers.steppers   or { }

local char_tracers    = tracers.characters
local step_tracers    = tracers.steppers

local copy_node_list  = node.copy_list
local hpack_node_list = node.hpack
local free_node_list  = node.flush_list
local traverse_nodes  = node.traverse

local nodecodes       = nodes.nodecodes
local whatcodes       = nodes.whatcodes
local skipcodes       = nodes.skipcodes

local glyph_code      = nodecodes.glyph
local hlist_code      = nodecodes.hlist
local vlist_code      = nodecodes.vlist
local disc_code       = nodecodes.disc
local glue_code       = nodecodes.glue
local kern_code       = nodecodes.kern
local rule_code       = nodecodes.rule
local whatsit_code    = nodecodes.whatsit

local localpar_code   = whatcodes.localpar
local dir_code        = whatcodes.dir

local userskip_code   = skipcodes.userskip

local nodepool        = nodes.pool

local new_glyph       = nodepool.glyph

function char_tracers.collect(head,list,tag,n)
    local fontdata = fonts.hashes.identifiers
    n = n or 0
    local ok, fn = false, nil
    while head do
        local id = head.id
        if id == glyph_code then
            local f = head.font
            if f ~= fn then
                ok, fn = false, f
            end
            local c = head.char
            local i = fontdata[f].indices[c] or 0
            if not ok then
                ok = true
                n = n + 1
                list[n] = list[n] or { }
                list[n][tag] = { }
            end
            local l = list[n][tag]
            l[#l+1] = { c, f, i }
        elseif id == disc_code then
            -- skip
        else
            ok = false
        end
        head = head.next
    end
end

function char_tracers.equal(ta, tb)
    if #ta ~= #tb then
        return false
    else
        for i=1,#ta do
            local a, b = ta[i], tb[i]
            if a[1] ~= b[1] or a[2] ~= b[2] or a[3] ~= b[3] then
                return false
            end
        end
    end
    return true
end

function char_tracers.string(t)
    local tt = { }
    for i=1,#t do
        tt[i] = utfchar(t[i][1])
    end
    return concat(tt,"")
end

function char_tracers.unicodes(t,decimal)
    local tt = { }
    for i=1,#t do
        local n = t[i][1]
        if n == 0 then
            tt[i] = "-"
        elseif decimal then
            tt[i] = n
        else
            tt[i] = format("U+%04X",n)
        end
    end
    return concat(tt," ")
end

function char_tracers.indices(t,decimal)
    local tt = { }
    for i=1,#t do
        local n = t[i][3]
        if n == 0 then
            tt[i] = "-"
        elseif decimal then
            tt[i] = n
        else
            tt[i] = format("U+%04X",n)
        end
    end
    return concat(tt," ")
end

function char_tracers.start()
    local npc = handlers.characters
    local list = { }
    function handlers.characters(head)
        local n = #list
        char_tracers.collect(head,list,'before',n)
        local h, d = npc(head)
        char_tracers.collect(head,list,'after',n)
        if #list > n then
            list[#list+1] = { }
        end
        return h, d
    end
    function char_tracers.stop()
        tracers.list['characters'] = list
        local variables = {
            ['title']                = 'ConTeXt Character Processing Information',
            ['color-background-one'] = lmx.get('color-background-yellow'),
            ['color-background-two'] = lmx.get('color-background-purple'),
        }
        lmx.show('context-characters.lmx',variables)
        handlers.characters = npc
        tasks.restart("processors", "characters")
    end
    tasks.restart("processors", "characters")
end

local stack = { }

function tracers.start(tag)
    stack[#stack+1] = tag
    local tracer = tracers[tag]
    if tracer and tracer.start then
        tracer.start()
    end
end
function tracers.stop()
    local tracer = stack[#stack]
    if tracer and tracer.stop then
        tracer.stop()
    end
    stack[#stack] = nil
end

-- experimental

local collection, collecting, messages = { }, false, { }

function step_tracers.start()
    collecting = true
end

function step_tracers.stop()
    collecting = false
end

function step_tracers.reset()
    for i=1,#collection do
        local c = collection[i]
        if c then
            free_node_list(c)
        end
    end
    collection, messages = { }, { }
end

function step_tracers.nofsteps()
    return tex.write(#collection)
end

function step_tracers.glyphs(n,i)
    local c = collection[i]
    if c then
        tex.box[n] = hpack_node_list(copy_node_list(c))
    end
end

function step_tracers.features()
    -- we cannot use first_glyph here as it only finds characters with subtype < 256
    local fontdata = fonts.hashes.identifiers
    local f = collection[1]
    while f do
        if f.id == glyph_code then
            local tfmdata, t = fontdata[f.font], { }
            for feature, value in table.sortedhash(tfmdata.shared.features) do
                if feature == "number" or feature == "features" then
                    -- private
                elseif type(value) == "boolean" then
                    if value then
                        t[#t+1] = format("%s=yes",feature)
                    else
                        -- skip
                    end
                else
                    t[#t+1] = format("%s=%s",feature,value)
                end
            end
            if #t > 0 then
                context(concat(t,", "))
            else
                context("no features")
            end
            return
        end
        f = f.next
    end
end

function tracers.fontchar(font,char)
    local fontchar = fonts.hashes.characters
    local n = new_glyph()
    n.font, n.char, n.subtype = font, char, 256
    context(n)
end

function step_tracers.codes(i,command)
    local fontdata = fonts.hashes.identifiers
    local c = collection[i]
    while c do
        local id = c.id
        if id == glyph_code then
            if command then
                local f, c = c.font,c.char
                local d = fontdata[f].descriptions
                local d = d and d[c]
                context[command](f,c,d and d.class or "")
            else
                context("[%s:U+%04X]",c.font,c.char)
            end
        elseif id == whatsit_code and (c.subtype == localpar_code or c.subtype == dir_code) then
            context("[%s]",c.dir)
        else
            context("[%s]",nodecodes[id])
        end
        c = c.next
    end
end

function step_tracers.messages(i,command,split)
    local list = messages[i] -- or { "no messages" }
    if list then
        for i=1,#list do
            local l = list[i]
            if not command then
                context("(%s)",l)
            elseif split then
                local a, b = match(l,"^(.-)%s*:%s*(.*)$")
                context[command](a or l or "",b or "")
            else
                context[command](l)
            end
        end
    end
end

-- hooks into the node list processor (see otf)

function step_tracers.check(head)
    if collecting then
        step_tracers.reset()
        local n = copy_node_list(head)
        injections.handler(n,nil,"trace",true)
        handlers.protectglyphs(n) -- can be option
        collection[1] = n
    end
end

function step_tracers.register(head)
    if collecting then
        local nc = #collection+1
        if messages[nc] then
            local n = copy_node_list(head)
            injections.handler(n,nil,"trace",true)
            handlers.protectglyphs(n) -- can be option
            collection[nc] = n
        end
    end
end

function step_tracers.message(str,...)
    str = format(str,...)
    if collecting then
        local n = #collection + 1
        local m = messages[n]
        if not m then m = { } messages[n] = m end
        m[#m+1] = str
    end
    return str -- saves an intermediate var in the caller
end

-- this will be reorganized:

function nodes.showlist(head, message)
    if message then
        write_nl(message)
    end
    for n in traverse_nodes(head) do
        write_nl(tostring(n))
    end
end

function nodes.handlers.checkglyphs(head,message)
    local t = { }
    for g in traverse_id(glyph_code,head) do
        t[#t+1] = format("U+%04X:%s",g.char,g.subtype)
    end
    if #t > 0 then
        if message and message ~= "" then
            report_nodes("%s, %s glyphs: %s",message,#t,concat(t," "))
        else
            report_nodes("%s glyphs: %s",#t,concat(t," "))
        end
    end
    return false
end

function nodes.handlers.checkforleaks(sparse)
    local l = { }
    local q = node.usedlist()
    for p in traverse(q) do
        local s = table.serialize(nodes.astable(p,sparse),nodecodes[p.id])
        l[s] = (l[s] or 0) + 1
    end
    node.flush_list(q)
    for k, v in next, l do
        write_nl(format("%s * %s", v, k))
    end
end

local function tosequence(start,stop,compact)
    if start then
        local t = { }
        while start do
            local id = start.id
            if id == glyph_code then
                local c = start.char
                if compact then
                    if start.components then
                        t[#t+1] = tosequence(start.components,nil,compact)
                    else
                        t[#t+1] = utfchar(c)
                    end
                else
                    t[#t+1] = format("U+%04X:%s",c,utfchar(c))
                end
            elseif id == whatsit_code and start.subtype == localpar_code or start.subtype == dir_code then
                t[#t+1] = "[" .. start.dir .. "]"
            elseif id == rule_code then
                if compact then
                    t[#t+1] = "|"
                else
                    t[#t+1] = nodecodes[id]
                end
            else
                if compact then
                    t[#t+1] = "[]"
                else
                    t[#t+1] = nodecodes[id]
                end
            end
            if start == stop then
                break
            else
                start = start.next
            end
        end
        if compact then
            return concat(t)
        else
            return concat(t," ")
        end
    else
        return "[empty]"
    end
end

nodes.tosequence = tosequence

function nodes.report(t,done)
    if done then
        if status.output_active then
            report_nodes("output, changed, %s nodes",nodes.count(t))
        else
            write_nl("nodes","normal, changed, %s nodes",nodes.count(t))
        end
    else
        if status.output_active then
            report_nodes("output, unchanged, %s nodes",nodes.count(t))
        else
            write_nl("nodes","normal, unchanged, %s nodes",nodes.count(t))
        end
    end
end

function nodes.packlist(head)
    local t = { }
    for n in traverse(head) do
        t[#t+1] = tostring(n)
    end
    return t
end

function nodes.idstostring(head,tail)
    local t, last_id, last_n = { }, nil, 0
    for n in traverse_nodes(head,tail) do -- hm, does not stop at tail
        local id = n.id
        if not last_id then
            last_id, last_n = id, 1
        elseif last_id == id then
            last_n = last_n + 1
        else
            if last_n > 1 then
                t[#t+1] = format("[%s*%s]",last_n,nodecodes[last_id] or "?")
            else
                t[#t+1] = format("[%s]",nodecodes[last_id] or "?")
            end
            last_id, last_n = id, 1
        end
        if n == tail then
            break
        end
    end
    if not last_id then
        t[#t+1] = "no nodes"
    elseif last_n > 1 then
        t[#t+1] = format("[%s*%s]",last_n,nodecodes[last_id] or "?")
    else
        t[#t+1] = format("[%s]",nodecodes[last_id] or "?")
    end
    return concat(t," ")
end

--~ function nodes.xidstostring(head,tail) -- only for special tracing of backlinks
--~     local n = head
--~     while n.next do
--~         n = n.next
--~     end
--~     local t, last_id, last_n = { }, nil, 0
--~     while n do
--~         local id = n.id
--~         if not last_id then
--~             last_id, last_n = id, 1
--~         elseif last_id == id then
--~             last_n = last_n + 1
--~         else
--~             if last_n > 1 then
--~                 t[#t+1] = format("[%s*%s]",last_n,nodecodes[last_id] or "?")
--~             else
--~                 t[#t+1] = format("[%s]",nodecodes[last_id] or "?")
--~             end
--~             last_id, last_n = id, 1
--~         end
--~         if n == head then
--~             break
--~         end
--~         n = n.prev
--~     end
--~     if not last_id then
--~         t[#t+1] = "no nodes"
--~     elseif last_n > 1 then
--~         t[#t+1] = format("[%s*%s]",last_n,nodecodes[last_id] or "?")
--~     else
--~         t[#t+1] = format("[%s]",nodecodes[last_id] or "?")
--~     end
--~     return table.concat(table.reversed(t)," ")
--~ end

local function showsimplelist(h,depth,n)
    while h do
        write_nl(rep(" ",n) .. tostring(h))
        if not depth or n < depth then
            local id = h.id
            if id == hlist_code or id == vlist_code then
                showsimplelist(h.list,depth,n+1)
            end
        end
        h = h.next
    end
end

--~ \startluacode
--~ callback.register('buildpage_filter',function() nodes.show_simple_list(tex.lists.contrib_head) end)
--~ \stopluacode
--~ \vbox{b\footnote{n}a}
--~ \startluacode
--~ callback.register('buildpage_filter',nil)
--~ \stopluacode

nodes.showsimplelist = function(h,depth) showsimplelist(h,depth,0) end

function nodes.listtoutf(h,joiner)
    local joiner = (joiner ==true and utfchar(0x200C)) or joiner -- zwnj
    local w = { }
    while h do
        if h.id == glyph_code then -- always true
            w[#w+1] = utfchar(h.char)
            if joiner then
                w[#w+1] = joiner
            end
        else
            w[#w+1] = "[-]"
        end
        h = h.next
    end
    return concat(w)
end

local what = { [0] = "unknown", "line", "box", "indent", "row", "cell" }

local function showboxes(n,symbol,depth)
    depth, symbol = depth or 0, symbol or "."
    for n in traverse_nodes(n) do
        local id = n.id
        if id == hlist_code or id == vlist_code then
            local s = n.subtype
            report_nodes(rep(symbol,depth) .. what[s] or s)
            showboxes(n.list,symbol,depth+1)
        end
    end
end

nodes.showboxes = showboxes

local threshold = 65536

local function toutf(list,result,nofresult,stopcriterium)
    if list then
        local fontchar = fonts.hashes.characters
        for n in traverse_nodes(list) do
            local id = n.id
            if id == glyph_code then
                local components = n.components
                if components then
                    result, nofresult = toutf(components,result,nofresult)
                else
                    local c = n.char
                    local fc = fontchar[n.font]
                    if fc then
                        local u = fc[c].tounicode
                        if u then
                            for s in gmatch(u,"....") do
                                nofresult = nofresult + 1
                                result[nofresult] = utfchar(tonumber(s,16))
                            end
                        else
                            nofresult = nofresult + 1
                            result[nofresult] = utfchar(c)
                        end
                    else
                        nofresult = nofresult + 1
                        result[nofresult] = utfchar(c)
                    end
                end
            elseif id == disc_code then
                result, nofresult = toutf(n.replace,result,nofresult) -- needed?
            elseif id == hlist_code or id == vlist_code then
--~                 if nofresult > 0 and result[nofresult] ~= " " then
--~                     nofresult = nofresult + 1
--~                     result[nofresult] = " "
--~                 end
                result, nofresult = toutf(n.list,result,nofresult)
            elseif id == glue_code and n.subtype == userskip_code and n.spec.width > threshold then
                if nofresult > 0 and result[nofresult] ~= " " then
                    nofresult = nofresult + 1
                    result[nofresult] = " "
                end
            elseif id == kern_code and n.kern > threshold then
                if nofresult > 0 and result[nofresult] ~= " " then
                    nofresult = nofresult + 1
                    result[nofresult] = " "
                end
            end
            if n == stopcriterium then
                break
            end
        end
    end
    return result, nofresult
end

function nodes.toutf(list,stopcriterium)
    local result, nofresult = toutf(list,{},0,stopcriterium)
    return concat(result)
end

-- might move elsewhere

local ptfactor = number.dimenfactors.pt
local bpfactor = number.dimenfactors.bp
local stripper = lpeg.patterns.stripzeros

local points = function(n)
    return lpegmatch(stripper,format("%.5fpt",n*ptfactor))
end

local basepoints = function(n)
    return lpegmatch(stripper,format("%.5fbp",n*bpfactor))
end

number.points     = points
number.basepoints = basepoints

--~ function nodes.thespec(s)
--~     local stretch_order = s.stretch_order
--~     local shrink_order = s.shrink_order
--~     local stretch_unit = (stretch_order ~= 0) and ("fi".. string.rep("l",stretch_order)) or "sp"
--~     local shrink_unit = (shrink_order ~= 0) and ("fi".. string.rep("l",shrink_order)) or "sp"
--~     return string.format("%ssp+ %ssp - %ssp",s.width,s.stretch,stretch_unit,s.shrink,shrink_unit)
--~ end

local colors   = { }
tracers.colors = colors

local get_attribute   = node.has_attribute
local set_attribute   = node.set_attribute
local unset_attribute = node.unset_attribute

local attribute  = attributes.private('color')
local colormodel = attributes.private('colormodel')
local mapping    = attributes.list[attribute] or { }

function colors.set(n,c,s)
    local mc = mapping[c]
    if not mc then
        unset_attribute(n,attribute)
    else
        if not get_attribute(n,colormodel) then
            set_attribute(n,colormodel,s or 1)
        end
        set_attribute(n,attribute,mc)
    end
end

function colors.reset(n)
    unset_attribute(n,attribute)
end
