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

-- This is a playground, a byproduct of some experiments in a project where
-- we needed something like this where it works ok, but nevertheless it's
-- still experimental code. It is very likely to change (or extended).

local unpack, rawget = unpack, rawget

local formatters        = string.formatters

local nodecodes         = nodes.nodecodes
local gluecodes         = nodes.gluecodes
local listcodes         = nodes.listcodes

local glyph_code        = nodecodes.glyph
local disc_code         = nodecodes.disc
local kern_code         = nodecodes.kern
local penalty_code      = nodecodes.penalty
local glue_code         = nodecodes.glue
local hlist_code        = nodecodes.hlist
local vlist_code        = nodecodes.vlist
local unset_code        = nodecodes.unset
local math_code         = nodecodes.math
local rule_code         = nodecodes.rule
local marginkern_code   = nodecodes.marginkern

local leaders_code      = gluecodes.leaders
local lineskip_code     = gluecodes.lineskip
local baselineskip_code = gluecodes.baselineskip
local line_code         = listcodes.line
local parskip_code      = listcodes.parskip

local texlists          = tex.lists
local gettexdimen       = tex.getdimen
local settexattribute   = tex.setattribute
local settexbox         = tex.setbox
local taketexbox        = tex.takebox

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

local getfield          = nuts.getfield
local getattr           = nuts.getattr
local getid             = nuts.getid
local getnext           = nuts.getnext
local getprev           = nuts.getprev
local getsubtype        = nuts.getsubtype
local getlist           = nuts.getlist
local gettexbox         = nuts.getbox

local setfield          = nuts.setfield
local setlink           = nuts.setlink
local setlist           = nuts.setlist
local setattr           = nuts.setattr

local theprop           = nuts.theprop

local floor             = math.floor
local ceiling           = math.ceil

local new_rule          = nuts.pool.rule
local new_glue          = nuts.pool.glue
local new_kern          = nuts.pool.kern
local hpack_nodes       = nuts.hpack
local link_nodes        = nuts.link
local find_node_tail    = nuts.tail
local setglue           = nuts.setglue

local properties        = nodes.properties.data

local a_visual          = attributes.private("visual")
local a_snapmethod      = attributes.private("snapmethod")
local a_profilemethod   = attributes.private("profilemethod")
local a_specialcontent  = attributes.private("specialcontent")

local variables         = interfaces.variables
local v_none            = variables.none
local v_fixed           = variables.fixed
local v_strict          = variables.strict

local setcolor          = nodes.tracers.colors.set
local settransparency   = nodes.tracers.transparencies.set

local profiling         = { }
builders.profiling      = profiling

local report            = logs.reporter("profiling")

local show_profile      = false  trackers.register("profiling.show", function(v) show_profile  = v end)
local trace_profile     = false  trackers.register("profiling.trace",function(v) trace_profile = v end)

