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

-- This code dates from the beginning (just after 2005) and and need a bit of a
-- rewrite: smaller blobs, less hassle with the 200 locals limit, maybe use some
-- more luametatex magic. But it being critial code we have to be careful so this
-- will be done stepwise.

local next, type, tonumber = next, type, tonumber
local gmatch = string.gmatch
local formatters = string.formatters

local nodes        =  nodes
local trackers     =  trackers
local attributes   =  attributes
----- context      =  context
local tex          =  tex

local texget       = tex.get
local texgetcount  = tex.getcount
local texgetdimen  = tex.getdimen
local texgetglue   = tex.getglue
local texset       = tex.set
local texsetdimen  = tex.setdimen
local texsetcount  = tex.setcount
local texgetbox    = tex.getbox

local texgetlist   = tex.getlist
local texsetlist   = tex.setlist
local texgetnest   = tex.getnest

local variables    = interfaces.variables

local v_local      <const> = variables["local"]
local v_global     <const> = variables["global"]
local v_box        <const> = variables.box
local v_split      <const> = variables.split
local v_min        <const> = variables.min
local v_max        <const> = variables.max
local v_none       <const> = variables.none
local v_first      <const> = variables.first
local v_last       <const> = variables.last
local v_top        <const> = variables.top
local v_bottom     <const> = variables.bottom
local v_maxheight  <const> = variables.maxheight
local v_minheight  <const> = variables.minheight
local v_mindepth   <const> = variables.mindepth
local v_maxdepth   <const> = variables.maxdepth
local v_offset     <const> = variables.offset
local v_strut      <const> = variables.strut

local v_hfraction  <const> = variables.hfraction
local v_dfraction  <const> = variables.dfraction
local v_bfraction  <const> = variables.bfraction
local v_tlines     <const> = variables.tlines
local v_blines     <const> = variables.blines

-- vertical space handler

local trace_vbox_vspacing    = false  trackers.register("vspacing.vbox",     function(v) trace_vbox_vspacing    = v end)
local trace_page_vspacing    = false  trackers.register("vspacing.page",     function(v) trace_page_vspacing    = v end)
local trace_collect_vspacing = false  trackers.register("vspacing.collect",  function(v) trace_collect_vspacing = v end)
local trace_vspacing         = false  trackers.register("vspacing.spacing",  function(v) trace_vspacing         = v end)
local trace_vsnapping        = false  trackers.register("vspacing.snapping", function(v) trace_vsnapping        = v end)
local trace_specials         = false  trackers.register("vspacing.specials", function(v) trace_specials         = v end)

----- remove_math_skips      = true   directives.register("vspacing.removemathskips", function(v) remnove_math_skips = v end)

local report_vspacing  = logs.reporter("vspacing","spacing")
local report_collapser = logs.reporter("vspacing","collapsing")
local report_snapper   = logs.reporter("vspacing","snapping")
local report_specials  = logs.reporter("vspacing","specials")

local a_skipcategory <const> = attributes.private('skipcategory')
local a_skippenalty  <const> = attributes.private('skippenalty')
local a_skiporder    <const> = attributes.private('skiporder')
local a_snapmethod   <const> = attributes.private('snapmethod')
local a_snapvbox     <const> = attributes.private('snapvbox')
local a_snaprange    <const> = attributes.private('snaprange')

local d_bodyfontstrutheight       <const> = tex.isdimen("bodyfontstrutheight")
local d_bodyfontstrutdepth        <const> = tex.isdimen("bodyfontstrutdepth")
local d_globalbodyfontstrutheight <const> = tex.isdimen("globalbodyfontstrutheight")
local d_globalbodyfontstrutdepth  <const> = tex.isdimen("globalbodyfontstrutdepth")
----- d_strutht                   <const> = tex.isdimen("strutht")
local d_strutdp                   <const> = tex.isdimen("strutdp")
local d_spac_overlay              <const> = tex.isdimen("d_spac_overlay")

local c_spac_vspacing_ignore_parskip <const> = tex.iscount("c_spac_vspacing_ignore_parskip")

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

local getnext             = nuts.getnext
local setlink             = nuts.setlink
local getprev             = nuts.getprev
local getid               = nuts.getid
local getlist             = nuts.getlist
local setlist             = nuts.setlist
local getattr             = nuts.getattr
local setattr             = nuts.setattr
local getsubtype          = nuts.getsubtype
local getbox              = nuts.getbox
local getwhd              = nuts.getwhd
local setwhd              = nuts.setwhd
local getprop             = nuts.getprop
local setprop             = nuts.setprop
local getglue             = nuts.getglue
local setglue             = nuts.setglue
local getkern             = nuts.getkern
local getpenalty          = nuts.getpenalty
local setoffsets          = nuts.setoffsets
local setwidth            = nuts.setwidth
local getwidth            = nuts.getwidth
local setheight           = nuts.setheight
local getheight           = nuts.getheight
local setdepth            = nuts.setdepth
local getdepth            = nuts.getdepth
local setnext             = nuts.setnext
local setprev             = nuts.setprev
local setoptions          = nuts.setoptions
local hasidsubtype        = nuts.hasidsubtype

local issnapped           = nuts.issnapped
local setsnapped          = nuts.setsnapped

local find_node_tail      = nuts.tail
local flushnode           = nuts.flushnode
local remove_node         = nuts.remove
local count_nodes         = nuts.countall
local hpack_node          = nuts.hpack
local vpack_node          = nuts.vpack

local startofpar          = nuts.startofpar

local write_node          = nuts.write

local nextnode            = nuts.traversers.node
local nexthlist           = nuts.traversers.hlist

local listtoutf           = nodes.listtoutf
local nodeidstostring     = nodes.idstostring

local nodepool            = nuts.pool

local new_penalty         = nodepool.penalty
local new_kern            = nodepool.kern
local new_glue            = nodepool.glue
local new_rule            = nodepool.rule

local nodecodes           = nodes.nodecodes
local gluecodes           = nodes.gluecodes
----- penaltycodes        = nodes.penaltycodes
----- listcodes           = nodes.listcodes

local penalty_code        <const> = nodecodes.penalty
local kern_code           <const> = nodecodes.kern
local glue_code           <const> = nodecodes.glue
local hlist_code          <const> = nodecodes.hlist
local vlist_code          <const> = nodecodes.vlist
local rule_code           <const> = nodecodes.rule
local par_code            <const> = nodecodes.par
----- boundary_code       <const> = nodecodes.boundary

local userskip_code       <const> = gluecodes.userskip
local lineskip_code       <const> = gluecodes.lineskip
local baselineskip_code   <const> = gluecodes.baselineskip
local parskip_code        <const> = gluecodes.parskip
local topskip_code        <const> = gluecodes.topskip
local splittopskip_code   <const> = gluecodes.splittopskip

local linelist_code       <const> = nodes.listcodes.line

local function setvisual(...)
    setvisual = nuts.setvisual
    return setvisual(...)
end

local properties          = nodes.properties.data

local vspacing            = builders.vspacing or { }
builders.vspacing         = vspacing

local vspacingdata        = vspacing.data or { }
vspacing.data             = vspacingdata

local snapmethods         = vspacingdata.snapmethods or { }
vspacingdata.snapmethods  = snapmethods

storage.register("builders/vspacing/data/snapmethods", snapmethods, "builders.vspacing.data.snapmethods")

do

    local todimen       = string.todimen

    local lpegmatch     = lpeg.match
    local colonsplitter = lpeg.splitat(":")

    local default = {
        [v_maxheight] = true,
        [v_maxdepth]  = true,
        [v_strut]     = true,
        [v_hfraction] = 1,
        [v_dfraction] = 1,
        [v_bfraction] = 0.25,
    }

    local fractions = {
        [v_minheight] = v_hfraction, [v_maxheight] = v_hfraction,
        [v_mindepth]  = v_dfraction, [v_maxdepth]  = v_dfraction,
        [v_box]       = v_bfraction,
        [v_top]       = v_tlines,    [v_bottom]    = v_blines,
    }

    local values = {
        offset = "offset"
    }

    local function listtohash(str)
        local t = { }
        for s in gmatch(str,"[^, ]+") do
            local key, detail = lpegmatch(colonsplitter,s)
            local v = variables[key]
            if v then
                t[v] = true
                if detail then
                    local k = fractions[key]
                    if k then
                        detail = tonumber("0" .. detail)
                        if detail then
                            t[k] = detail
                        end
                    else
                        k = values[key]
                        if k then
                            detail = todimen(detail)
                            if detail then
                                t[k] = detail
                            end
                        end
                    end
                end
            else
                detail = tonumber("0" .. key)
                if detail then
                    t[v_hfraction] = detail
                    t[v_dfraction] = detail
                end
            end
        end
        if next(t) then
            t[v_hfraction] = t[v_hfraction] or 1
            t[v_dfraction] = t[v_dfraction] or 1
            return t
        else
            return default
        end
    end

    function vspacing.definesnapmethod(name,method)
        local n = #snapmethods + 1
        local t = listtohash(method)
        snapmethods[n] = t
        t.name          = name   -- not interfaced
        t.specification = method -- not interfaced
        return n
    end

end

local function validvbox(parentid,list)
    if parentid == hlist_code then
        local id = getid(list)
        if id == par_code and startofpar(list) then
            list = getnext(list)
            if not next then
                return nil
            end
        end
        local done = nil
        for n, id in nextnode, list do
            if id == vlist_code or id == hlist_code then
                if done then
                    return nil
                else
                    done = n
                end
            elseif id == glue_code or id == penalty_code then
                -- go on
            else
                return nil -- whatever
            end
        end
        if done then
            local id = getid(done)
            if id == hlist_code then
                return validvbox(id,getlist(done))
            end
        end
        return done -- only one vbox
    end
end

local function already_done(parentid,list,a_snapmethod) -- todo: done when only boxes and all snapped
    -- problem: any snapped vbox ends up in a line
    if list and parentid == hlist_code then
        local id = getid(list)
        if id == par_code and startofpar(list) then
            list = getnext(list)
            if not list then
                return false
            end
        end
        for n, id in nextnode, list do
            if id == hlist_code or id == vlist_code then
             -- local a = getattr(n,a_snapmethod)
             -- if not a then
             --  -- return true -- not snapped at all
             -- elseif a == 0 then
             --     return true -- already snapped
             -- end
                local p = getprop(n,"snapper")
                if p then
                    return p
                end
            elseif id == glue_code or id == penalty_code then -- or id == kern_code then
                -- go on
            else
                return false -- whatever
            end
        end
    end
    return false
end

-- check variables.none etc

