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

local string, number, table = string, number, table
local node, nodes, attributes, fonts, tex = node, nodes, attributes, fonts, tex
local type = type
local format = string.format
local formatters = string.formatters

-- This module started out in the early days of mkiv and luatex with
-- visualizing kerns related to fonts. In the process of cleaning up the
-- visual debugger code it made sense to integrate some other code that
-- I had laying around and replace the old supp-vis debugging code. As
-- only a subset of the old visual debugger makes sense it has become a
-- different implementation. Soms of the m-visual functionality will also
-- be ported. The code is rather trivial. The caching is not really needed
-- but saves upto 50% of the time needed to add visualization. Of course
-- the overall runtime is larger because of color and layer processing in
-- the backend (can be times as much) so the runtime is somewhat larger
-- with full visualization enabled. In practice this will never happen
-- unless one is demoing.

-- We could use pdf literals and re stream codes but it's not worth the
-- trouble because we would end up in color etc mess. Maybe one day I'll
-- make a nodeinjection variant.

-- todo: global switch (so no attributes)
-- todo: maybe also xoffset, yoffset of glyph
-- todo: inline concat (more efficient)
-- todo: tags can also be numbers (just add to hash)

-- todo: dir and localpar nodes

local nodecodes           = nodes.nodecodes
local disc_code           = nodecodes.disc
local kern_code           = nodecodes.kern
local glyph_code          = nodecodes.glyph
local hlist_code          = nodecodes.hlist
local vlist_code          = nodecodes.vlist
local glue_code           = nodecodes.glue
local penalty_code        = nodecodes.penalty
local whatsit_code        = nodecodes.whatsit
local user_code           = nodecodes.user
local math_code           = nodecodes.math
local gluespec_code       = nodecodes.gluespec

local kerncodes           = nodes.kerncodes
local font_kern_code      = kerncodes.fontkern
local user_kern_code      = kerncodes.userkern

local gluecodes           = nodes.gluecodes
local cleaders_code       = gluecodes.cleaders
local userskip_code       = gluecodes.userskip
local space_code          = gluecodes.space
local xspace_code         = gluecodes.xspace
local leftskip_code       = gluecodes.leftskip
local rightskip_code      = gluecodes.rightskip

local whatsitcodes        = nodes.whatsitcodes
local mathcodes           = nodes.mathcodes

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

local setfield            = nuts.setfield
local setboth             = nuts.setboth
local setlink             = nuts.setlink
local setdisc             = nuts.setdisc
local setlist             = nuts.setlist
local setleader           = nuts.setleader
local setsubtype          = nuts.setsubtype
local setattr             = nuts.setattr

local getfield            = nuts.getfield
local getid               = nuts.getid
local getfont             = nuts.getfont
local getattr             = nuts.getattr
local getsubtype          = nuts.getsubtype
local getchar             = nuts.getchar
local getbox              = nuts.getbox
local getlist             = nuts.getlist
local getleader           = nuts.getleader
local getnext             = nuts.getnext
local getprev             = nuts.getprev
local getboth             = nuts.getboth
local getdisc             = nuts.getdisc

local hpack_nodes         = nuts.hpack
local vpack_nodes         = nuts.vpack
local copy_node           = nuts.copy
local copy_list           = nuts.copy_list
local free_node           = nuts.free
local free_node_list      = nuts.flush_list
local insert_node_before  = nuts.insert_before
local insert_node_after   = nuts.insert_after
local traverse_nodes      = nuts.traverse
local linked_nodes        = nuts.linked

local effectiveglue       = nuts.effective_glue

local hpack_string        = nuts.typesetters.tohpack

local texgetattribute     = tex.getattribute
local texsetattribute     = tex.setattribute

local unsetvalue          = attributes.unsetvalue

local current_font        = font.current

local fonthashes          = fonts.hashes
local chardata            = fonthashes.characters
local exheights           = fonthashes.exheights
local emwidths            = fonthashes.emwidths
local pt_factor           = number.dimenfactors.pt

local nodepool            = nuts.pool
local new_rule            = nodepool.rule
local new_kern            = nodepool.kern
local new_glue            = nodepool.glue
local new_penalty         = nodepool.penalty
local new_hlist           = nodepool.hlist
local new_vlist           = nodepool.vlist

local tracers             = nodes.tracers
local visualizers         = nodes.visualizers