local function getprofile(line,step)

    -- only l2r
    -- no hz yet

    local line    = tonut(line)
    local current = getlist(line)

    if not current then
        return
    end

    local glue_sign  = getfield(line,"glue_sign")
    local glue_order = getfield(line,"glue_order")
    local glue_set   = getfield(line,"glue_set")

    local heights  = { }
    local depths   = { }
    local width    = 0
    local position = 0
    local step     = step or 65536 -- * 2 -- 2pt
    local margin   = step / 4
    local min      = 0
    local max      = ceiling(getfield(line,"width")/step) + 1

    for i=min,max do
        heights[i] = 0
        depths [i] = 0
    end

    -- remember p

    local wd, ht, dp = 0, 0, 0

    local function progress()
        position = width
        width    = position + wd
            p = floor((position - margin)/step + 0.5)
            w = floor((width    + margin)/step - 0.5)
        if p < 0 then
            p = 0
        end
        if w < 0 then
            w = 0
        end
        if p > w then
            w, p = p, w
        end
        if w > max then
            for i=max+1,w+1 do
                heights[i] = 0
                depths [i] = 0
            end
            max = w
        end
        for i=p,w do
            if ht > heights[i] then
                heights[i] = ht
            end
            if dp > depths[i] then
                depths[i] = dp
            end
        end
    end

    local function process(current) -- called nested in disc replace
        while current do
            local id = getid(current)
            if id == glyph_code then
                wd = getfield(current,"width")
                ht = getfield(current,"height")
                dp = getfield(current,"depth")
                progress()
            elseif id == kern_code then
                wd = getfield(current,"kern")
                ht = 0
                dp = 0
                progress()
            elseif id == disc_code then
                local replace = getfield(current,"replace")
                if replace then
                    process(replace)
                end
            elseif id == glue_code then
                wd = getfield(current,"width")
                if glue_sign == 1 then
                    if getfield(current,"stretch_order") == glue_order then
                        wd = wd + getfield(current,"stretch") * glue_set
                    end
                elseif glue_sign == 2 then
                    if getfield(current,"shrink_order") == glue_order then
                        wd = wd - getfield(current,"shrink") * glue_set
                    end
                end
                if getsubtype(current) >= leaders_code then
                    local leader = getleader(current)
                    ht = getfield(leader,"height")
                    dp = getfield(leader,"depth")
                else
                    ht = 0
                    dp = 0
                end
                progress()
            elseif id == hlist_code then
                -- we could do a nested check .. but then we need to push / pop glue
                local shift = getfield(current,"shift")
                wd = getfield(current,"width")
                if getattr(current,a_specialcontent) then
                    -- like a margin note, maybe check for wd
                    ht = 0
                    dp = 0
                else
                    ht = getfield(current,"height") - shift
                    dp = getfield(current,"depth")  + shift
                end
                progress()
            elseif id == vlist_code or id == unset_code then
                local shift = getfield(current,"shift") -- todo
                wd = getfield(current,"width")
                ht = getfield(current,"height") -- - shift
                dp = getfield(current,"depth")  -- + shift
                progress()
            elseif id == rule_code then
                wd = getfield(current,"width")
                ht = getfield(current,"height")
                dp = getfield(current,"depth")
                progress()
            elseif id == math_code then
                wd = getfield(current,"surround")
                ht = 0
                dp = 0
                progress()
            elseif id == marginkern_code then
                wd = getfield(current,"width")
                ht = 0
                dp = 0
                progress()
            else
--     print(nodecodes[id])
            end
            current = getnext(current)
        end
    end

    process(current)

    return {
        heights = heights,
        depths  = depths,
        min     = min, -- not needed
        max     = max,
        step    = step,
    }

end

profiling.get = getprofile

local function getpagelist()
    local pagehead = texlists.page_head
    if pagehead then
        pagehead = tonut(texlists.page_head)
        pagetail = find_node_tail(pagehead)
    else
        pagetail = nil
    end
    return pagehead, pagetail
end

local function setprofile(n,step)
    local p = rawget(properties,n)
    if p then
        local pp = p.profile
        if not pp then
            pp = getprofile(n,step)
            p.profile = pp
        end
        return pp
    else
        local pp = getprofile(n,step)
        properties[n] = { profile = pp }
        return pp
    end
end

local function hasprofile(n)
    local p = rawget(properties,n)
    if p then
        return p.profile
    end
end

local function addstring(height,depth)
    local typesetters = nuts.typesetters
    local hashes   = fonts.hashes
    local infofont = fonts.infofont()
    local emwidth  = hashes.emwidths [infofont]
    local exheight = hashes.exheights[infofont]
    local httext   = height
    local dptext   = depth
    local httext   = typesetters.tohpack(height,infofont)
    local dptext   = typesetters.tohpack(depth,infofont)
    setfield(httext,"shift",- 1.2 * exheight)
    setfield(dptext,"shift",  0.6 * exheight)
    local text = nuts.hpack(
        nuts.linked(
            new_kern(-getfield(httext,"width")-emwidth),
            httext,
            new_kern(-getfield(dptext,"width")),
            dptext
        )
    )
    setfield(text,"height",0)
    setfield(text,"depth",0)
    setfield(text,"width",0)
    return text
end

local function addprofile(node,profile,step)

    local line = tonut(node)

    if not profile then
        profile = setprofile(line,step)
    end

    if not profile then
        report("some error")
        return node
    end

    if profile.shown then
        return node
    end

    local list    = getlist(line)
    profile.shown = true

    local heights = profile.heights
    local depths  = profile.depths
    local step    = profile.step

    local head    = nil
    local tail    = nil

    local lastht  = 0
    local lastdp  = 0
    local lastwd  = 0

    local visual  = "f:s:t" -- this can change !

    local function progress()
        if lastwd == 0 then
            return
        end
        local what = nil
        if lastht == 0 and lastdp == 0 then
            what = new_kern(lastwd)
        else
            what = new_rule(lastwd,lastht,lastdp)
            setcolor(what,visual)
            settransparency(what,visual)
        end
        if tail then
            setlink(tail,what)
        else
            head = what
        end
        tail = what
    end