local snap_hlist  do

    local rangesnap

    do

        local upper = string.upper
        local remove = table.remove

        local snapranges = { }
        local snapremap  = {
              ["Q"] = "-Q",  ["H"] = "-H",
              ["L"] = "-L",  ["T"] = "-T",
             ["-I"] =  "I", ["+I"] =  "I",
             ["-Z"] =  "Z", ["+Z"] =  "Z",
              ["0"] =  "Z", ["+0"] =  "Z", ["-0"] =  "Z",
              ["1"] = "-L", ["+1"] = "+L", ["-1"] = "-L",
              ["2"] = "-T", ["+2"] = "+T", ["-2"] = "-T",
        }

        interfaces.implement {
            name      = "registersnaprange",
            arguments = { "integer", "string" },
            actions   = function(index,str)
                local list = { }
                for s in gmatch(upper(str),"[+-]?[QHLZIT012]") do
                    list[#list+1] = snapremap[s] or s
                end
                snapranges[index] = list
            end,
        }

        rangesnap = function(r,d,v)
            local s = snapranges[r]
            if s then
                s = remove(s,1)
                if not s then
                elseif s == "-T" then return -d*2 -- minus  two     lines
                elseif s == "-L" then return -d   -- minus  one     line
                elseif s == "-H" then return -d/2 -- minus  half    line
                elseif s == "-Q" then return -d/4 -- minus  quarter line
                elseif s == "+T" then return  d*2 -- plus   two     lines
                elseif s == "+L" then return  d   -- plus   one     line
                elseif s == "+H" then return  d/2 -- plus   half    line
                elseif s == "+Q" then return  d/4 -- plus   quarter line
                elseif s ==  "Z" then return -v   -- zero   ht/dp
                elseif s ==  "I" then return  0   -- ignore entry
                end
            end
            return 0
        end

    end

    local v_noheight   <const> = variables.noheight
    local v_nodepth    <const> = variables.nodepth
    local v_line       <const> = variables.line
    local v_halfline   <const> = variables.halfline
    local v_line_m     <const> = "-" .. v_line
    local v_halfline_m <const> = "-" .. v_halfline

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

    local function fixedprofile(current)
        local profiling = builders.profiling
        return profiling and profiling.fixedprofile(current)
    end

    -- quite tricky: ceil(-something) => -0

    local function ceiled(n)
        if n < 0 or n < 0.01 then
            return 0
        else
            return ceil(n)
        end
    end

    local function floored(n)
        if n < 0 or n < 0.01 then
            return 0
        else
            return floor(n)
        end
    end

    snap_hlist = function(where,current,method,height,depth) -- method[v_strut] is default
        if fixedprofile(current) then
            return
        end
        local list = getlist(current)
        local t = trace_vsnapping and { }
        if t then
            t[#t+1] = formatters["list content: %s"](listtoutf(list))
            t[#t+1] = formatters["snap method: %s"](method.name) -- not interfaced
            t[#t+1] = formatters["specification: %s"](method.specification) -- not interfaced
        end
        local snapht, snapdp
        if method[v_local] then
            -- snapping is done immediately here
            snapht = texgetdimen(d_bodyfontstrutheight)
            snapdp = texgetdimen(d_bodyfontstrutdepth)
            if t then
                t[#t+1] = formatters["local: snapht %p snapdp %p"](snapht,snapdp)
            end
        elseif method[v_global] then
            snapht = texgetdimen(d_globalbodyfontstrutheight)
            snapdp = texgetdimen(d_globalbodyfontstrutdepth)
            if t then
                t[#t+1] = formatters["global: snapht %p snapdp %p"](snapht,snapdp)
            end
        else
            -- maybe autolocal
            -- snapping might happen later in the otr
            snapht = texgetdimen(d_globalbodyfontstrutheight)
            snapdp = texgetdimen(d_globalbodyfontstrutdepth)
            local lsnapht = texgetdimen(d_bodyfontstrutheight)
            local lsnapdp = texgetdimen(d_bodyfontstrutdepth)
            if snapht ~= lsnapht and snapdp ~= lsnapdp then
                snapht, snapdp = lsnapht, lsnapdp
            end
            if t then
                t[#t+1] = formatters["auto: snapht %p snapdp %p"](snapht,snapdp)
            end
        end
        --
        local range = getattr(current,a_snaprange)
        if range then
            local oldht   = getheight(current)
            local olddp   = getdepth (current)
            local extraht = rangesnap(range,snapht,oldht)
            local extradp = rangesnap(range,snapdp,olddp)
            local newht   = oldht + extraht
            local newdp   = olddp + extradp
            if newht < 0  or newdp < 0 then
                report_snapper("ignoring snaprange %i: old (%p,%p) new (%p,%p)",range,oldht,olddp,newht,newdp)
            else
                setheight(current,newht)
                setdepth (current,newdp)
            end
        end
        local wd, ht, dp = getwhd(current)
        --
        local h        = (method[v_noheight] and 0) or height or ht
        local d        = (method[v_nodepth]  and 0) or depth  or dp
        local hr       = method[v_hfraction] or 1
        local dr       = method[v_dfraction] or 1
        local br       = method[v_bfraction] or 0
        local ch       = h
        local cd       = d
        local tlines   = method[v_tlines] or 1
        local blines   = method[v_blines] or 1
        local done     = false
        local plusht   = snapht
        local plusdp   = snapdp
        local snaphtdp = snapht + snapdp
        local extra    = 0

        if t then
            t[#t+1] = formatters["hlist: wd %p ht %p (used %p) dp %p (used %p)"](wd,ht,h,dp,d)
            t[#t+1] = formatters["fractions: hfraction %s dfraction %s bfraction %s tlines %s blines %s"](hr,dr,br,tlines,blines)
        end
        if method[v_box] then
            local br = 1 - br
            if br < 0 then
                br = 0
            elseif br > 1 then
                br = 1
            end
            local n = ceiled((h+d-br*snapht-br*snapdp)/snaphtdp)
            local x = n * snaphtdp - h - d
            plusht = h + x / 2
            plusdp = d + x / 2
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_box,plusht,plusdp)
            end
        elseif method[v_max] then
            local n = ceiled((h+d)/snaphtdp)
            local x = n * snaphtdp - h - d
            plusht = h + x / 2
            plusdp = d + x / 2
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_max,plusht,plusdp)
            end
        elseif method[v_min] then
            -- we catch a lone min
            if method.specification ~= v_min then
                local n = floored((h+d)/snaphtdp)
                local x = n * snaphtdp - h - d
                plusht = h + x / 2
                plusdp = d + x / 2
                if plusht < 0 then
                    plusht = 0
                end
                if plusdp < 0 then
                    plusdp = 0
                end
            end
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_min,plusht,plusdp)
            end
        elseif method[v_none] then
            plusht, plusdp = 0, 0
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_none,0,0)
            end
        end
        -- for now, we actually need to tag a box and then check at several points if something ended up
        -- at the top of a page
        if method[v_halfline] then -- extra halfline
            extra  = snaphtdp/2
            plusht = plusht + extra
            plusdp = plusdp + extra
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline,plusht,plusdp)
            end
        end
        if method[v_line] then -- extra line
            extra  = snaphtdp
            plusht = plusht + extra
            plusdp = plusdp + extra
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line,plusht,plusdp)
            end
        end
        if method[v_halfline_m] then -- extra halfline
            extra  = - snaphtdp/2
            plusht = plusht + extra
            plusdp = plusdp + extra
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline_m,plusht,plusdp)
            end
        end
        if method[v_line_m] then -- extra line
            extra  = - snaphtdp
            plusht = plusht + extra
            plusdp = plusdp + extra
            if t then
                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line_m,plusht,plusdp)
            end
        end
        if method[v_first] then
            local thebox = current
            local id = getid(thebox)
            if id == hlist_code then
                thebox = validvbox(id,getlist(thebox))
                id = thebox and getid(thebox)
            end
            if thebox and id == vlist_code then
                local list = getlist(thebox)
                local lw, lh, ld
                for n in nexthlist, list do
                    lw, lh, ld = getwhd(n)
                    break
                end
                if lh then
                    local wd, ht, dp = getwhd(thebox)
                    if t then
                        t[#t+1] = formatters["first line: height %p depth %p"](lh,ld)
                        t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp)
                    end
                    local delta = h - lh
                    ch, cd = lh, delta + d
                    h, d = ch, cd
                    local shifted = hpack_node(getlist(current))
                    setoffsets(shifted,nil,delta)
                    setlist(current,shifted)
                    done = true
                    if t then
                        t[#t+1] = formatters["first: height %p depth %p shift %p"](ch,cd,delta)
                    end
                elseif t then
                    t[#t+1] = "first: not done, no content"
                end
            elseif t then
                t[#t+1] = "first: not done, no vbox"
            end
        elseif method[v_last] then
            local thebox = current
            local id = getid(thebox)
            if id == hlist_code then
                thebox = validvbox(id,getlist(thebox))
                id = thebox and getid(thebox)
            end
            if thebox and id == vlist_code then
                local list = getlist(thebox)
                local lw, lh, ld
                for n in nexthlist, list do
                    lw, lh, ld = getwhd(n)
                end
                if lh then
                    local wd, ht, dp = getwhd(thebox)
                    if t then
                        t[#t+1] = formatters["last line: height %p depth %p" ](lh,ld)
                        t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp)
                    end
                    local delta = d - ld
                    cd, ch = ld, delta + h
                    h, d = ch, cd
                    local shifted = hpack_node(getlist(current))
                    setoffsets(shifted,nil,delta)
                    setlist(current,shifted)
                    done = true
                    if t then
                        t[#t+1] = formatters["last: height %p depth %p shift %p"](ch,cd,delta)
                    end
                elseif t then
                    t[#t+1] = "last: not done, no content"
                end
            elseif t then
                t[#t+1] = "last: not done, no vbox"
            end
        end
        if method[v_minheight] then
            ch = floored((h-hr*snapht)/snaphtdp)*snaphtdp + plusht
            if t then
                t[#t+1] = formatters["minheight: %p"](ch)
            end
        elseif method[v_maxheight] then
            ch = ceiled((h-hr*snapht)/snaphtdp)*snaphtdp + plusht
            if t then
                t[#t+1] = formatters["maxheight: %p"](ch)
            end
        else
            ch = plusht
            if t then
                t[#t+1] = formatters["set height: %p"](ch)
            end
        end
        if method[v_mindepth] then
            cd = floored((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp
            if t then
                t[#t+1] = formatters["mindepth: %p"](cd)
            end
        elseif method[v_maxdepth] then
            cd = ceiled((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp
            if t then
                t[#t+1] = formatters["maxdepth: %p"](cd)
            end
        else
            cd = plusdp
            if t then
                t[#t+1] = formatters["set depth: %p"](cd)
            end
        end
        if method[v_top] then
            ch = ch + tlines * snaphtdp
            if t then
                t[#t+1] = formatters["top height: %p"](ch)
            end
        end
        if method[v_bottom] then
            cd = cd + blines * snaphtdp
            if t then
                t[#t+1] = formatters["bottom depth: %p"](cd)
            end
        end
        local offset = method[v_offset]
        if offset and offset ~= 0 then
            -- we need to set the attr
            if t then
                local wd, ht, dp = getwhd(current)
                t[#t+1] = formatters["before offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp)
            end
            local shifted = hpack_node(getlist(current))
            setoffsets(shifted,nil,offset)
            setlist(current,shifted)
            if t then
                local wd, ht, dp = getwhd(current)
                t[#t+1] = formatters["after offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp)
            end
            setattr(shifted,a_snapmethod,0)
            setattr(current,a_snapmethod,0)
        end
        if not height then
            setheight(current,ch)
            if t then
                t[#t+1] = formatters["forced height: %p"](ch)
            end
        end
        if not depth then
            setdepth(current,cd)
            if t then
                t[#t+1] = formatters["forced depth: %p"](cd)
            end
        end
        local lines = (ch+cd)/snaphtdp
        if t then
            local original = (h+d)/snaphtdp
            local whatever = (ch+cd)/(texgetdimen(d_globalbodyfontstrutheight) + texgetdimen(d_globalbodyfontstrutdepth))
            t[#t+1] = formatters["final lines : %N -> %N (%N)"](original,lines,whatever)
            t[#t+1] = formatters["final height: %p -> %p"](h,ch)
            t[#t+1] = formatters["final depth : %p -> %p"](d,cd)
        end
    -- todo:
    --
    --     if h < 0 or d < 0 then
    --         h = 0
    --         d = 0
    --     end
        if t then
            report_snapper("trace: %s type %s\n\t%\n\tt",where,nodecodes[getid(current)],t)
        end
        if not method[v_split] then
            -- so extra will not be compensated at the top of a page
            extra = 0
        end

        return h, d, ch, cd, lines, extra
    end

end

local categories = { [0] =
    "discard",
    "largest",
    "force",
    "penalty",
    "add",
    "disable",
    "nowhite",
    "goback",
    "packed",
    "overlay",
    "enable",
    "notopskip",
    "keep", -- 12
}

categories          =  utilities.storage.allocate(table.swapped(categories,categories))
vspacing.categories = categories

function vspacing.tocategories(str)
    local t = { }
    for s in gmatch(str,"[^, ]") do -- use lpeg instead
        local n = tonumber(s)
        if n then
            t[categories[n]] = true
        else
            t[b] = true
        end
    end
    return t
end

function vspacing.tocategory(str) -- can be optimized
    if type(str) == "string" then
        return set.tonumber(vspacing.tocategories(str))
    else
        return set.tonumber({ [categories[str]] = true })
    end
end

vspacingdata.map  = vspacingdata.map  or { } -- allocate ?
vspacingdata.skip = vspacingdata.skip or { } -- allocate ?

storage.register("builders/vspacing/data/map",  vspacingdata.map,  "builders.vspacing.data.map")
storage.register("builders/vspacing/data/skip", vspacingdata.skip, "builders.vspacing.data.skip")

----- delay = false  experiments.register("vspacing.delay", function(v) delay = v end)

-- todo: also isspecification

local setspecification, getspecification

-- 1 statepool  : 2 : more overhead : a bit slower than properties
-- 2 attributes : 1 : more overhead : feels faster than properties
-- 3 properties : 3 : more natural  : feels slower than attributes
-- 4 data       : 1 : more native   : is little faster than attributes (limited penalty)

-- testfile: t:/bugs/bottomfloats-001.tex

local method = 1 -- better tracing
-- local method = 2
-- local method = 3
-- local method = 4
-- local method = 5

-- todo: not true but only visual a_visual,tex.getattribute(a_visual)

if method == 1 then

    local registervalue = attributes.registervalue
    local getvalue      = attributes.getvalue
    local values        = attributes.values

    setspecification = function(n,category,penalty,order)
        local detail = { category, penalty, order or 1 }
        local value  = registervalue(a_skipcategory,detail)
        setattr(n,a_skipcategory,value)
    end

    getspecification = function(n)
        local value = getattr(n,a_skipcategory)
        if value then
            local detail = getvalue(a_skipcategory,value)
         -- local detail = attributes.values[a_skipcategory][value]
         -- local detail = values[a_skipcategory][value]
            if detail then
                return detail[1], detail[2], detail[3]
            end
        end
        return false, false, 1
    end

elseif method == 2 then

    -- quite okay but more memory due to attributes (not many)

    local setattrs = nuts.setattrs
    local getattrs = nuts.getattrs

    setspecification = function(n,category,penalty,order)
        setattrs(n,false,a_skipcategory,category or nil,a_skippenalty,penalty or nil,a_skiporder,order or 1)
    end

    getspecification = function(n)
        local c, p, o = getattrs(n,a_skipcategory,a_skippenalty,a_skiporder)
        return c or false, p or false, o or 1
    end

elseif method == 3 then

    -- more natural as we stay in lua

    setspecification = function(n,category,penalty,order)
        -- we know that there are no properties
        properties[n] = {
            [a_skipcategory] = category,
            [a_skippenalty]  = penalty,
            [a_skiporder]    = order or 1,
        }
    end

    getspecification = function(n)
        local p = properties[n]
        if p then
            return p[a_skipcategory], p[a_skippenalty], p[a_skiporder]
        end
    end

elseif method == 4 then

    -- quite efficient but needs testing because we limit values

    local getdata = nuts.getdata
    local setdata = nuts.setdata

    setspecification = function(n,category,penalty,order)
        if not category or category > 0xF then
            category = 0xF
        end
        if not order or order > 0xFF then
            order = 0xFF
        end
        if not penalty or penalty > 0x7FFFF then
            penalty = 0x7FFFF
        elseif penalty < -0x7FFFF then
            penalty = -0x7FFFF
        end
        -- we need overflow checks
        setdata(n, (penalty << 12) + (order << 4) + category)
    end

    getspecification = function(n)
        local data = getdata(n)
        if data and data ~= 0 then
            local category =  data        & 0x0F
            local order    = (data >>  4) & 0xFF
            local penalty  =  data >> 12
            if category == 0xF then
                category = nil
            end
            if order == 0xFF then
                order = nil
            end
            if penalty == 0x7FFFF then
                penalty = nil
            end
            return category, penalty, order
        else
            return nil, nil, nil
        end
    end

elseif method == 5 then

    -- This a bit cleaner than storing all in data because we can have large penalties
    -- now. Beware: this is very context specific usage of data, as with glyphs. We
    -- could have a first byte indicating what kind of usage (here vertical spacing). Or
    -- we could have an extra user subtype. But it will never be cross package anyway
    -- so for now the category has to do.

    local getdata = nuts.getdata
    local setdata = nuts.setdata

    local id <const> = 0x01

    setspecification = function(n,category,penalty,order)
        if not category or category > 0xF then
            category = 0x0F00
        else
            category = category << 8
        end
        local penaltyflag = penalty and 0xF000 or 0
        if not order or order > 0x0FFF then
            order = 0x0FFF0000
        else
            order = order << 16
        end
        setdata(n, order + penaltyflag + category + id, penalty or nil)
    end

    getspecification = function(n)
        local data, penalty = getdata(n)
        if data and data ~ 0 and (data & id) == id then
            local category    =  data >>  8  & 0xF
            local penaltyflag = (data >> 12) & 0xF
            local order       = (data >> 16) & 0x0FFF
            if category == 0xF then
                category = nil
            end
            if order == 0xFFFF then
                order = nil
            end
            if penaltyflag ~= 0xF then
                penalty = false
            end
            return category, penalty, order
        else
            return nil, nil, nil
        end
    end

end

do

    local P, C, R, S, Cc, Cs = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs
    local lpegmatch = lpeg.match

    vspacing.fixed   = false

    local map        = vspacingdata.map
    local skip       = vspacingdata.skip

    local sign       = S("+-")^0
    local multiplier = C(sign * R("09")^1) * P("*")
    local singlefier = Cs(sign * Cc(1))
    local separator  = S(", ")
    local category   = P(":") * C((1-separator)^1)
    local keyword    = C((1-category-separator)^1)
    local splitter   = (multiplier + Cc(1)) * keyword * (category + Cc(false))

    local k_fixed    <const> = variables.fixed
    local k_flexible <const> = variables.flexible
    local k_limit    <const> = variables.limit

    local k_category <const> = "category"
    local k_penalty  <const> = "penalty"
    local k_order    <const> = "order"

    function vspacing.setmap(from,to)
        map[from] = to
    end

    function vspacing.setskip(key,value,grid)
        if value ~= "" then
            if grid == "" then grid = value end
            skip[key] = { value, grid }
        end
    end

    local expandmacro = token.expandmacro -- todo
 -- local runlocal    = tex.runlocal
 -- local setmacro    = tokens.setters.macro
 -- local settoks     = tex.settoks
    local toscaled    = tex.toscaled

    local b_done     = false
    local b_packed   = false
    local b_keep     = false

    local b_amount   = 0
    local b_stretch  = 0
    local b_shrink   = 0
    local b_category = false
    local b_penalty  = false
    local b_order    = false
    local b_fixed    = false
    local b_grid     = false
    local b_limit    = false

    local pattern    = nil

    local packed     <const> = categories.packed
    local keep       <const> = categories.keep

    local gluefactor = .25

    local ctx_ignoreparskip = context.core.ignoreparskip

    local function before()
        b_amount   = 0
        b_stretch  = 0
        b_shrink   = 0
        b_category = 1
        b_penalty  = false
        b_order    = false
        b_fixed    = b_grid
        b_limit    = false
    end

    local function after()
        if b_fixed then
            b_stretch = 0
            b_shrink  = 0
        else
            b_stretch = gluefactor * b_amount
            b_shrink  = gluefactor * b_amount
        end
    end

    -- use a cache for predefined ones

    local inject  do

        local limit_code <const> = tex.glueoptioncodes.limit
     -- local delay_code <const> = tex.glueoptioncodes.delay

        inject = function()
            local n = new_glue(b_amount,b_stretch,b_shrink)
            setspecification(n,b_category,b_penalty,b_order or 1)
            setvisual(n)
         -- if delay then
         --     if b_limit then
         --         setoptions(n,limit_code | delay_code)
         --     else
         --         setoptions(n,delay_code)
         --     end
         -- else
                if b_limit then
                    setoptions(n,limit_code)
                end
         -- end
            write_node(n)
            -- todo: inject via value
        end

    end

    local function flush()
        after()
        if b_done then
            inject()
            b_done = false
        end
        before()
    end

    local s_predefined = tex.isskip("s_spac_vspacing_predefined")

    local function handler(multiplier, keyword, detail)
        if not keyword then
            report_vspacing("unknown directive %a",s)
        else
            local mk = map[keyword]
            if mk then
                lpegmatch(pattern,mk)
            elseif keyword == k_fixed then
                b_fixed = true
            elseif keyword == k_flexible then
                b_fixed = false
            elseif keyword == k_category then
                local category = tonumber(detail)
                if category == packed then
                    b_packed = true
                elseif category then
                    b_category = category
                    b_done     = true
                    flush()
                end
            elseif keyword == k_order and detail then
                local order = tonumber(detail)
                if order then
                    b_order = order
                end
            elseif keyword == k_penalty and detail then
                local penalty = tonumber(detail)
                if penalty then
                    flush()
                    b_done = true
                    b_category = 3
                    b_penalty = penalty
                    flush()
                end
            elseif keyword == k_limit then
                b_limit = true
            else
                local amount, stretch, shrink
                multiplier = tonumber(multiplier) or 1
                local sk = skip[keyword]
                if sk then
                    -- multiplier, keyword
                    -- best, for now, todo: runlocal with arguments
                    expandmacro("vspacingpredefinedvalue",true,keyword)
                 -- expandmacro(cmd,true,keyword)
                 -- setmacro("tempstring",keyword)
                 -- runlocal(cmd)
                    -- nicest
                 -- runlocal(cache[keyword])
                    -- fast
                 -- settoks("scratchtoks",keyword)
                 -- runlocal("vspacingfromscratchtoks")
                    -- middleground
                 -- setmacro("tempstring",keyword)
                 -- runlocal(ctx_vspacingfromtempstring)
                    --
                    amount, stretch, shrink = texgetglue(s_predefined)
                    if not stretch then
                        stretch = 0
                    end
                    if not shrink then
                        shrink = 0
                    end
                    if stretch == 0 and shrink == 0 then
                        stretch = gluefactor * amount -- always unless grid
                        shrink  = stretch             -- always unless grid
                    end
                else -- no check, todo: parse plus and minus
                    amount  = toscaled(keyword)
                    stretch = gluefactor * amount -- always unless grid
                    shrink  = stretch             -- always unless grid
                end
                -- we look at fixed later
                b_amount  = b_amount  + multiplier * amount
                b_stretch = b_stretch + multiplier * stretch
                b_shrink  = b_shrink  + multiplier * shrink
                b_done    = true
            end
        end
    end

    -- alternatively we can make a table and have a keyword -> split cache but this is probably
    -- not really a bottleneck

    local splitter = ((multiplier + singlefier) * keyword * (category + Cc(false))) / handler
          pattern  = (splitter + separator^1)^0

    function vspacing.inject(grid,str)
        if trace_vspacing then
         -- ctx_pushlogger(report_vspacing)
        end
        b_done   = false
        b_packed = false
        b_keep   = false
        b_grid   = grid == true or grid == 1
        before()
        lpegmatch(pattern,str)
        after()
        if b_done then
            inject()
        end
        if b_packed then
            ctx_ignoreparskip()
        end
        if trace_vspacing then
         -- ctx_poplogger()
        end
    end

    -- If this happens a lot we could preallocate thenodes anc do a copy
    -- instead.

    local c_penalty <const> = categories.penalty
    local c_largest <const> = categories.largest
    local c_disable <const> = categories.disable
    local c_nowhite <const> = categories.nowhite

    local function inject(category,amount,penalty)
        local n = new_glue(amount)
        setspecification(n,category,penalty,1)
        setvisual(k)
        write_node(n)
    end

    function vspacing.injectpenalty(penalty)
        inject(c_penalty,0,penalty)
    end

    function vspacing.injectskip(amount)
        inject(c_largest,amount,false)
    end

    function vspacing.injectdisable()
        inject(c_disable,0,false)
    end

    function vspacing.injectnowhite()
        inject(c_nowhite,0,false)
    end

end

-- implementation

-- alignment box begin_of_par vmodepar hmodepar insert penalty before_display after_display

function vspacing.snapbox(n,how)
    local sv = snapmethods[how]
    if sv then
        local box = getbox(n)
        local list = getlist(box)
        if list then
            local s = getattr(list,a_snapmethod)
            if s == 0 then
                if trace_vsnapping then
                --  report_snapper("box list not snapped, already done")
                end
            else
                local wd, ht, dp = getwhd(box)
                if false then -- todo: already_done
                    -- assume that the box is already snapped
                    if trace_vsnapping then
                        report_snapper("box list already snapped at (%p,%p): %s",
                            ht,dp,listtoutf(list))
                    end
                else
                    local h, d, ch, cd, lines, extra = snap_hlist("box",box,sv,ht,dp)
                    setprop(box,"snapper",{
                        ht = h,
                        dp = d,
                        ch = ch,
                        cd = cd,
                        extra = extra,
                        current = current,
                    })
                    setwhd(box,wd,ch,cd)
                    if trace_vsnapping then
                        report_snapper("box list snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s",
                            h,d,ch,cd,sv.name,sv.specification,"direct",lines,listtoutf(list))
                    end
                    setattr(box,a_snapmethod,0)  --
                    setattr(list,a_snapmethod,0) -- yes or no
                end
            end
        end
    end
end

-- I need to figure out how to deal with the prevdepth that crosses pages. In fact,
-- prevdepth is often quite interfering (even over a next paragraph) so I need to
-- figure out a trick. Maybe use something other than a rule. If we visualize we'll
-- see the baselineskip in action:
--
-- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page
-- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page
-- \blank[force,5*big] { \baselineskip5cm xxxxxxxxx \par } \page

-- We can register and copy the rule instead.

do

    local unpack = unpack or table.unpack
    local concat = table.concat

    local insertnodeafter  = nuts.insertafter
    local insertnodebefore = nuts.insertbefore

    ----- abovedisplayskip_code      <const> = gluecodes.abovedisplayskip
    ----- belowdisplayskip_code      <const> = gluecodes.belowdisplayskip
    ----- abovedisplayshortskip_code <const> = gluecodes.abovedisplayshortskip
    ----- belowdisplayshortskip_code <const> = gluecodes.belowdisplayshortskip

    local w, h, d = 0, 0, 0
    ----- w, h, d = 100*65536, 65536, 65536

    local trace_list   = { }
    local tracing_info = { }
    local before       = ""
    local after        = ""

    local texnest      = tex.nest

    local function nodes_to_string(head)
        local current = head
        local t       = { }
        while current do
            local id = getid(current)
            local ty = nodecodes[id]
            if id == penalty_code then
                t[#t+1] = formatters["%s:%s"](ty,getpenalty(current))
            elseif id == glue_code then
                t[#t+1] = formatters["%s:%s:%p"](ty,gluecodes[getsubtype(current)],getwidth(current))
            elseif id == kern_code then
                t[#t+1] = formatters["%s:%p"](ty,getkern(current))
            else
                t[#t+1] = ty
            end
            current = getnext(current)
        end
        return concat(t," + ")
    end

    local function reset_tracing(head)
        trace_list, tracing_info, before, after = { }, false, nodes_to_string(head), ""
    end

    local function trace_skip(str,sc,so,sp,data)
        trace_list[#trace_list+1] = { "skip", formatters["%s | %p | category %s | order %s | penalty %s | subtype %s"](str, getwidth(data), sc or "-", so or "-", sp or "-", gluecodes[getsubtype(data)]) }
        tracing_info = true
    end

    local function trace_natural(str,data)
        trace_list[#trace_list+1] = { "skip", formatters["%s | %p"](str, getwidth(data)) }
        tracing_info = true
    end

    local function trace_info(message, where, what)
        trace_list[#trace_list+1] = { "info", formatters["%s: %s/%s"](message,where,what) }
    end

    local function trace_node(what)
        local nt = nodecodes[getid(what)]
        local tl = trace_list[#trace_list]
        if tl and tl[1] == "node" then
            trace_list[#trace_list] = { "node", formatters["%s + %s"](tl[2],nt) }
        else
            trace_list[#trace_list+1] = { "node", nt }
        end
    end

    local function show_tracing(head)
        if tracing_info then
            after = nodes_to_string(head)
            for i=1,#trace_list do
                local tag, text = unpack(trace_list[i])
                if tag == "info" then
                    report_collapser(text)
                else
                    report_collapser("  %s: %s",tag,text)
                end
            end
            report_collapser("before: %s",before)
            report_collapser("after : %s",after)
        end
    end

    local function trace_done(str,data)
        if getid(data) == penalty_code then
            trace_list[#trace_list+1] = { "penalty", formatters["%s | %s"](str,getpenalty(data)) }
        else
            trace_list[#trace_list+1] = { "glue", formatters["%s | %p"](str,getwidth(data)) }
        end
        tracing_info = true
    end

    local function forced_skip(head,current,width,where,trace) -- looks old ... we have other tricks now
        if head == current then
            if getsubtype(head) == baselineskip_code then
                width = width - getwidth(head)
            end
        end
        if width == 0 then
            -- do nothing
        else
            local b = new_rule(w,h,d)
            local k = new_kern(width)
            local a = new_rule(w,h,d)
            setvisual(k)
            if where == "after" then
                head, current = insertnodeafter(head,current,b)
                head, current = insertnodeafter(head,current,k)
                head, current = insertnodeafter(head,current,a)
            else
                local c = current
                head, current = insertnodebefore(head,current,b)
                head, current = insertnodebefore(head,current,k)
                head, current = insertnodebefore(head,current,a)
                current = c
            end
        end
        if trace then
            report_vspacing("inserting forced skip of %p",width)
        end
        return head, current
    end

    -- penalty only works well when before skip

    local discard   <const> = categories.discard
    local largest   <const> = categories.largest
    local force     <const> = categories.force
    local penalty   <const> = categories.penalty
    local add       <const> = categories.add
    local disable   <const> = categories.disable
    local nowhite   <const> = categories.nowhite
    local goback    <const> = categories.goback
    local packed    <const> = categories.packed
    local overlay   <const> = categories.overlay
    local enable    <const> = categories.enable
    local notopskip <const> = categories.notopskip
    local keep      <const> = categories.keep

    -- [whatsits][hlist][glue][glue][penalty]

    local special_penalty_min <const> = 32250
    local special_penalty_max <const> = 35000
    local special_penalty_xxx <const> =     0

    -- this is rather messy and complex: we want to make sure that successive
    -- header don't break but also make sure that we have at least a decent
    -- break when we have succesive ones (often when testing)

    -- todo: mark headers as such so that we can recognize them

    local specialmethods = { }
    local specialmethod  = 1

    specialmethods[1] = function(pagehead,pagetail,start,penalty)
        --
        if not pagehead or penalty < special_penalty_min or penalty > special_penalty_max then
            return
        end
        local current  = pagetail
        --
        -- nodes.showsimplelist(pagehead,0)
        --
        if trace_specials then
            report_specials("checking penalty %a",penalty)
        end
        while current do
            local id = getid(current)
-- if id == penalty_code or id == glue_code then -- see (!!)
            if id == penalty_code then
                local p = properties[current]
                if p then
                    local p = p.special_penalty
                    if not p then
                        if trace_specials then
                            report_specials("  regular penalty, continue")
                        end
                    elseif p == penalty then
                        if trace_specials then
                            report_specials("  context penalty %a, same level, overloading",p)
                        end
                        return special_penalty_xxx
                    elseif p > special_penalty_min and p < special_penalty_max then
                        if penalty < p then
                            if trace_specials then
                                report_specials("  context penalty %a, lower level, overloading",p)
                            end
                            return special_penalty_xxx
                        else
                            if trace_specials then
                                report_specials("  context penalty %a, higher level, quitting",p)
                            end
                            return
                        end
                    elseif trace_specials then
                        report_specials("  context penalty %a, higher level, continue",p)
                    end
                else
                    local p = getpenalty(current)
                    if p < 10000 then
                        -- assume some other mechanism kicks in so we seem to have content
                        if trace_specials then
                            report_specials("  regular penalty %a, quitting",p)
                        end
                        break
                    else
                        if trace_specials then
                            report_specials("  regular penalty %a, continue",p)
                        end
                    end
                end
            end
            current = getprev(current)
        end
        -- none found, so no reason to be special
        if trace_specials then
            if pagetail then
                report_specials("  context penalty, discarding, nothing special")
            else
                report_specials("  context penalty, discarding, nothing preceding")
            end
        end
        return special_penalty_xxx
    end

    -- This will be replaced after 0.80+ when we have a more robust look-back and
    -- can look at the bigger picture.

    -- todo: look back and when a special is there before a list is seen penalty keep ut

    -- we now look back a lot, way too often

    -- userskip
    -- lineskip
    -- baselineskip
    -- parskip
    -- abovedisplayskip
    -- belowdisplayskip
    -- abovedisplayshortskip
    -- belowdisplayshortskip
    -- topskip
    -- splittopskip

    -- we could inject a vadjust to force a recalculation .. a mess
    --
    -- So, the next is far from robust and okay but for the moment this overlaying
    -- has to do. Always test this with the examples in spac-ver.mkvi!

    local function snap_topskip(current,method)
        local w = getwidth(current)
        setwidth(current,0)
        return w, 0
    end

    local function check_experimental_overlay(head,current)
        local p = nil
        local c = current
        local n = nil
        local function overlay(p,n,mvl)
            local p_wd, p_ht, p_dp = getwhd(p)
            local n_wd, n_ht, n_dp = getwhd(n)
            local skips = 0
            --
            -- We deal with this at the tex end .. we don't see spacing .. enabling this code
            -- is probably harmless but then we need to test it.
            --
            -- we could calculate this before we call
            --
            -- problem: prev list and next list can be unconnected
            --
            local c = getnext(p)
            local l = c
            while c and c ~= n do
                local id = getid(c)
                if id == glue_code then
                    local w = getwidth(c)
                    skips = skips + w
                    setglue(c,w) -- nil stretch
                elseif id == kern_code then
                    skips = skips + getkern(c)
                end
                l = c
                c = getnext(c)
            end
            local c = getprev(n)
            while c and c ~= n and c ~= l do
                local id = getid(c)
                if id == glue_code then
                    local w = getwidth(c)
                    skips = skips + w
                    setglue(c,w) -- nil stretch
                elseif id == kern_code then
                    skips = skips + getkern(c)
                end
                c = getprev(c)
            end
            --
            local delta = n_ht + skips + p_dp
            texsetdimen("global",d_spac_overlay,-delta) -- for tracing
            -- we should adapt pagetotal ! (need a hook for that) .. now we have the wrong pagebreak
            local k = new_kern(-delta)
            setvisual(k)
            head = insertnodebefore(head,n,k)
            if n_ht > p_ht then
                local k = new_kern(n_ht-p_ht)
                setvisual(k)
                head = insertnodebefore(head,p,k)
            end
            if trace_vspacing then
                report_vspacing("overlaying, prev height: %p, prev depth: %p, next height: %p, skips: %p, move up: %p",p_ht,p_dp,n_ht,skips,delta)
            end
            return remove_node(head,current,true)
        end

        -- goto next line
        while c do
            local id = getid(c)
            if id == glue_code or id == penalty_code or id == kern_code then
                -- skip (actually, remove)
                c = getnext(c)
            elseif id == hlist_code then
                n = c
                break
            else
                break
            end
        end
        if n then
            -- we have a next line, goto prev line
            c = current
            while c do
                local id = getid(c)
                if id == glue_code or id == penalty_code then -- kern ?
                    c = getprev(c)
                elseif id == hlist_code then
                    p = c
                    break
                else
                    break
                end
            end
            if not p then
                if a_snapmethod == a_snapvbox then
                    -- quit, we're not on the mvl
                else
                    -- inefficient when we're at the end of a page
                    local c = tonut(texgetlist("pagehead"))
                    while c and c ~= n do
                        local id = getid(c)
                        if id == hlist_code then
                            p = c
                        end
                        c = getnext(c)
                    end
                    if p and p ~= n then
                        return overlay(p,n,true)
                    end
                end
            elseif p ~= n then
                return overlay(p,n,false)
            end
        end
        -- in fact, we could try again later ... so then no remove (a few tries)
        return remove_node(head,current,true)
    end

    -- where -> scope
    -- what  -> where (original context)

    local checkslide = false

    directives.register("vspacing.checkslide", function(v)
        if v then
            checkslide = function(head,where,what)
                nuts.checkslide(head,where .. " : " .. what)
            end
        else
            checkslide = false
        end
    end)

    local setdiscardable   <const> = tex.glueoptioncodes.setdiscardable
    local resetdiscardable <const> = tex.glueoptioncodes.resetdiscardable

    -- We used to have these helpers local in the function but in the end moved
    -- them out. Such local definitions also cost time and passing parameters
    -- is in the end cheaper (but maybe also more messy. However we can now use
    -- (experimental) variants easier.

    local function getpagelist(pagehead,pagetail)
        if not pagehead then
            pagehead, pagetail = nuts.getmvllist()
        end
        return pagehead, pagetail
    end

    local function compensate(n)
        local g = 0
        while n and getid(n) == glue_code do
            g = g + getwidth(n)
            n = getnext(n)
        end
        if n then
            local p = getprop(n,"snapper")
            if p then
                local extra = p.extra
                if extra and extra < 0 then -- hm, extra can be unset ... needs checking
                    local h = p.ch -- getheight(n)
                    -- maybe an extra check
                 -- if h - extra < g then
                        setheight(n,h-2*extra)
                        p.extra = 0
                        if trace_vsnapping then
                            report_snapper("removed extra space at top: %p",extra)
                        end
                 -- end
                end
            end
            return n
        end
    end

    local function checkoptions(n,where,dont_discard)
        if getid(n) ~= glue_code then
            -- something is wrong
         -- print("IGNORE OPTION",where)
        elseif dont_discard then
         -- print("SET OPTION")
            setoptions(n,setdiscardable)
        else
         -- print("RESET OPTION")
            setoptions(n,resetdiscardable)
        end
    end

    -- todo: make this nicer in the engine

    local function removetopsnap(head,pagehead,pagetail)
        pagehead, pagetail = getpagelist(pagehead,pagetail)
        if pagehead then
            local n = pagehead and compensate(pagehead)
            if n and n ~= pagetail then
                local p = getprop(pagetail,"snapper")
                if p then
                    local e = p.extra
                    if e and e < 0 then
                        -- what if mvl's ...
                        local t = texget("pagetotal")
                        if t > 0 then
                            local g = texget("pagegoal") -- 1073741823 is signal
                            local d = g - t
                            if d < -e then
                                local penalty = new_penalty(1000000)
                                setvisual(penalty)
                                setlink(penalty,head)
                                head = penalty
                                report_snapper("force pagebreak due to extra space at bottom: %p",e)
                            end
                        end
                    end
                end
            end
        elseif head then
            compensate(head)
        end
        return head, pagehead, pagetail
    end

    local function wrapup(head,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
        local tail = find_node_tail(head)
        if penalty_data then
            local p = new_penalty(penalty_data)
            setvisual(p)
            if trace then
                trace_done("result",p)
            end
            setlink(tail,p)
            tail = p
            local props = properties[p] -- (!!)
            if props then
                props.special_penalty = special_penalty or penalty_data
            else
                properties[p] = {
                    special_penalty = special_penalty or penalty_data
                }
            end
        end
        if glue_data then
            if trace then
                trace_done("result",glue_data)
            end
            if force_glue then
                checkoptions(head,4,dont_discard)
                head, tail = forced_skip(head,tail,getwidth(glue_data),"after",trace)
                flushnode(glue_data)
                glue_data = nil
            elseif tail then
                setlink(tail,glue_data)
                setnext(glue_data)
                checkoptions(glue_data,5,dont_discard)
            else
                head = glue_data
                checkoptions(glue_data,6,dont_discard)
            end
            -- appending to the list bypasses tex's prevdepth handler
            texnest[texnest.ptr].prevdepth = 0
        end
        return head, glue_data
    end

    local function flush(why,head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
        if penalty_data then
            local p = new_penalty(penalty_data)
            setvisual(p)
            if trace then
                trace_done("flushed due to " .. why,p)
            end
            if penalty_data >= 10000 then -- or whatever threshold?
                local prev = getprev(current)
                if getid(prev) == glue_code then -- maybe go back more, or maybe even push back before any glue
                        -- tricky case: spacing/grid-007.tex: glue penalty glue
                    head = insertnodebefore(head,prev,p)
                else
                    head = insertnodebefore(head,current,p)
                end
            else
                head = insertnodebefore(head,current,p)
            end
            local props = properties[p] -- (!!)
            if props then
                props.special_penalty = special_penalty or penalty_data
            else
                properties[p] = {
                    special_penalty = special_penalty or penalty_data
                }
            end
        end
        if glue_data then
            if force_glue then
                if trace then
                    trace_done("flushed due to forced " .. why,glue_data)
                end
                checkoptions(head,1,dont_discard)
                head = forced_skip(head,current,getwidth(glue_data,width),"before",trace)
                flushnode(glue_data)
             -- glue_data = nil -- we reset anyway
            else
                local width, stretch, shrink = getglue(glue_data)
                if width ~= 0 then
                    if trace then
                        trace_done("flushed due to non zero " .. why,glue_data)
                    end
                    checkoptions(glue_data,2,dont_discard)
                    head = insertnodebefore(head,current,glue_data)
                elseif stretch ~= 0 or shrink ~= 0 then
                    if trace then
                        trace_done("flushed due to stretch/shrink in" .. why,glue_data)
                    end
                    head = insertnodebefore(head,current,glue_data)
                else
                 -- report_vspacing("needs checking (%s): %p",gluecodes[getsubtype(glue_data)],w)
                    checkoptions(glue_data,3,dont_discard)
                    flushnode(glue_data)
                end
            end
        end
        if trace then
            trace_node(current)
        end
        return head
    end

    local function collapser(head,where,what,trace,snap,a_snapmethod) -- maybe also pass tail
        if trace then
            reset_tracing(head)
        end
        local current           = head
        local oldhead           = head
        local glue_order        = 0
        local glue_data
        local force_glue        = false
        local penalty_order     = 0
        local penalty_data
        local natural_penalty
        local special_penalty
        local parskip
        local ignore_parskip    = false
        local ignore_following  = false
        local ignore_whitespace = false
        local keep_together     = false
        local dont_discard      = false
        local lastsnap
        local pagehead
        local pagetail
        --
        local function reset()
            glue_order        = 0
            glue_data         = nil
            force_glue        = false
            penalty_order     = 0
            penalty_data      = nil
            natural_penalty   = nil
            parskip           = nil
            ignore_parskip    = false
            ignore_following  = false
            ignore_whitespace = false
            dont_discard      = false
        end
        --
        if trace_vsnapping then
            report_snapper("global ht/dp = %p/%p, local ht/dp = %p/%p",
                texgetdimen(d_globalbodyfontstrutheight),
                texgetdimen(d_globalbodyfontstrutdepth),
                texgetdimen(d_bodyfontstrutheight),
                texgetdimen(d_bodyfontstrutdepth)
            )
        end
        if trace then
            trace_info("start analyzing",where,what)
        end
        if snap and where == "page" then
            head, pagehead, pagetail = removetopsnap(head,pagehead,pagetail)
        end
        if checkslide then
            checkslide(head,where,what)
        end
     -- -- In the case of delay we only have glue nodes with a user or parskip subtype
     -- -- so prioritize these. We could have a stripped down variant but that doesn't
     -- -- save much.
        while current do
            local id = getid(current)
            if id == glue_code then
                local subtype = getsubtype(current)
                if subtype == userskip_code then
                    local sc, sp, so = getspecification(current)
                    if not so then
                        so = 1 -- the others have no default value
                    end
                    if sp and sc == penalty then
                        -- what with delay ...
                        if where == "page" then
                            pagehead, pagetail = getpagelist(pagehead,pagetail)
                            local p = specialmethods[specialmethod](pagehead,pagetail,current,sp)
                            if p then
                             -- todo: other tracer
                             --
                             -- if trace then
                             --     trace_skip("previous special penalty %a is changed to %a using method %a",sp,p,specialmethod)
                             -- end
                                special_penalty = sp
                                sp = p
                            end
                        end
                        if not penalty_data then
                            penalty_data = sp
                        elseif penalty_order < so then
                            penalty_order, penalty_data = so, sp
                        elseif penalty_order == so and sp > penalty_data then
                            penalty_data = sp
                        end
                        if trace then
                            trace_skip("penalty in skip",sc,so,sp,current)
                        end
                        head, current = remove_node(head,current,true)
                    elseif not sc then  -- if not sc then
                        if glue_data then
                            if trace then
                                trace_done("flush",glue_data)
                            end
                            head = insertnodebefore(head,current,glue_data)
                            if trace then
                                trace_natural("natural",current)
                            end
                            current = getnext(current)
                            glue_data = nil
                        else
                            -- not look back across head
                            -- todo: prev can be whatsit (latelua)
                            local previous = getprev(current)
                            if previous and hasidsubtype(previous,glue_code,userskip_code) then
                                local pwidth, pstretch, pshrink, pstretch_order, pshrink_order = getglue(previous)
                                local cwidth, cstretch, cshrink, cstretch_order, cshrink_order = getglue(current)
                                if pstretch_order == 0 and pshrink_order == 0 and cstretch_order == 0 and cshrink_order == 0 then
                                    setglue(previous,pwidth + cwidth, pstretch + cstretch, pshrink  + cshrink)
                                    if trace then
                                        trace_natural("removed",current)
                                    end
                                    head, current = remove_node(head,current,true)
                                    if trace then
                                        trace_natural("collapsed",previous)
                                    end
                                else
                                    if trace then
                                        trace_natural("filler",current)
                                    end
                                    current = getnext(current)
                                end
                            else
                                if trace then
                                    trace_natural("natural (no prev)",current)
                                end
                                current = getnext(current)
                            end
                        end
                        glue_order = 0
                    elseif sc == disable or sc == enable then
                        local next = getnext(current)
                        if next then
                            ignore_following = sc == disable
                            if trace then
                                trace_skip(sc == disable and "disable" or "enable",sc,so,sp,current)
                            end
                            head, current = remove_node(head,current,true)
                        else
                            current = next
                        end
                    elseif sc == packed then
                        if trace then
                            trace_skip("packed",sc,so,sp,current)
                        end
                        -- can't happen !
                        head, current = remove_node(head,current,true)
                    elseif sc == nowhite then
                        local next = getnext(current)
                        if next then
                            ignore_whitespace = true
                            head, current = remove_node(head,current,true)
                        else
                            current = next
                        end
                    elseif sc == discard then
                        if trace then
                            trace_skip("discard",sc,so,sp,current)
                        end
                        head, current = remove_node(head,current,true)
                    elseif sc == overlay then
                        -- todo (overlay following line over previous
                        if trace then
                            trace_skip("overlay",sc,so,sp,current)
                        end
                            -- beware: head can actually be after the affected nodes as
                            -- we look back ... some day head will the real head
                        head, current = check_experimental_overlay(head,current,a_snapmethod)
                    elseif ignore_following then
                        if trace then
                            trace_skip("disabled",sc,so,sp,current)
                        end
                        head, current = remove_node(head,current,true)
                    elseif not glue_data then
                        if trace then
                            trace_skip("assign",sc,so,sp,current)
                        end
                        if sc == keep then
                            if trace then
                                trace_skip("keep 1",sc,so,sp,current)
                            end
                            dont_discard = true
                        end
                        glue_order = so
                        head, current, glue_data = remove_node(head,current)
                    elseif glue_order < so then
                        if trace then
                            trace_skip("force",sc,so,sp,current)
                        end
                        glue_order = so
                        flushnode(glue_data)
                        head, current, glue_data = remove_node(head,current)
                        if sc == keep then
                            if trace then
                                trace_skip("keep 2",sc,so,sp,current)
                            end
                            dont_discard = true
                        end
                    elseif glue_order == so then
                        -- is now exclusive, maybe support goback as combi, else why a set
                        if sc == largest then
                            local cw = getwidth(current)
                            local gw = getwidth(glue_data)
                            if cw > gw then
                                if trace then
                                    trace_skip("largest",sc,so,sp,current)
                                end
                                flushnode(glue_data)
                                head, current, glue_data = remove_node(head,current)
                            else
                                if trace then
                                    trace_skip("remove smallest",sc,so,sp,current)
                                end
                                head, current = remove_node(head,current,true)
                            end
                        elseif sc == goback then
                            if trace then
                                trace_skip("goback",sc,so,sp,current)
                            end
                            flushnode(glue_data)
                            head, current, glue_data = remove_node(head,current)
                        elseif sc == force then
                            -- last one counts, some day we can provide an accumulator and largest etc
                            -- but not now
                            if trace then
                                trace_skip("force",sc,so,sp,current)
                            end
                            flushnode(glue_data)
                            head, current, glue_data = remove_node(head,current)
                        elseif sc == penalty then
                            if trace then
                                trace_skip("penalty",sc,so,sp,current)
                            end
                            flushnode(glue_data)
                            glue_data = nil
                            head, current = remove_node(head,current,true)
                        elseif sc == add then
                            if trace then
                                trace_skip("add",sc,so,sp,current)
                            end
                            local cwidth, cstretch, cshrink = getglue(current)
                            local gwidth, gstretch, gshrink = getglue(glue_data)
                            setglue(glue_data,gwidth + cwidth, gstretch + cstretch,gshrink + cshrink)
                            -- toto: order
                            head, current = remove_node(head,current,true)
                        else
                            if trace then
                                trace_skip("unknown 1",sc,so,sp,current)
                            end
                            head, current = remove_node(head,current,true)
                            if sc == keep then
                                if trace then
                                    trace_skip("keep 4",sc,so,sp,current)
                                end
                                dont_discard = true
                            end
                        end
                    else
                        if trace then
                            trace_skip("unknown 2",sc,so,sp,current)
                        end
                        head, current = remove_node(head,current,true)
                    end
                    if sc == force then
                        force_glue = true
                    end
                elseif subtype == parskip_code then
                    -- parskip always comes later
                    if ignore_whitespace then
                        if trace then
                            trace_natural("ignored parskip",current)
                        end
                        head, current = remove_node(head,current,true)
                    elseif glue_data then
                        local w = getwidth(current)
                        if w ~= 0 and w > getwidth(glue_data) then
                            flushnode(glue_data)
                            glue_data = current
                            if trace then
                                trace_natural("taking parskip",current)
                            end
                            head, current = remove_node(head,current)
                        else
                            if trace then
                                trace_natural("removed parskip",current)
                            end
                            head, current = remove_node(head,current,true)
                        end
                    else
                        if trace then
                            trace_natural("honored parskip",current)
                        end
                        head, current, glue_data = remove_node(head,current)
                    end
                elseif subtype == baselineskip_code then
                    if snap then
                        local s = getattr(current,a_snapmethod)
                        if s and s ~= 0 then
                            setattr(current,a_snapmethod,0)
                            setwidth(current,0)
                            if trace_vsnapping then
                                report_snapper("baselineskip set to zero")
                            end
                        else
                            if trace then
                                trace_skip("baselineskip",sc,so,sp,current)
                            end
                            head = flush("baselineskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                            reset()
                        end
                    else
                        if trace then
                            trace_skip("baselineskip",sc,so,sp,current)
                        end
                        head = flush("baselineskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                        reset()
                    end
                    current = getnext(current)
                elseif subtype == lineskip_code then
                    if snap then
                        local s = getattr(current,a_snapmethod)
                        if s and s ~= 0 then
                            setattr(current,a_snapmethod,0)
                            setwidth(current,0)
                            if trace_vsnapping then
                                report_snapper("lineskip set to zero")
                            end
                        else
                            if trace then
                                trace_skip("lineskip",sc,so,sp,current)
                            end
                            head = flush("lineskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                            reset()
                        end
                    else
                        if trace then
                            trace_skip("lineskip",sc,so,sp,current)
                        end
                        head = flush("lineskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                        reset()
                    end
                    current = getnext(current)
                elseif subtype == topskip_code or subtype == splittopskip_code then
                    local next = getnext(current)
                 -- if next and getattr(next,a_skipcategory) == notopskip then
                    if next and getspecification(next) == notopskip then
                        setglue(current) -- zero
                    end
                    if snap then
                        local s = getattr(current,a_snapmethod)
                        if s and s ~= 0 then
                            setattr(current,a_snapmethod,0)
                            local sv = snapmethods[s]
                            local w, cw = snap_topskip(current,sv)
                            if trace_vsnapping then
                                report_snapper("topskip snapped from %p to %p for %a",w,cw,where)
                            end
                        else
                            if trace then
                                trace_skip("topskip",sc,so,sp,current)
                            end
                            head = flush("topskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                            reset()
                        end
                    else
                        if trace then
                            trace_skip("topskip",sc,so,sp,current)
                        end
                        head = flush("topskip",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                        reset()
                    end
                    current = getnext(current)
             -- elseif subtype == abovedisplayskip_code and remove_math_skips then
             --     --
             --     if trace then
             --         trace_skip("above display skip (normal)",sc,so,sp,current)
             --     end
             --     head = flush("above display skip (normal)",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
             --     reset()
             --     current = getnext(current)
             --     --
             -- elseif subtype == belowdisplayskip_code and remove_math_skips then
             --     --
             --     if trace then
             --         trace_skip("below display skip (normal)",sc,so,sp,current)
             --     end
             --     head = flush("below display skip (normal)",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
             --     reset()
             --     current = getnext(current)
             --    --
             -- elseif subtype == abovedisplayshortskip_code and remove_math_skips then
             --     --
             --     if trace then
             --         trace_skip("above display skip (short)",sc,so,sp,current)
             --     end
             --     head = flush("above display skip (short)",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
             --     reset()
             --     current = getnext(current)
             --     --
             -- elseif subtype == belowdisplayshortskip_code and remove_math_skips then
             --     --
             --     if trace then
             --         trace_skip("below display skip (short)",sc,so,sp,current)
             --     end
             --     head = flush("below display skip (short)",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
             --     reset()
             --     current = getnext(current)
                else -- other glue
                    if snap and trace_vsnapping then
                        local w = getwidth(current)
                        if w ~= 0 then
                            report_snapper("glue %p of type %a kept",w,gluecodes[subtype])
                        end
                    end
                    if trace then
                        trace_skip(formatters["glue of type %a"](subtype),sc,so,sp,current)
                    end
                    head = flush("some glue",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                    reset()
                    current = getnext(current)
                end
            elseif id == hlist_code or id == vlist_code then
                -- don't snap due to attr
                -- needs checking, why so many calls
                if snap then
                    lastsnap = nil
                    local list = getlist(current)
                    local s = getattr(current,a_snapmethod)
                    if not s then
                    --  if trace_vsnapping then
                    --      report_snapper("mvl list not snapped")
                    --  end
                    elseif s == 0 then
                        if trace_vsnapping then
                            report_snapper("mvl %a not snapped, already done: %s",nodecodes[id],listtoutf(list))
                        end
                    else
                        local sv = snapmethods[s]
                        if sv then
                            -- check if already snapped
                            local done = list and already_done(id,list,a_snapmethod)
                            if done then
                                -- assume that the box is already snapped
                                if trace_vsnapping then
                                    local w, h, d = getwhd(current)
                                    report_snapper("mvl list already snapped at (%p,%p): %s",h,d,listtoutf(list))
                                end
                            else
                                local h, d, ch, cd, lines, extra = snap_hlist("mvl",current,sv,false,false)
                                lastsnap = {
                                    ht = h,
                                    dp = d,
                                    ch = ch,
                                    cd = cd,
                                    extra = extra,
                                    current = current,
                                }
                                setprop(current,"snapper",lastsnap)
                                if trace_vsnapping then
                                    report_snapper("mvl %a snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s",
                                        nodecodes[id],h,d,ch,cd,sv.name,sv.specification,where,lines,listtoutf(list))
                                end
                            end
                        elseif trace_vsnapping then
                            report_snapper("mvl %a not snapped due to unknown snap specification: %s",nodecodes[id],listtoutf(list))
                        end
                        setattr(current,a_snapmethod,0)
                    end
                else
                    --
                end
                head = flush("list",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                reset()
                current = getnext(current)
            elseif id == penalty_code then
             -- natural_penalty = getpenalty(current)
             -- if trace then
             --     trace_done("removed penalty",current)
             -- end
             -- head, current = remove_node(head,current,true)
                if trace then
                    trace_done("kept penalty",current)
                end
                current = getnext(current)
            elseif id == kern_code then
                if snap and trace_vsnapping and getkern(current) ~= 0 then
                    report_snapper("kern of %p kept",getkern(current))
                end
                head = flush("kern",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                reset()
                current = getnext(current)
            else
                head = flush(trace and formatters["node with id %a"](id) or "other node",head,current,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
                reset()
                current = getnext(current)
            end
        end
        if trace then
            trace_info("stop analyzing",where,what)
        end
     -- if natural_penalty and (not penalty_data or natural_penalty > penalty_data) then
     --     penalty_data = natural_penalty
     -- end
        if glue_data or penalty_data then
            if trace then
                trace_info("start flushing",where,what)
            end
            head, glue_data = wrapup(head,penalty_data,glue_data,trace,force_glue,dont_discard,special_penalty)
            if trace then
                trace_info("stop flushing",where,what)
            end
        end
        if trace then
            show_tracing(head)
            if oldhead ~= head then
                trace_info("head has been changed from %a to %a",nodecodes[getid(oldhead)],nodecodes[getid(head)])
            end
        end
        return head
    end

    -- This really need a rework. although, it has now been stable for 15 years so why
    -- mess with it. We get sequences like these where penalties come from the par
    -- builder:
    --
    -- glue
    -- glue hlist
    -- glue hlist penalty glue hlist penalty glue hlist
    --
    -- and in context we never use penalties other than spacing related

    local stackhead  = { }
    local stacktail  = { }
    local stackhack  = { }

    local forceflush = false
    local stackmvl   = 0

    local function report(message,where,lst)
        if lst and where then
            report_vspacing(message,where,count_nodes(lst,true),nodeidstostring(lst))
        else
            report_vspacing(message,count_nodes(lst,true),nodeidstostring(lst))
        end
    end

    local getcurrentmvl = tex.getcurrentmvl

    function vspacing.pagehandler(newhead,where)

        if newhead then
            --
            local mvl = getcurrentmvl()
            if mvl ~= stackmvl then
                if stackhead[stackmvl] then
                    report_vspacing("pending stack entries for mvl %i, changing to %i",stackmvl,mvl)
                end
             -- if stackmvl == 0 then
             --     stackhack[0] = false
             --     stackhead[0] = false
             --     stacktail[0] = false
             -- end
                stackmvl = mvl
            end
            --
            local newtail = find_node_tail(newhead) -- best pass that tail, known anyway
            local flush = false
            stackhack[stackmvl] = true -- todo: only when grid snapping once enabled
            for n, id, subtype in nextnode, newhead do
                if id ~= glue_code then
                    flush = true
                elseif subtype == userskip_code then
                 -- local sc = getattr(n,a_skipcategory)
                    local sc = getspecification(n)
                    if sc then
                        stackhack[stackmvl] = true
                    else
                        flush = true
                    end
                elseif subtype == parskip_code then
                    -- if where == new_graf then ... end
                    if texgetcount(c_spac_vspacing_ignore_parskip) > 0 then
                        setglue(n)
                     -- maybe removenode
                    end
                end
            end
            texsetcount(c_spac_vspacing_ignore_parskip,0)
            if forceflush then
                forceflush = false
                flush      = true
            end
            if flush then
                if stackhead[stackmvl] then
                    if trace_collect_vspacing then report("%s > appending %s nodes to stack (final): %s",where,newhead) end
                    setlink(stacktail[stackmvl],newhead)
                    newhead = stackhead[stackmvl]
                    stackhead[stackmvl] = nil
                    stacktail[stackmvl] = nil
                end
                if stackhack[stackmvl] then
                    stackhack[stackmvl] = false
                    if trace_collect_vspacing then report("%s > processing %s nodes: %s",where,newhead) end
                    newhead = collapser(newhead,"page",where,trace_page_vspacing,true,a_snapmethod)
                else
                    if trace_collect_vspacing then report("%s > flushing %s nodes: %s",where,newhead) end
                end
                return newhead
            else
                if stackhead[stackmvl] then
                    if trace_collect_vspacing then report("%s > appending %s nodes to stack (intermediate): %s",where,newhead) end
                    setlink(stacktail[stackmvl],newhead)
                else
                    if trace_collect_vspacing then report("%s > storing %s nodes in stack (initial): %s",where,newhead) end
                    stackhead[stackmvl] = newhead
                end
                stacktail[stackmvl] = newtail
            end
        end
        return nil
    end

    function vspacing.pageoverflow()
        local h = 0
        if stackhead[stackmvl] then
            for n, id in nextnode, stackhead[stackmvl] do
                if id == glue_code then
                    h = h + getwidth(n)
                elseif id == kern_code then
                    h = h + getkern(n)
                end
            end
        end
        return h
    end

    function vspacing.forcepageflush()
        forceflush = true
    end

    local ignored = table.tohash {
        "splitkeep",
        "splitoff",
     -- "insert",
    }

    function vspacing.vboxhandler(head,where)
        if head and not ignored[where] and getnext(head) then
            head = collapser(head,"vbox",where,trace_vbox_vspacing,true,a_snapvbox) -- todo: local snapper
        end
        return head
    end

    function vspacing.collapsevbox(n,aslist) -- for boxes but using global a_snapmethod
        local box = getbox(n)
        if box then
            local list = getlist(box)
            if list then
                list = collapser(list,"snapper","vbox",trace_vbox_vspacing,true,a_snapmethod)
                if aslist then
                    setlist(box,list) -- beware, dimensions of box are wrong now
                else
                    setlist(box,vpack_node(list))
                end
            end
        end
    end

end

-- This one is needed to prevent bleeding of prevdepth to the next page
-- which doesn't work well with forced skips. I'm not that sure if the
-- following is a good way out.

do

    local outer   = tex.getnest(0)

    local enabled = true
    local trace   = false
    local report  = logs.reporter("vspacing")

    trackers.register("vspacing.synchronizepage",function(v)
        trace = v
    end)

    directives.register("vspacing.synchronizepage",function(v)
        enabled = v
    end)

    local function ignoredepth()
        return texgetdimen("ignoredepthcriterion") -- -65536000
    end

    -- A previous version analyzed the number of lines moved to the next page in
    -- synchronizepage because prevgraf is unreliable in that case. However, we cannot
    -- tweak that parameter because it is also used in postlinebreak and hangafter, so
    -- there is a danger for interference. Therefore we now do it dynamically.

    -- We can also support other lists but there prevgraf probably is ok.

    function vspacing.getnofpreviouslines(head)
        if enabled then
            if not thead then
                head = texgetlist("pagehead")
            end
            local noflines = 0
            if head then
                local tail = find_node_tail(tonut(head))
                while tail do
                    local id = getid(tail)
                    if id == hlist_code then
                        if getsubtype(tail) == linelist_code then
                            noflines = noflines + 1
                        else
                            break
                        end
                    elseif id == vlist_code then
                        break
                    elseif id == glue_code then
                        local subtype = getsubtype(tail)
                        if subtype == baselineskip_code or subtype == lineskip_code then
                            -- we're ok
                        elseif subtype == parskip_code then
                            if getwidth(tail) > 0 then
                                break
                            else
                                -- we assume we're ok
                            end
                        end
                    elseif id == penalty_code then
                        -- we're probably ok
                    elseif id == rule_code or id == kern_code then
                        break
                    else
                        -- ins, mark, boundary, whatsit
                    end
                    tail = getprev(tail)
                end
            end
            return noflines
        end
    end

    interfaces.implement {
        name    = "getnofpreviouslines",
        public  = true,
        actions = vspacing.getnofpreviouslines,
    }

    function vspacing.synchronizepage()
        if enabled then
            if trace then
                local newdepth = outer.prevdepth
                local olddepth = newdepth
                if not texgetlist("pagehead") then
                    newdepth = ignoredepth()
                    texset("prevdepth",newdepth)
                    outer.prevdepth = newdepth
                end
                report("page %i, prevdepth %p => %p",texgetcount("realpageno"),olddepth,newdepth)
             -- report("list %s",nodes.idsandsubtypes(head))
            else
                if not texgetlist("pagehead") then
                    local newdepth = ignoredepth()
                    texset("prevdepth",newdepth)
                    outer.prevdepth = newdepth
                end
            end
        end
    end

    local trace       = false
    local abs         = math.abs
 -- local last        = nil
    local getnodetail = nodes.tail

    local vmode_code <const> = tex.modelevels.vertical
    local temp_code  <const> = nodecodes.temp

 -- trackers.register("vspacing.forcestrutdepth",function(v) trace = v end)

    -- abs : negative is inner

    function vspacing.checkstrutdepth(depth)
        local nest = texgetnest()
        if abs(nest.mode) == vmode_code and nest.head then
            local tail = nest.tail
            local id   = tail.id
            if id == hlist_code then
                if tail.depth < depth then
                    tail.depth = depth
                end
                nest.prevdepth = depth
            elseif id == temp_code and texgetnest("ptr") == 0 then
             -- This has to be done better:
                local head = texgetlist("pagehead")
                if head then
                    tail = getnodetail(head)
                    if tail and tail.id == hlist_code then
                        if tail.depth < depth then
                            tail.depth = depth
                        end
                        nest.prevdepth = depth
                        -- only works in lmtx
                        texset("pagedepth",depth)
                    end
                end
            end
        end
    end

    interfaces.implement {
        name      = "checkstrutdepth",
        arguments = "dimension",
        actions   = vspacing.checkstrutdepth,
    }

 -- function vspacing.forcestrutdepth(n,depth,trace_mode,plus)
 --     local box = texgetbox(n)
 --     if box then
 --         box = tonut(box)
 --         local head = getlist(box)
 --         if head then
 --             local tail = find_node_tail(head)
 --             if tail then
 --                 if getid(tail) == hlist_code then
 --                     local dp = getdepth(tail)
 --                     if dp < depth then
 --                         setdepth(tail,depth)
 --                         outer.prevdepth = depth
 --                         if trace or trace_mode > 0 then
 --                             nuts.setvisual(tail,"depth")
 --                         end
 --                     end
 --                 end
 --              -- last = nil
 --              -- if plus then
 --              --     -- penalty / skip ...
 --              --     local height = 0
 --              --     local sofar  = 0
 --              --     local same   = false
 --              --     local seen   = false
 --              --     local list   = { }
 --              --           last   = nil
 --              --     while tail do
 --              --         local id = getid(tail)
 --              --         if id == hlist_code or id == vlist_code then
 --              --             local w, h, d = getwhd(tail)
 --              --             height = height + h + d + sofar
 --              --             sofar  = 0
 --              --             last   = tail
 --              --         elseif id == kern_code then
 --              --             sofar = sofar + getkern(tail)
 --              --         elseif id == glue_code then
 --              --             if seen then
 --              --                 sofar = sofar + getwidth(tail)
 --              --                 seen  = false
 --              --             else
 --              --                 break
 --              --             end
 --              --         elseif id == penalty_code then
 --              --             local p = getpenalty(tail)
 --              --             if p >= 10000 then
 --              --                 same = true
 --              --                 seen = true
 --              --             else
 --              --                 break
 --              --             end
 --              --         else
 --              --             break
 --              --         end
 --              --         tail = getprev(tail)
 --              --     end
 --              --     texsetdimen("global","d_spac_prevcontent",same and height or 0)
 --              -- end
 --             end
 --         end
 --     end
 -- end

    local hlist_code  <const> = nodes.nodecodes.hlist
    local insert_code <const> = nodes.nodecodes.insert
    local mark_code   <const> = nodes.nodecodes.mark
    local line_code   <const> = nodes.listcodes.line

 -- local nuts             = nodes.nuts
 -- local getid            = nuts.getid
 -- local getsubtype       = nuts.getsubtype
 -- local getdepth         = nuts.getdepth
 -- local setdepth         = nuts.setdepth
    local gettotal         = nuts.gettotal
    local getspeciallist   = nuts.getspeciallist
    local setspeciallist   = nuts.setspeciallist
    local getmvllist       = nuts.getmvllist
    local setmvllist       = nuts.setmvllist

    local triggerbuildpage = tex.triggerbuildpage

 -- local texgetdimen = tex.getdimen
 -- local texsetdimen = tex.setdimen
    local texgetnest  = tex.getnest
 -- local texget      = tex.get
 -- local texset      = tex.set

    local trace = false  trackers.register("otr.forcestrutdepth", function(v)
        trace = v and function(n)
            setvisual(nuts.tonut(n),nodes.visualizers.modes.depth)
        end
    end)

    local treversenode = nuts.treversers.node

    local function flushcontributions()
        if texgetnest("ptr") == 0 then
            -- this flushes the contributions
            local prev  = nil
            local cycle = 1
            while cycle <= 10 do
                local head = getspeciallist("contributehead")
                if head == prev then
                    -- This can happen .. maybe 10 is already too much ... e.g.
                    -- extreme side float case in m4all.
                    cycle = cycle + 1
                else
                    triggerbuildpage()
                    prev = head
                end
            end
            return true
        else
            return false
        end
    end

    vspacing.flushcontributions = flushcontributions

    function vspacing.forcestrutdepth()
        -- check if in mvl
        if flushcontributions() then
            -- now we consult the last line (if present)
            local head, tail = getspeciallist("pagehead")
            if tail then
                for n, id, subtype in treversenode, tail do
                    if id == hlist_code then
                        if subtype == line_code then
                            local strutdp = texgetdimen(d_strutdp)
                            local delta   = strutdp - getdepth(n)
                            if delta > 0 then
                                --- also pagelastdepth
                                setdepth(n,strutdp)
                                texset("pagetotal",texget("pagetotal") + delta)
                                texset("pagedepth",strutdp)
                                if trace then
                                    trace(n)
                                end
                            end
                        end
                        break
                    elseif id == insert_code or id == mark_code then
                        -- prev
                    else
                        break
                    end
                end
            end
        else
            local nest = texgetnest()
         -- if abs(nest.mode) == vmode_code and nest.head then
                local tail = nest.tail
                if tail.id == hlist_code and tail.subtype == line_code then
                    local strutdp = texgetdimen(d_strutdp)
                    if tail.depth < strutdp then
                        tail.depth = strutdp
                    end
                    nest.prevdepth = strutdp
                    if trace then
                        trace(tail)
                    end
                end
         -- end
        end
    end

    -- highly experimental, only for m4all now; todo: tracing

    local setbox = nuts.setbox

    function vspacing.interceptsamepagecontent(box)
        if vspacing.flushcontributions() then
            -- now we consult the last line (if present)
            local head, tail = getmvllist()
            if tail and getid(tail) == glue_code then
                local prev = getprev(tail)
                if prev and getid(prev) == penalty_code then
                    if getpenalty(prev) >= 10000 then
                        local state = nil
                        local first = nil
                        local last  = tail
                        local c = getprev(prev)
                        while c do
                            if getid(c) == glue_code then
                                local p = getprev(c)
                                if p and getid(p) == penalty_code then
                                    if getpenalty(p) < 10000 then
                                        state = 1
                                    end
                                else
                                    state = 2
                                    break
                                end
                            end
                            first = c
                            c = getprev(c)
                        end
                        if first and first ~= head then
                            setnext(getprev(first))
                            setprev(first)
                            local vbox = vpack_node(first)
                            setvisual(vbox)
                            setbox(box,vbox)
                            report_vspacing("same page intercept, case %i")
                        end
                    end
                end
            end
        end
    end

    interfaces.implement {
        name      = "interceptsamepagecontent",
        arguments = "integer",
        actions   = vspacing.interceptsamepagecontent,
    }

 -- interfaces.implement {
 --     name    = "removelastline",
 --     actions = function()
 --         local h, t = getspeciallist("pagehead")
 --         if t and hasidsubtype(t,hlist_code,line_code) then
 --             local total = gettotal(t)
 --             h = remove_node(h,t,true)
 --             setspeciallist(h)
 --             texset("pagetotal",texget("pagetotal") - total)
 --             -- set prevdepth
 --         end
 --     end
 -- }

    function vspacing.pushatsame()
        -- needs better checking !
        if last then -- setsplit
            nuts.setnext(getprev(last))
            nuts.setprev(last)
        end
    end

    function vspacing.popatsame()
        -- needs better checking !
        nuts.write(last)
    end

end

-- interface

do

    local implement = interfaces.implement

    implement {
        name      = "injectvspacing",
        actions   = vspacing.inject,
        arguments = { "integer", "string" },
    }

    implement {
        name      = "injectvpenalty",
        actions   = vspacing.injectpenalty,
        arguments = "integer",
    }

    implement {
        name      = "injectvskip",
        actions   = vspacing.injectskip,
        arguments = "dimension",
    }

    implement {
        name    = "injectdisable",
        actions = vspacing.injectdisable,
    }

    implement {
        name    = "injectnowhite",
        actions = vspacing.injectnowhite,
    }

    --

    implement {
        name      = "synchronizepage",
        actions   = vspacing.synchronizepage,
        scope     = "private"
    }

 -- implement {
 --     name      = "forcestrutdepth",
 --     arguments = { "integer", "dimension", "integer" },
 --     actions   = vspacing.forcestrutdepth,
 --     scope     = "private"
 -- }

 -- implement {
 --     name      = "forcestrutdepthplus",
 --     arguments = { "integer", "dimension", "integer", true },
 --     actions   = vspacing.forcestrutdepth,
 --     scope     = "private"
 -- }

    implement {
        name      = "forcestrutdepth",
        public    = true,
        protected = true,
        actions   = vspacing.forcestrutdepth,
    }

    implement {
        name      = "pushatsame",
        actions   = vspacing.pushatsame,
        scope     = "private"
    }

    implement {
        name      = "popatsame",
        actions   = vspacing.popatsame,
        scope     = "private"
    }

    implement {
        name      = "vspacingsetamount",
        actions   = vspacing.setskip,
        scope     = "private",
        arguments = "string",
    }

    implement {
        name      = "vspacingdefine",
        actions   = vspacing.setmap,
        scope     = "private",
        arguments = "2 strings",
    }

    implement {
        name      = "vspacingcollapse",
        actions   = vspacing.collapsevbox,
        scope     = "private",
        arguments = "integer"
    }

    implement {
        name      = "vspacingcollapseonly",
        actions   = vspacing.collapsevbox,
        scope     = "private",
        arguments = { "integer", true }
    }

    implement {
        name      = "vspacingsnap",
        actions   = vspacing.snapbox,
        scope     = "private",
        arguments = "2 integers",
    }

    local integer_value = tokens.values.integer

    implement {
        name      = "definesnapmethod",
        usage     = "value",
        actions   = function(key,value)
            return integer_value, vspacing.definesnapmethod(key,value)
        end,
        scope     = "private",
        arguments = "2 strings",
    }

    local buildpage = tex.triggerbuildpage

    implement {
        name    = "removelastline",
        actions = function()
            local head = texgetlist("pagehead")
            if head then
                local tail = find_node_tail(head)
                if tail then
                    -- maybe check for hlist subtype 1
                    local head = remove_node(head,tail,true)
                    texsetlist("pagehead", head)
                    buildpage()
                end
            end
        end
    }

    implement {
        name    = "showpagelist", -- will improve
        actions = function()
            local head = texgetlist("pagehead")
            if head then
                print("start")
                while head do
                    print("  " .. tostring(head))
                    head = head.next
                end
            end
        end
    }

    implement {
        name    = "pageoverflow",
        actions = { vspacing.pageoverflow, context }
    }

    implement {
        name    = "forcepageflush",
        actions = vspacing.forcepageflush
    }

    implement {
        name      = "injectzerobaselineskip",
        protected = true,
        public    = true,
        actions   = { nodes.pool.baselineskip, context },
    }

end

do

    local vboxhandler = builders.vspacing.vboxhandler

 -- inspect(tex.getdelayedgluetargetvalues())
 -- inspect(tex.getdelayedgluelocationvalues())

 -- local trace = false

--     local function delayed_glue_callback(target,location,head,tail)
-- --         if delaying and location == 0 and target == 2 then -- build and page
-- --             head = vboxhandler(head,"page")
-- --         end
--         if delay then
-- --             head = vboxhandler(head,location == 0 and "page" or "vbox")
--             head = vboxhandler(head,"vbox")
--         end
--         return head
--     end

--     callbacks.register {
--         name    = "delayed_glue",
--         action  = delayed_glue_callback,
--         comment = "vspace processing",
--         frozen  = true,
--     }

--     local enableaction  = nodes.tasks.enableaction
--     local disableaction = nodes.tasks.disableaction

 -- experiments.register("vspacing.delay",function(v)
 --     if delay then
 --      -- disableaction("vboxbuilders", "builders.vspacing.vboxhandler")
 --      -- disableaction("mvlbuilders",  "builders.vspacing.pagehandler")
 --         context.sprint(tex.prtcatcodes,"\\global\\c_spac_delay\\conditionaltrue") -- for now
 --     else
 --      -- enableaction("vboxbuilders", "builders.vspacing.vboxhandler")
 --      -- enableaction("mvlbuilders",  "builders.vspacing.pagehandler")
 --         context.sprint(tex.prtcatcodes,"\\global\\c_spac_delay\\conditionalfalse") -- for now
 --     end
 -- end)

 -- local checkdelayedglue = tex.checkdelayedglue
    local triggerbuildpage = tex.triggerbuildpage

 -- interfaces.implement {
 --     name      = "checkspacingbeforepar",
 --     public    = true,
 --     protected = true,
 --     actions   = function()
 --      -- if delay then
 --      --     checkdelayedglue(0, true)
 --      -- end
 --         triggerbuildpage()
 --     end
 -- }

    local d_spac_total <const> = tex.isdimen("d_spac_total")

 -- interfaces.implement {
 --     name      = "checkspacingavailable",
 --     public    = true,
 --     protected = true,
 --     actions   = function()
 --      -- if delay then
 --      --     checkdelayedglue(0, true)
 --      -- end
 --         triggerbuildpage()
 --      -- texsetdimen(d_spac_total,(delay and 0 or texget("parskip",true)) + texget("pagetotal"))
 --         texsetdimen(d_spac_total,texget("parskip",true) + texget("pagetotal"))
 --     end
 -- }

    interfaces.implement {
        name      = "checkspacingavailable",
        public    = true,
        protected = true,
        actions   = function()
            triggerbuildpage()
            texsetdimen(d_spac_total,texget("parskip",true) + texget("pagetotal"))
        end
    }

end