local setcolor            = tracers.colors.set
local setlistcolor        = tracers.colors.setlist
local settransparency     = tracers.transparencies.set
local setlisttransparency = tracers.transparencies.setlist

local starttiming         = statistics.starttiming
local stoptiming          = statistics.stoptiming

local a_visual            = attributes.private("visual")
local a_fontkern          = attributes.private("fontkern")
local a_layer             = attributes.private("viewerlayer")

local hasbit              = number.hasbit
local bit                 = number.bit
local setbit              = number.setbit
local clearbit            = number.clearbit

local trace_hbox
local trace_vbox
local trace_vtop
local trace_kern
local trace_glue
local trace_penalty
local trace_fontkern
local trace_strut
local trace_whatsit
local trace_user
local trace_math
local trace_italic

local report_visualize = logs.reporter("visualize")

local modes = {
    hbox       =     1,
    vbox       =     2,
    vtop       =     4,
    kern       =     8,
    glue       =    16,
    penalty    =    32,
    fontkern   =    64,
    strut      =   128,
    whatsit    =   256,
    glyph      =   512,
    simple     =  1024,
    simplehbox =  1024 + 1,
    simplevbox =  1024 + 2,
    simplevtop =  1024 + 4,
    user       =  2048,
    math       =  4096,
    italic     =  8192,
    origin     = 16384,
}

local usedfont, exheight, emwidth
local l_penalty, l_glue, l_kern, l_fontkern, l_hbox, l_vbox, l_vtop, l_strut, l_whatsit, l_glyph, l_user, l_math, l_italic, l_origin

local enabled = false
local layers  = { }

local preset_boxes  = modes.hbox + modes.vbox + modes.origin
local preset_makeup = preset_boxes
                    + modes.kern + modes.glue + modes.penalty
local preset_all    = preset_makeup
                    + modes.fontkern + modes.whatsit + modes.glyph + modes.user + modes.math

function visualizers.setfont(id)
    usedfont = id or current_font()
    exheight = exheights[usedfont]
    emwidth  = emwidths[usedfont]
end

-- we can preset a bunch of bits

local function enable()
    if not usedfont then
        -- we use a narrow monospaced font -- infofont ?
        visualizers.setfont(fonts.definers.define { name = "lmmonoltcond10regular", size = tex.sp("4pt") })
    end
    for mode, value in next, modes do
        local tag = formatters["v_%s"](mode)
        attributes.viewerlayers.define {
            tag       = tag,
            title     = formatters["visualizer %s"](mode),
            visible   = "start",
            editable  = "yes",
            printable = "yes"
        }
        layers[mode] = attributes.viewerlayers.register(tag,true)
    end
    l_hbox     = layers.hbox
    l_vbox     = layers.vbox
    l_vtop     = layers.vtop
    l_glue     = layers.glue
    l_kern     = layers.kern
    l_penalty  = layers.penalty
    l_fontkern = layers.fontkern
    l_strut    = layers.strut
    l_whatsit  = layers.whatsit
    l_glyph    = layers.glyph
    l_user     = layers.user
    l_math     = layers.math
    l_italic   = layers.italic
    l_origin   = layers.origin
    nodes.tasks.enableaction("shipouts","nodes.visualizers.handler")
    report_visualize("enabled")
    enabled = true
    tex.setcount("global","c_syst_visualizers_state",1) -- so that we can optimize at the tex end
end