-- inspect(profile)

    for i=profile.min,profile.max do
        local ht = heights[i]
        local dp = depths[i]
        if ht ~= lastht or dp ~= lastdp and lastwd > 0 then
            progress()
            lastht = ht
            lastdp = dp
            lastwd = step
        else
            lastwd = lastwd + step
        end
    end
    if lastwd > 0 then
        progress()
    end

    local rule = hpack_nodes(head)

    setfield(rule,"width", 0)
    setfield(rule,"height",0)
    setfield(rule,"depth", 0)

 -- if texttoo then
 --
 --     local text = addstring(
 --         formatters["%0.4f"](getfield(rule,"height")/65536),
 --         formatters["%0.4f"](getfield(rule,"depth") /65536)
 --     )
 --
 --     setlink(text,rule)
 --
 --     rule = text
 --
 -- end

    setlink(rule,list)
    setlist(line,rule)

end

profiling.add = addprofile

local methods = { }

local function getdelta(t_profile,b_profile)
    local t_heights  = t_profile.heights
    local t_depths   = t_profile.depths
    local t_max      = t_profile.max
    local b_heights  = b_profile.heights
    local b_depths   = b_profile.depths
    local b_max      = b_profile.max

    local max        = t_max
    local delta      = 0

    if t_max > b_max then
        for i=b_max+1,t_max do
            b_depths [i] = 0
            b_heights[i] = 0
        end
        max = t_max
    elseif b_max > t_max then
        for i=t_max+1,b_max do
            t_depths [i] = 0
            t_heights[i] = 0
        end
        max = b_max
    end

    for i=0,max do
        local ht = b_heights[i]
        local dp = t_depths[i]
        local hd = ht + dp
        if hd > delta then
            delta = hd
        end
    end

    return delta
end

-- local properties = theprop(bot)
-- local unprofiled = properties.unprofiled
-- if not unprofiled then -- experiment
--     properties.unprofiled = {
--         height  = height,
--         strutht = strutht,
--     }
-- end

-- lineskip | lineskiplimit

local function inject(top,bot,amount) -- todo: look at penalties
    local glue = new_glue(amount)
    --
    setattr(glue,a_profilemethod,0)
    setattr(glue,a_visual,getattr(top,a_visual))
    --
    setlink(glue,bot)
    setlink(top,glue)
end

methods[v_none] = function()
    return false
end

methods[v_strict] = function(top,bot,t_profile,b_profile,specification)

    local top        = tonut(top)
    local bot        = tonut(bot)

    local strutht    = specification.height or texdimen.strutht
    local strutdp    = specification.depth  or texdimen.strutdp
    local lineheight = strutht + strutdp

    local depth      = getfield(top,"depth")
    local height     = getfield(bot,"height")
    local total      = depth + height
    local distance   = specification.distance or 0
    local delta      = lineheight - total

    -- there is enough room between the lines so we don't need
    -- to add extra distance

    if delta >= distance then
        inject(top,bot,delta)
        return true
    end

    local delta = getdelta(t_profile,b_profile)
    local skip  = delta - total + distance

    -- we don't want to be too tight so we limit the skip and
    -- make sure we have at least lineheight

    inject(top,bot,skip)
    return true

end

-- todo: also set ht/dp of first / last (but what is that)

methods[v_fixed] = function(top,bot,t_profile,b_profile,specification)

    local top        = tonut(top)
    local bot        = tonut(bot)

    local strutht    = specification.height or texdimen.strutht
    local strutdp    = specification.depth  or texdimen.strutdp
    local lineheight = strutht + strutdp

    local depth      = getfield(top,"depth")
    local height     = getfield(bot,"height")
    local total      = depth + height
    local distance   = specification.distance or 0
    local delta      = lineheight - total

    local snapmethod = getattr(top,a_snapmethod)

    if snapmethod then

        -- no distance (yet)

        if delta < lineheight then
            setfield(top,"depth",strutdp)
            setfield(bot,"height",strutht)
            return true
        end

        local delta  = getdelta(t_profile,b_profile)

        local dp = strutdp
        while depth > lineheight - strutdp do
            depth = depth - lineheight
            dp = dp + lineheight
        end
        setfield(top,"depth",dp)
        local ht = strutht
        while height > lineheight - strutht do
            height = height - lineheight
            ht = ht + lineheight
        end
        setfield(bot,"height",ht)
        local lines = floor(delta/lineheight)
        if lines > 0 then
            inject(top,bot,-lines * lineheight)
        end

        return true

    end

    if total < lineheight then
        setfield(top,"depth",strutdp)
        setfield(bot,"height",strutht)
        return true
    end

    if depth < strutdp then
        setfield(top,"depth",strutdp)
        total = total - depth + strutdp
    end
    if height < strutht then
        setfield(bot,"height",strutht)
        total = total - height + strutht
    end

    local delta      = getdelta(t_profile,b_profile)

    local target     = total - delta
    local factor     = specification.factor or 1
    local step       = lineheight / factor
    local correction = 0
    local nofsteps   = 0
    while correction < target - step - distance do -- a loop is more accurate, for now
        correction = correction + step
        nofsteps   = nofsteps + 1
    end

    if trace_profile then
        report("top line     : %s %05i > %s",t_profile.shown and "+" or "-",top,nodes.toutf(getlist(top)))
        report("bottom line  : %s %05i > %s",b_profile.shown and "+" or "-",bot,nodes.toutf(getlist(bot)))
        report("  depth      : %p",depth)
        report("  height     : %p",height)
        report("  total      : %p",total)
        report("  lineheight : %p",lineheight)
        report("  delta      : %p",delta)
        report("  target     : %p",target)
        report("  factor     : %i",factor)
        report("  distance   : %p",distance)
        report("  step       : %p",step)
        report("  nofsteps   : %i",nofsteps)
     -- report("  max lines  : %s",lines == 0 and "unset" or lines)
        report("  correction : %p",correction)
    end

    inject(top,bot,-correction) -- we could mess with the present glue (if present)

    return true -- remove interlineglue

end

function profiling.distance(top,bot,specification)
    local step   = specification.step
    local method = specification.method
    local ptop   = getprofile(top,step)
    local pbot   = getprofile(bot,step)
    local action = methods[method or v_strict] or methods[v_strict]
    return action(top,bot,ptop,pbot,specification)
end

local specifications = { } -- todo: save these !

function profiling.fixedprofile(current)
    local a = getattr(current,a_profilemethod)
    if a then
        local s = specifications[a]
        if s then
            return s.method == v_fixed
        end
    end
    return false
end

local function profilelist(line,mvl)

    local current       = tonut(line)

    local top           = nil
    local bot           = nil

    local t_profile     = nil
    local b_profile     = nil

    local specification = nil
    local lastattr      = nil
    local method        = nil
    local action        = nil

    local distance      = 0
    local lastglue      = nil

    local pagehead      = nil
    local pagetail      = nil

    if mvl then

        pagehead, pagetail = getpagelist()

        if pagetail then
            local current = pagetail
            while current do
                local id = getid(current)
                if id == hlist_code then
                    local subtype = getsubtype(current)
                    if subtype == line_code then
                        t_profile = hasprofile(current)
                        if t_profile then
                            top = current
                        end
                    end
                    break
                elseif id == glue_code then
                    local wd = getfield(current,"width")
                    if not wd or wd == 0 then
                        -- go on
                    else
                        break
                    end
                elseif id == penalty_code then
                    -- ok
                else
                    break
                end
                current = getnext(current)
            end
        end

    end

    while current do

        local attr = getattr(current,a_profilemethod)

        if attr then

            if attr ~= lastattr then
                specification = specifications[attr]
                method        = specification and specification.method
                action        = method and methods[method] or methods[v_strict]
                lastattr      = attr
            end

            local id = getid(current)

            if id == hlist_code then -- check subtype
                local subtype = getsubtype(current)
                if subtype == line_code then
                    if top == current then
                        -- skip
                        bot = nil -- to be sure
                    elseif top then
                        bot       = current
                        b_profile = setprofile(bot)
                        if show_profile then
                            addprofile(bot,b_profile)
                        end
                        if not t_profile.done then
                            if action then
                                local ok = action(top,bot,t_profile,b_profile,specification)
                                if ok and lastglue and distance ~= 0 then
                                    setglue(lastglue)
                                end
                            end
                            t_profile.done = true
                        end
                        top       = bot
                        bot       = nil
                        t_profile = b_profile
                        b_profile = nil
                        distance  = 0
                    else
                        top       = current
                        t_profile = setprofile(top)
                        bot       = nil
                        if show_profile then
                            addprofile(top,t_profile)
                        end
                    end
                else
                    top = nil
                    bot = nil
                end
            elseif id == glue_code then
                if top then
                    local subtype = getsubtype(current)
                 -- if subtype == lineskip_code or subtype == baselineskip_code then
                        local wd   = getfield(current,"width")
                        if wd > 0 then
                            distance = wd
                            lastglue = current
                        elseif wd < 0 then
                            top = nil
                            bot = nil
                        else
                            -- ok
                        end
                 -- else
                 --     top = nil
                 --     bot = nil
                 -- end
                else
                    top = nil
                    bot = nil
                end
            elseif id == penalty_code then
                -- okay
            else
                top = nil
                bot = nil
            end
        else
            top = nil
            bot = nil
        end
        current = getnext(current)
    end
    if top then
        t_profile = setprofile(top)
        if show_profile then
            addprofile(top,t_profile)
        end
    end