local function setvisual(n,a,what) -- this will become more efficient when we have the bit lib linked in
    if not n or n == "reset" then
        return unsetvalue
    elseif n == "makeup" then
        if not a or a == 0 or a == unsetvalue then
            a = preset_makeup
        else
            a = setbit(a,preset_makeup)
        end
    elseif n == "boxes" then
        if not a or a == 0 or a == unsetvalue then
            a = preset_boxes
        else
            a = setbit(a,preset_boxes)
        end
    elseif n == "all" then
        if what == false then
            return unsetvalue
        elseif not a or a == 0 or a == unsetvalue then
            a = preset_all
        else
            a = setbit(a,preset_all)
        end
    else
        local m = modes[n]
        if not m then
            -- go on
        elseif a == unsetvalue then
            if what == false then
                return unsetvalue
            else
             -- a = setbit(0,m)
                a = m
            end
        elseif what == false then
            a = clearbit(a,m)
        elseif not a or a == 0 then
            a = m
        else
            a = setbit(a,m)
        end
    end
    if not a or a == 0 or a == unsetvalue then
        return unsetvalue
    elseif not enabled then -- must happen at runtime (as we don't store layers yet)
        enable()
    end
    return a
end

function nuts.setvisual(n,mode)
    setattr(n,a_visual,setvisual(mode,getattr(n,a_visual),true))
end

function nuts.copyvisual(n,m)
    setattr(n,a_visual,getattr(m,a_visual))
end

function visualizers.setvisual(n)
    texsetattribute(a_visual,setvisual(n,texgetattribute(a_visual)))
end

function visualizers.setlayer(n)
    texsetattribute(a_layer,layers[n] or unsetvalue)
end

local function set(mode,v)
    texsetattribute(a_visual,setvisual(mode,texgetattribute(a_visual),v))
end

for mode, value in next, modes do
    trackers.register(formatters["visualizers.%s"](mode), function(v) set(mode,v) end)
end

local raisepenalties = false

directives.register("visualizers.raisepenalties",function(v) raisepenalties = v end)

local fraction = 10

trackers  .register("visualizers.reset",    function(v) set("reset", v) end)
trackers  .register("visualizers.all",      function(v) set("all",   v) end)
trackers  .register("visualizers.makeup",   function(v) set("makeup",v) end)
trackers  .register("visualizers.boxes",    function(v) set("boxes", v) end)
directives.register("visualizers.fraction", function(v) fraction = (v and tonumber(v)) or (v == "more" and 5) or 10 end)

local c_positive   = "trace:b"
local c_negative   = "trace:r"
local c_zero       = "trace:g"
local c_text       = "trace:s"
local c_space      = "trace:y"
local c_skip_a     = "trace:c"
local c_skip_b     = "trace:m"
local c_glyph      = "trace:o"
local c_ligature   = "trace:s"
local c_white      = "trace:w"
local c_math       = "trace:r"
local c_origin     = "trace:o"

local c_positive_d = "trace:db"
local c_negative_d = "trace:dr"
local c_zero_d     = "trace:dg"
local c_text_d     = "trace:ds"
local c_space_d    = "trace:dy"
local c_skip_a_d   = "trace:dc"
local c_skip_b_d   = "trace:dm"
local c_glyph_d    = "trace:do"
local c_ligature_d = "trace:ds"
local c_white_d    = "trace:dw"
local c_math_d     = "trace:dr"
local c_origin_d   = "trace:do"

local function sometext(str,layer,color,textcolor,lap) -- we can just paste verbatim together .. no typesteting needed
    local text = hpack_string(str,usedfont)
    local size = getfield(text,"width")
    local rule = new_rule(size,2*exheight,exheight/2)
    local kern = new_kern(-size)
    if color then
        setcolor(rule,color)
    end
    if textcolor then
        setlistcolor(getlist(text),textcolor)
    end
    local info = linked_nodes(rule,kern,text)
    setlisttransparency(info,c_zero)
    info = new_hlist(info)
    local width = getfield(info,"width")
    if lap then
        info = new_hlist(linked_nodes(new_kern(-width),info))
    end
    if layer then
        setattr(info,a_layer,layer)
    end
    return info, width
end

local f_cache = { }

local function fontkern(head,current)
    local kern = getfield(current,"kern") + getfield(current,"expansion_factor")
    local info = f_cache[kern]
    if info then
        -- print("hit fontkern")
    else
        local text = hpack_string(formatters[" %0.3f"](kern*pt_factor),usedfont)
        local rule = new_rule(emwidth/fraction,6*exheight,2*exheight)
        local list = getlist(text)
        if kern > 0 then
            setlistcolor(list,c_positive_d)
        elseif kern < 0 then
            setlistcolor(list,c_negative_d)
        else
            setlistcolor(list,c_zero_d)
        end
        setlisttransparency(list,c_text_d)
        settransparency(rule,c_text_d)
        setfield(text,"shift",-5 * exheight)
        info = new_hlist(linked_nodes(rule,text))
        setattr(info,a_layer,l_fontkern)
        f_cache[kern] = info
    end
    head = insert_node_before(head,current,copy_list(info))
    return head, current
end

local w_cache = { }
local tags    = {
    open             = "FIC",
    write            = "FIW",
    close            = "FIC",
    special          = "SPE",
    latelua          = "LUA",
    savepos          = "POS",
    userdefined      = "USR",
 -- backend stuff
    pdfliteral       = "PDF",
    pdfrefobj        = "PDF",
    pdfannot         = "PDF",
    pdfstartlink     = "PDF",
    pdfendlink       = "PDF",
    pdfdest          = "PDF",
    pdfthread        = "PDF",
    pdfstartthread   = "PDF",
    pdfendthread     = "PDF",
    pdfthreaddata    = "PDF",
    pdflinkdata      = "PDF",
    pdfcolorstack    = "PDF",
    pdfsetmatrix     = "PDF",
    pdfsave          = "PDF",
    pdfrestore       = "PDF",
}

local function whatsit(head,current)
    local what = getsubtype(current)
    local info = w_cache[what]
    if info then
        -- print("hit whatsit")
    else
        local tag = whatsitcodes[what]
        -- maybe different text colors per tag
        info = sometext(formatters["W:%s"](tag and tags[tag] or what),usedfont,nil,c_white)
        setattr(info,a_layer,l_whatsit)
        w_cache[what] = info
    end
    head, current = insert_node_after(head,current,copy_list(info))
    return head, current
end

local u_cache = { }

local function user(head,current)
    local what = getsubtype(current)
    local info = u_cache[what]
    if info then
        -- print("hit user")
    else
        info = sometext(formatters["U:%s"](what),usedfont)
        setattr(info,a_layer,l_user)
        u_cache[what] = info
    end
    head, current = insert_node_after(head,current,copy_list(info))
    return head, current
end

local m_cache = { }
local tags    = {
    beginmath = "B",
    endmath   = "E",
}

local function math(head,current)
    local what = getsubtype(current)
    local info = m_cache[what]
    if info then
        -- print("hit math")
    else
        local tag = mathcodes[what]
        info = sometext(formatters["M:%s"](tag and tags[tag] or what),usedfont,nil,c_math_d)
        setattr(info,a_layer,l_math)
        m_cache[what] = info
    end
    head, current = insert_node_after(head,current,copy_list(info))
    return head, current
end

local b_cache = { }

local o_cache = table.setmetatableindex(function(t,size)
    local rule = new_rule(2*size,size,size)
    origin = hpack_nodes(rule)
    setcolor(rule,c_origin_d)
    settransparency(rule,c_origin_d)
    setattr(rule,a_layer,l_origin)
    t[size] = origin
    return origin
end)

local function ruledbox(head,current,vertical,layer,what,simple,previous,trace_origin,parent)
    local wd = getfield(current,"width")
    if wd ~= 0 then
        local ht = getfield(current,"height")
        local dp = getfield(current,"depth")
        local shift = getfield(current,"shift")
        local next = getnext(current)
        local prev = previous
     -- local prev = getprev(current) -- prev can be wrong in math mode < 0.78.3
        setboth(current)
        local linewidth = emwidth/fraction
        local size      = 2*linewidth
        local baseline, baseskip
        if dp ~= 0 and ht ~= 0 then
            if wd > 20*linewidth then
                baseline = b_cache.baseline
                if not baseline then
                    -- due to an optimized leader color/transparency we need to set the glue node in order
                    -- to trigger this mechanism
                    local leader = linked_nodes(new_glue(size),new_rule(3*size,linewidth,0),new_glue(size))
                    leader = hpack_nodes(leader)
                    baseline = new_glue(0)
                    setleader(baseline,leader)
                    setsubtype(baseline,cleaders_code)
                    setfield(baseline,"stretch",65536)
                    setfield(baseline,"stretch_order",2)
                    setlisttransparency(baseline,c_text)
                    b_cache.baseline = baseline
                end
                baseline = copy_list(baseline)
                baseline = hpack_nodes(baseline,wd-size)
                -- or new_hlist, set head and also:
                -- baseline.width = wd
                -- baseline.glue_set = wd/65536
                -- baseline.glue_order = 2
                -- baseline.glue_sign = 1
                baseskip = new_kern(-wd+linewidth)
            else
                baseline = new_rule(wd-size,linewidth,0)
                baseskip = new_kern(-wd+size)
            end
        end
        local this
        if not simple then
            this = b_cache[what]
            if not this then
                local text = hpack_string(what,usedfont)
                this = linked_nodes(new_kern(-getfield(text,"width")),text)
                setlisttransparency(this,c_text)
                this = new_hlist(this)
                b_cache[what] = this
            end
        end
        -- we need to trigger the right mode (else sometimes no whatits)
        local info = linked_nodes(
            this and copy_list(this) or nil,
            new_rule(linewidth,ht,dp),
            new_rule(wd-size,-dp+linewidth,dp),
            new_rule(linewidth,ht,dp),
            new_kern(-wd+linewidth),
            new_rule(wd-size,ht,-ht+linewidth)
        )
        if baseskip then
            info = linked_nodes(info,baseskip,baseline) -- could be in previous linked
        end
        setlisttransparency(info,c_text)
        info = new_hlist(info)
        --
        setattr(info,a_layer,layer)
        if vertical then
            if shift == 0 then
                info = linked_nodes(current,info)
            elseif trace_origin then
                local size   = 2*size
                local origin = o_cache[size]
                origin = copy_list(origin)
                if getid(parent) == vlist_code then
                    setfield(origin,"shift",-shift)
                    info = linked_nodes(current,new_kern(-size),origin,new_kern(-size),info)
                else
                    -- todo .. i need an example
                    info = linked_nodes(current,info)
                end
                setfield(current,"shift",0)
            else
                info = linked_nodes(current,info)
                setfield(current,"shift",0)
            end
            info = new_vlist(info,wd,ht,dp,shift)
        else
            if shift == 0 then
                info = linked_nodes(current,new_kern(-wd),info)
            elseif trace_origin then
                local size   = 2*size
                local origin = o_cache[size]
                origin = copy_list(origin)
                if getid(parent) == vlist_code then
                    info = linked_nodes(current,new_kern(-wd-size-shift),origin,new_kern(-size+shift),info)
                else
                    setfield(origin,"shift",-shift)
                    info = linked_nodes(current,new_kern(-wd-size),origin,new_kern(-size),info)
                end
                setfield(current,"shift",0)
            else
                info = linked_nodes(current,new_kern(-wd),info)
                setfield(current,"shift",0)
            end
            info = new_hlist(info,wd,ht,dp,shift)
        end

-- how about dir, so maybe just copy the node
--
-- local l = getlist(current)
-- setlist(current,nil)
-- local c = copy_node(current)
-- setlist(current,l)
-- setlist(c,info)
-- info = c

        if next then
            setlink(info,next)
        end
        if prev then
            if getid(prev) == gluespec_code then
                report_visualize("ignoring invalid prev")
                -- weird, how can this happen, an inline glue-spec, probably math
            else
                setlink(prev,info)
            end
        end
        if head == current then
            return info, info
        else
            return head, info
        end
    else
        return head, current
    end
end

local bpfactor =  number.dimenfactors.bp

-- callback.register("process_rule",function(n,h,v)
--     local p = string.formatters["0 0 %0.6F %0.6F re f"](h*bpfactor,v*bpfactor)
--     pdf.print("direct",p)
-- end)

local function ruledglyph(head,current,previous)
    local wd = getfield(current,"width")
 -- local wd = chardata[getfont(current)][getchar(current)].width
    if wd ~= 0 then
        local ht = getfield(current,"height")
        local dp = getfield(current,"depth")
        local next = getnext(current)
        local prev = previous
        setboth(current)
        local linewidth = emwidth/(2*fraction)
        local baseline
     -- if dp ~= 0 and ht ~= 0 then
        if (dp >= 0 and ht >= 0) or (dp <= 0 and ht <= 0) then
            baseline = new_rule(wd-2*linewidth,linewidth,0)
        end
        local doublelinewidth = 2*linewidth
        -- could be a pdf rule (or a user rule now)
        local info = linked_nodes(
            new_rule(linewidth,ht,dp),
            new_rule(wd-doublelinewidth,-dp+linewidth,dp),
            new_rule(linewidth,ht,dp),
            new_kern(-wd+linewidth),
            new_rule(wd-doublelinewidth,ht,-ht+linewidth),
            new_kern(-wd+doublelinewidth),
            baseline
        )
        local char = chardata[getfont(current)][getchar(current)]
        if char and type(char.unicode) == "table" then -- hackery test
            setlistcolor(info,c_ligature)
            setlisttransparency(info,c_ligature_d)
        else
            setlistcolor(info,c_glyph)
            setlisttransparency(info,c_glyph_d)
        end
        info = new_hlist(info)
        setattr(info,a_layer,l_glyph)
        local info = linked_nodes(current,new_kern(-wd),info)
        info = hpack_nodes(info)
        setfield(info,"width",wd)
        if next then
            setlink(info,next)
        end
        if prev then
            setlink(prev,info)
        end
        if head == current then
            return info, info
        else
            return head, info
        end
    else
        return head, current
    end
end

local g_cache_v = { }
local g_cache_h = { }

local tags = {
 -- userskip              = "US",
    lineskip              = "LS",
    baselineskip          = "BS",
    parskip               = "PS",
    abovedisplayskip      = "DA",
    belowdisplayskip      = "DB",
    abovedisplayshortskip = "SA",
    belowdisplayshortskip = "SB",
    leftskip              = "LS",
    rightskip             = "RS",
    topskip               = "TS",
    splittopskip          = "ST",
    tabskip               = "AS",
    spaceskip             = "SS",
    xspaceskip            = "XS",
    parfillskip           = "PF",
    thinmuskip            = "MS",
    medmuskip             = "MM",
    thickmuskip           = "ML",
    leaders               = "NL",
    cleaders              = "CL",
    xleaders              = "XL",
    gleaders              = "GL",
 -- true                  = "VS",
 -- false                 = "HS",
}

-- we sometimes pass previous as we can have issues in math (not watertight for all)

local function ruledglue(head,current,vertical,parent)
    local subtype = getsubtype(current)
    local width   = effectiveglue(current,parent)
    local amount  = formatters["%s:%0.3f"](tags[subtype] or (vertical and "VS") or "HS",width*pt_factor)
    local info    = (vertical and g_cache_v or g_cache_h)[amount]
    if info then
        -- print("glue hit")
    else
        if subtype == space_code or subtype == xspace_code then -- not yet all space
            info = sometext(amount,l_glue,c_space)
        elseif subtype == leftskip_code or subtype == rightskip_code then
            info = sometext(amount,l_glue,c_skip_a)
        elseif subtype == userskip_code then
            if width > 0 then
                info = sometext(amount,l_glue,c_positive)
            elseif width < 0 then
                info = sometext(amount,l_glue,c_negative)
            else
                info = sometext(amount,l_glue,c_zero)
            end
        else
            info = sometext(amount,l_glue,c_skip_b)
        end
        (vertical and g_cache_v or g_cache_h)[amount] = info
    end
    info = copy_list(info)
    if vertical then
        info = vpack_nodes(info)
    end
    head, current = insert_node_before(head,current,info)
    return head, getnext(current)
end

local k_cache_v = { }
local k_cache_h = { }

local function ruledkern(head,current,vertical)
    local kern = getfield(current,"kern")
    local info = (vertical and k_cache_v or k_cache_h)[kern]
    if info then
        -- print("kern hit")
    else
        local amount = formatters["%s:%0.3f"](vertical and "VK" or "HK",kern*pt_factor)
        if kern > 0 then
            info = sometext(amount,l_kern,c_positive)
        elseif kern < 0 then
            info = sometext(amount,l_kern,c_negative)
        else
            info = sometext(amount,l_kern,c_zero)
        end
        (vertical and k_cache_v or k_cache_h)[kern] = info
    end
    info = copy_list(info)
    if vertical then
        info = vpack_nodes(info)
    end
    head, current = insert_node_before(head,current,info)
    return head, getnext(current)
end

local i_cache = { }

local function ruleditalic(head,current)
    local kern = getfield(current,"kern")
    local info = i_cache[kern]
    if info then
        -- print("kern hit")
    else
        local amount = formatters["%s:%0.3f"]("IC",kern*pt_factor)
        if kern > 0 then
            info = sometext(amount,l_kern,c_positive)
        elseif kern < 0 then
            info = sometext(amount,l_kern,c_negative)
        else
            info = sometext(amount,l_kern,c_zero)
        end
        i_cache[kern] = info
    end
    info = copy_list(info)
    head, current = insert_node_before(head,current,info)
    return head, getnext(current)
end

local p_cache_v = { }
local p_cache_h = { }

local function ruledpenalty(head,current,vertical)
    local penalty = getfield(current,"penalty")
    local info = (vertical and p_cache_v or p_cache_h)[penalty]
    if info then
        -- print("penalty hit")
    else
        local amount = formatters["%s:%s"](vertical and "VP" or "HP",penalty)
        if penalty > 0 then
            info = sometext(amount,l_penalty,c_positive)
        elseif penalty < 0 then
            info = sometext(amount,l_penalty,c_negative)
        else
            info = sometext(amount,l_penalty,c_zero)
        end
        (vertical and p_cache_v or p_cache_h)[penalty] = info
    end
    info = copy_list(info)
    if vertical then
        info = vpack_nodes(info)
    elseif raisepenalties then
        setfield(info,"shift",-65536*4)
    end
    head, current = insert_node_before(head,current,info)
    return head, getnext(current)
end

local function visualize(head,vertical,forced,parent)
    local trace_hbox     = false
    local trace_vbox     = false
    local trace_vtop     = false
    local trace_kern     = false
    local trace_glue     = false
    local trace_penalty  = false
    local trace_fontkern = false
    local trace_strut    = false
    local trace_whatsit  = false
    local trace_glyph    = false
    local trace_simple   = false
    local trace_user     = false
    local trace_math     = false
    local trace_italic   = false
    local trace_origin   = false
    local current        = head
    local previous       = nil
    local attr           = unsetvalue
    local prev_trace_fontkern = nil
    while current do
        local id = getid(current)
        local a = forced or getattr(current,a_visual) or unsetvalue
        if a ~= attr then
            prev_trace_fontkern = trace_fontkern
            if a == unsetvalue then
                trace_hbox     = false
                trace_vbox     = false
                trace_vtop     = false
                trace_kern     = false
                trace_glue     = false
                trace_penalty  = false
                trace_fontkern = false
                trace_strut    = false
                trace_whatsit  = false
                trace_glyph    = false
                trace_simple   = false
                trace_user     = false
                trace_math     = false
                trace_italic   = false
                trace_origin   = false
            else -- dead slow:
                trace_hbox     = hasbit(a,    1)
                trace_vbox     = hasbit(a,    2)
                trace_vtop     = hasbit(a,    4)
                trace_kern     = hasbit(a,    8)
                trace_glue     = hasbit(a,   16)
                trace_penalty  = hasbit(a,   32)
                trace_fontkern = hasbit(a,   64)
                trace_strut    = hasbit(a,  128)
                trace_whatsit  = hasbit(a,  256)
                trace_glyph    = hasbit(a,  512)
                trace_simple   = hasbit(a, 1024)
                trace_user     = hasbit(a, 2048)
                trace_math     = hasbit(a, 4096)
                trace_italic   = hasbit(a, 8192)
                trace_origin   = hasbit(a,16384)
            end
            attr = a
        end
        if trace_strut then
            setattr(current,a_layer,l_strut)
        elseif id == glyph_code then
            if trace_glyph then
                head, current = ruledglyph(head,current,previous)
            end
        elseif id == disc_code then
            local pre, post, replace = getdisc(current)
            if pre then
                pre = visualize(pre,false,a,parent)
            end
            if post then
                post = visualize(post,false,a,parent)
            end
            if replace then
                replace = visualize(replace,false,a,parent)
            end
            setdisc(current,pre,post,replace)
        elseif id == kern_code then
            local subtype = getsubtype(current)
            -- tricky ... we don't copy the trace attribute in node-inj (yet)
            if subtype == font_kern_code or getattr(current,a_fontkern) then
                if trace_fontkern or prev_trace_fontkern then
                    head, current = fontkern(head,current)
                end
            else -- if subtype == user_kern_code then
                if trace_italic then
                    head, current = ruleditalic(head,current)
                elseif trace_kern then
                    head, current = ruledkern(head,current,vertical)
                end
            end
        elseif id == glue_code then
            local content = getleader(current)
            if content then
                setleader(current,visualize(content,false,nil,parent))
            elseif trace_glue then
                head, current = ruledglue(head,current,vertical,parent)
            end
        elseif id == penalty_code then
            if trace_penalty then
                head, current = ruledpenalty(head,current,vertical)
            end
        elseif id == hlist_code then
            local content = getlist(current)
            if content then
                setlist(current,visualize(content,false,nil,current))
            end
            if trace_hbox then
                head, current = ruledbox(head,current,false,l_hbox,"H__",trace_simple,previous,trace_origin,parent)
            end
        elseif id == vlist_code then
            local content = getlist(current)
            if content then
                setlist(current,visualize(content,true,nil,current))
            end
            if trace_vtop then
                head, current = ruledbox(head,current,true,l_vtop,"_T_",trace_simple,previous,trace_origin,parent)
            elseif trace_vbox then
                head, current = ruledbox(head,current,true,l_vbox,"__V",trace_simple,previous,trace_origin,parent)
            end
        elseif id == whatsit_code then
            if trace_whatsit then
                head, current = whatsit(head,current)
            end
        elseif id == user_code then
            if trace_user then
                head, current = user(head,current)
            end
        elseif id == math_code then
            if trace_math then
                head, current = math(head,current)
            end
        end
        previous = current
        current  = getnext(current)
    end
    return head
end

do

    local function freed(cache)
        local n = 0
        for k, v in next, cache do
            free_node_list(v)
            n = n + 1
        end
        if n == 0 then
            return 0, cache
        else
            return n, { }
        end
    end

    local function cleanup()
        local hf, nw, nb, ng_v, ng_h, np_v, np_h, nk_v, nk_h
        nf,   f_cache   = freed(f_cache)
        nw,   w_cache   = freed(w_cache)
        nb,   b_cache   = freed(b_cache)
        no,   o_cache   = freed(o_cache)
        ng_v, g_cache_v = freed(g_cache_v)
        ng_h, g_cache_h = freed(g_cache_h)
        np_v, p_cache_v = freed(p_cache_v)
        np_h, p_cache_h = freed(p_cache_h)
        nk_v, k_cache_v = freed(k_cache_v)
        nk_h, k_cache_h = freed(k_cache_h)
     -- report_visualize("cache cleanup: %s fontkerns, %s skips, %s penalties, %s kerns, %s whatsits, %s boxes, %s origins",
     --     nf,ng_v+ng_h,np_v+np_h,nk_v+nk_h,nw,nb,no)
    end

    local function handler(head)
        if usedfont then
            starttiming(visualizers)
         -- local l = texgetattribute(a_layer)
         -- local v = texgetattribute(a_visual)
         -- texsetattribute(a_layer,unsetvalue)
         -- texsetattribute(a_visual,unsetvalue)
            head = visualize(tonut(head),true)
         -- texsetattribute(a_layer,l)
         -- texsetattribute(a_visual,v)
         -- -- cleanup()
            stoptiming(visualizers)
            return tonode(head), true
        else
            return head, false
        end
    end

    visualizers.handler = handler

    luatex.registerstopactions(cleanup)

end

function visualizers.box(n)
    if usedfont then
        starttiming(visualizers)
        local box = getbox(n)
        if box then
            setlist(box,visualize(getlist(box),getid(box) == vlist_code))
        end
        stoptiming(visualizers)
        return head, true
    else
        return head, false
    end
end

do

    local last = nil
    local used = nil

    local mark = {
        "trace:1", "trace:2", "trace:3",
        "trace:4", "trace:5", "trace:6",
        "trace:7",
    }

    local function markfonts(list)
        for n in traverse_nodes(list) do
            local id = getid(n)
            if id == glyph_code then
                local font = getfont(n)
                local okay = used[font]
                if not okay then
                    last = last + 1
                    okay = mark[last]
                    used[font] = okay
                end
                setcolor(n,okay)
            elseif id == hlist_code or id == vlist_code then
                markfonts(getlist(n))
            end
        end
    end

    function visualizers.markfonts(list)
        last, used = 0, { }
        markfonts(type(n) == "number" and getlist(getbox(n)) or n)
    end

end

statistics.register("visualization time",function()
    if enabled then
     -- cleanup() -- in case we don't don't do it each time
        return format("%s seconds",statistics.elapsedtime(visualizers))
    end
end)

-- interface

local implement = interfaces.implement

implement { name = "setvisual",       arguments = "string",  actions = visualizers.setvisual }
implement { name = "getvisual",       arguments = "string",  actions = { setvisual, context } }
implement { name = "setvisuallayer",  arguments = "string",  actions = visualizers.setlayer }
implement { name = "markvisualfonts", arguments = "integer", actions = visualizers.markfonts }
implement { name = "setvisualfont",   arguments = "integer", actions = visualizers.setfont }