end

profiling.list = profilelist

local enabled = false

function profiling.set(specification)
    if not enabled then
        nodes.tasks.enableaction("mvlbuilders", "builders.profiling.pagehandler")
     -- too expensive so we expect that this happens explicitly, we keep for reference:
     -- nodes.tasks.enableaction("vboxbuilders","builders.profiling.vboxhandler")
        enabled = true
    end
    local n = #specifications + 1
    specifications[n] = specification
    settexattribute(a_profilemethod,n)
end

function profiling.profilebox(specification)
    local boxnumber = specification.box
    local current   = getlist(gettexbox(boxnumber))
    local top       = nil
    local bot       = nil
    local t_profile = nil
    local b_profile = nil
    local method    = specification and specification.method
    local action    = method and methods[method] or methods[v_strict]
    local lastglue  = nil
    local distance  = 0
    while current do
        local id = getid(current)
        if id == hlist_code then
            local subtype = getsubtype(current)
            if subtype == line_code then
                if top then
                    bot       = current
                    b_profile = setprofile(bot)
                    if show_profile then
                        addprofile(bot,b_profile)
                    end
                    if not t_profile.done then
                        if action then
                            local ok = action(top,bot,t_profile,b_profile,specification)
                            if ok and lastglue and distance ~= 0 then
                                setglue(lastglue)
                            end
                        end
                        t_profile.done = true
                    end
                    top       = bot
                    t_profile = b_profile
                    b_profile = nil
                    distance  = 0
                else
                    top       = current
                    t_profile = setprofile(top)
                    if show_profile then
                        addprofile(top,t_profile)
                    end
                    bot       = nil
                end
            else
                top = nil
                bot = nil
            end
        elseif id == glue_code then
            local subtype = getsubtype(current)
            if subtype == lineskip_code or subtype == baselineskip_code then
                if top then
                    local wd   = getfield(current,"width")
                    if wd > 0 then
                        distance = wd
                        lastglue = current
                    elseif wd < 0 then
                        top = nil
                        bot = nil
                    else
                        -- ok
                    end
                else
                    top = nil
                    bot = nil
                end
            else
                top = nil
                bot = nil
            end
        elseif id == penalty_code then
            -- okay
        else
            top = nil
            bot = nil
        end
        current = getnext(current)
    end

    if top then
        t_profile = setprofile(top) -- not needed
        if show_profile then
            addprofile(top,t_profile)
        end
    end

end

local ignore = table.tohash {
    "split_keep",
    "split_off",
 -- "vbox",
}

-- function profiling.vboxhandler(head,where)
--     if head and not ignore[where] then
--         local h = tonut(head)
--         if getnext(h) then
--             profilelist(h)
--         end
--     end
--     return head
-- end

function profiling.pagehandler(head)
    if head then
        profilelist(head,true)
    end
    return head, true
end

interfaces.implement {
    name      = "setprofile",
    actions   = profiling.set,
    arguments = {
        {
            { "name" },
            { "height", "dimen" },
            { "depth", "dimen" },
            { "distance", "dimen" },
            { "factor", "integer" },
            { "lines", "integer" },
            { "method" }
        }
    }
}

interfaces.implement {
    name      = "profilebox",
    actions   = profiling.profilebox,
    arguments = {
        {
            { "box", "integer" },
            { "height", "dimen" },
            { "depth", "dimen" },
            { "distance", "dimen" },
            { "factor", "integer" },
            { "lines", "integer" },
            { "method" }
        }
    }
}
