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

-- no need for nuts in the one-line demo (that might move anyway)

local next = next
local insert, remove, sort = table.insert, table.remove, table.sort

builders                 = builders or { }
local builders           = builders

builders.paragraphs      = builders.paragraphs or { }
local parbuilders        = builders.paragraphs

parbuilders.constructors = parbuilders.constructors or { }
local       constructors = parbuilders.constructors

builders.hpack           = builders.hpack or { }
builders.vpack           = builders.vpack or { }

local hpackbuilders      = builders.hpack
local vpackbuilders      = builders.vpack

constructors.names       = constructors.names or { }
local names              = constructors.names

constructors.numbers     = constructors.numbers or { }
local numbers            = constructors.numbers

constructors.methods     = constructors.methods or { }
local methods            = constructors.methods

local a_parbuilder       <const> = attributes.numbers['parbuilder'] or 999 -- why 999
constructors.attribute   = a_parbuilder

local unsetvalue         <const> = attributes.unsetvalue

local texsetattribute    = tex.setattribute
local texnest            = tex.nest
local texget             = tex.get
local texset             = tex.set

local getspeciallist     = nodes.nuts.getspeciallist
local setspeciallist     = nodes.nuts.setspeciallist

local nodes              = nodes
local nodeidstostring    = nodes.idstostring
local nodepool           = nodes.pool
local new_baselineskip   = nodepool.baselineskip
local new_lineskip       = nodepool.lineskip
local insertnodebefore   = nodes.insertbefore
local hpack_node         = nodes.hpack

local nuts               = nodes.nuts
local getattr            = nuts.getattr
local flush              = nuts.flush

local registercallback   = callbacks.register

storage.register("builders/paragraphs/constructors/names",   names,   "builders.paragraphs.constructors.names")
storage.register("builders/paragraphs/constructors/numbers", numbers, "builders.paragraphs.constructors.numbers")

local trace_page_builder = false  trackers.register("builders.page", function(v) trace_page_builder = v end)
local trace_vbox_builder = false  trackers.register("builders.vbox", function(v) trace_vbox_builder = v end)
----- trace_par_builder  = false  trackers.register("builders.par",  function(v) trace_par_builder  = v end)

local report_page_builder = logs.reporter("builders","page")
local report_vbox_builder = logs.reporter("builders","vbox")
local report_par_builder  = logs.reporter("builders","par")

-- this will be redone ... we will never write a par builder in lua --

local mainconstructor = nil -- not stored in format
local nofconstructors = 0
local stack           = { }

function constructors.define(name)
    nofconstructors = nofconstructors + 1
    names[nofconstructors] = name
    numbers[name] = nofconstructors
end

function constructors.set(name) --- will go
    if name then
        mainconstructor = numbers[name] or unsetvalue
    else
        mainconstructor = stack[#stack] or unsetvalue
    end
    texsetattribute(a_parbuilder,mainconstructor)
    if mainconstructor ~= unsetvalue then
        constructors.enable()
    end
end

function constructors.start(name)
    local number = numbers[name]
    insert(stack,number)
    mainconstructor = number or unsetvalue
    texsetattribute(a_parbuilder,mainconstructor)
    if mainconstructor ~= unsetvalue then
        constructors.enable()
    end
 -- report_par_builder("start %a",name)
end

function constructors.stop()
    remove(stack)
    mainconstructor = stack[#stack] or unsetvalue
    texsetattribute(a_parbuilder,mainconstructor)
    if mainconstructor == unsetvalue then
        constructors.disable()
    end
 -- report_par_builder("stop")
end

-- return values:
--
-- true  : tex will break itself
-- false : idem but dangerous
-- head  : list of valid vmode nodes with last being hlist

function constructors.handler(head,followed_by_display)
    if type(head) == "boolean" then
        return head
    else
        local attribute = getattr(head,a_parbuilder) -- or mainconstructor
        if attribute then
            local method = names[attribute]
            if method then
                local handler = methods[method]
                if handler then
                    return handler(head,followed_by_display)
                else
                    report_par_builder("contructor method %a is not defined",tostring(method))
                    return true -- let tex break
                end
            end
        end
        return true -- let tex break
    end
end

-- just for testing

function constructors.methods.default(head,followed_by_display)
    return true -- let tex break
end

-- also for testing (now also surrounding spacing done)

function parbuilders.constructors.methods.oneline(head,followed_by_display)
    -- when needed we will turn this into a helper
    local t = texnest[texnest.ptr]
    local h = hpack_node(head)
    local d = texget("baselineskip",false) - t.prevdepth - h.height
    t.prevdepth = h.depth
    t.prevgraf  = 1
    if d < texget("lineskiplimit") then
        return insertnodebefore(h,h,new_lineskip(texget("lineskip",false))) -- no stretch etc
    else
        return insertnodebefore(h,h,new_baselineskip(d))
    end
end

-- It makes no sense to have a sequence here as we already have
-- pre and post hooks and only one parbuilder makes sense, so no:
--
-- local actions = nodes.tasks.actions("parbuilders")
--
-- yet ... maybe some day.

local actions = constructors.handler
local enabled = false

local function linebreak_callback(head,followed_by_display)
    -- todo: not again in otr so we need to flag
    if enabled then
        return actions(head,followed_by_display)
    else
        -- let tex do the work
    end
end

local id, enable, disable = callbacks.optimizer("linebreak")

function constructors.enable () enable()  enabled = true  end
function constructors.disable() disable() enabled = false end

registercallback {
    name    = "linebreak",
    action  = linebreak_callback,
    comment = "breaking paragraps into lines",
    frozen  = true,
    disable = true,
}

-- disable()

-- so far for the exprimental parbuilder hook

-- todo: move from nodes.builders to builders

nodes.builders = nodes.builder or { }
local builders = nodes.builders

local vpackactions = nodes.tasks.actions("vboxbuilders")

local function vpack_callback(head,groupcode,size,packtype,maxdepth,direction)
    if head then
        head = vpackactions(head,groupcode)
    end
    return head
end

-- This one is special in the sense that it has no head and we operate on the mlv. Also,
-- we need to do the vspacing last as it removes items from the mvl.

local pageactions = nodes.tasks.actions("mvlbuilders")
----- lineactions = nodes.tasks.actions("linebuilders")

local function report(pagecontext,head)
    report_page_builder("trigger: %s at level %i",pagecontext,texnest.ptr)
    report_page_builder("  vsize    : %p",texget("vsize"))
    report_page_builder("  pagegoal : %p",texget("pagegoal"))
    report_page_builder("  pagetotal: %p",texget("pagetotal"))
    report_page_builder("  list     : %s",head and nodeidstostring(head) or "<empty>")
end

-- Watch out: contributehead can be any head (kind of) not per se the page one
-- but that needs to be intercepted when needed by groupcode and level. At some
-- point this one might be split by group.

-- Tricky: these pointers can change in the callback in which case the
-- original lists can get messed up. This is the users responsibility!

-- local function buildpage_callback(pagecontext,head,tail,pagehead,pagetail,boundary)
--     local head, tail = getspeciallist("contributehead")
--     if head then
--         if trace_page_builder then
--             report(pagecontext,head)
--         end
--         head = pageactions(head,pagecontext) -- todo: tail
--         setspeciallist("contributehead", head)
--     else
--         if trace_page_builder then
--             report(pagecontext)
--         end
--     end
-- end

-- In the case of vspacing we actually don't want to trigger the page builder so
-- we need a way to signal that. This can also be a separate callback.

--     local nextnode               = nuts.traversers.node
--     local getattr                = nuts.getattr
--     local glue_code              = nodes.nodecodes.glue
--     local parskip_code           = nodes.gluecodes.parskip
--     local baselineskip_code      = nodes.gluecodes.baselineskip
--     local a_skipcategory <const> = attributes.private('skipcategory')
--     local function checkglues(head)
--         local found = false
--         for n, id, subtype in nextnode, head do
--             if id == glue_code and getattr(n,a_skipcategory) then
--                 found = true
--             elseif found then
--                 if id == glue_code and subtype == parskip_code then
--                 elseif id == glue_code and subtype == baselineskip_code then
--                 else
--                     -- rules and alignments and a few userskips
--                     print("MIX MIX MIX MIX MIX",nuts.tonode(n))
--                 end
--             else
--                 return false
--             end
--         end
--         --         print("ONLY ONLY ONLY")
--         return true
--     end

-- maybe hook the tracer into before

local function buildpage_callback(pagecontext,head,pagehead,pagetail,boundary)
    if trace_page_builder then
        report(pagecontext,head)
    end
    if head then
-- checkglues(head)
        -- One should leave pagehead and pagetail untouched!
        head = pageactions(head,pagecontext,pagehead,pagetail,boundary)
    end
    return head -- can be a direct
end

registercallback {
    name    = "vpack",
    action  = vpack_callback,
    comment = "vertical spacing etc",
    frozen  = true,
}

registercallback {
    name    = "buildpage",
    action  = buildpage_callback,
    comment = "vertical spacing etc (mvl)",
    frozen  = true,
}

local vboxactions = nodes.tasks.actions("vboxhandlers")

local function vbox_callback(head,groupcode)
    if head then
        head = vboxactions(head,groupcode)
    end
    return head
end

registercallback {
    name    = "packed_vbox",
    action  = vbox_callback,
    comment = "packed vbox treatments",
    frozen  = true,
}

-- statistics.register("v-node processing time", function()
--     return statistics.elapsedseconds(builders)
-- end)

local implement = interfaces.implement

implement { name = "defineparbuilder",  actions = constructors.define, arguments = "string" }
implement { name = "setparbuilder",     actions = constructors.set,    arguments = "string" }
implement { name = "startparbuilder",   actions = constructors.start,  arguments = "string" }
implement { name = "stopparbuilder",    actions = constructors.stop    }
implement { name = "enableparbuilder",  actions = constructors.enable  }
implement { name = "disableparbuilder", actions = constructors.disable }

-- Here are some tracers:

local nuts         = nodes.nuts
local setcolor     = nodes.tracers.colors.set
local listtoutf    = nodes.listtoutf
local new_kern     = nuts.pool.kern
local new_rule     = nuts.pool.rule
local hpack        = nuts.hpack
local getheight    = nuts.getheight
local getdepth     = nuts.getdepth
local getdirection = nuts.getdirection
local getlist      = nuts.getlist
local setwidth     = nuts.setwidth
local setdirection = nuts.setdirection
local setlink      = nuts.setlink

local list         = { }

local report_quality = logs.reporter("pack quality")

statistics.feedback.register { name = "vpack quality", index = 12, category = "problem" }
statistics.feedback.register { name = "hpack quality", index = 11, category = "problem" }

-- overflow|badness w h d dir

local report  = true   trackers.register("builders.vpack.quality", function(v) report  = v end)
local collect = false  trackers.register("builders.vpack.collect", function(v) collect = v end)
----  show    = false  trackers.register("builders.vpack.overflow",function(v) show    = v end)

function vpackbuilders.report(how,detail,n,first,last,filename) -- no return value
    if report then
        if last <= 0 then
            if how == "overfull" then
                report_quality("%s vbox (%p)",how,detail)
            else
                report_quality("%s vbox",how)
            end
        elseif first > 0 and first < last then
            if how == "overfull" then
                report_quality("%s vbox at line %i - %i in file %a (%p)",how,first,last,filename or "?",detail)
            else
                report_quality("%s vbox at line %i - %i in file %a",how,first,last,filename or "?")
            end
        else
            if how == "overfull" then
                report_quality("%s vbox at line %i in file %a (%p)",how,last,filename or "?",detail)
            else
                report_quality("%s vbox at line %i in file %a",how,last,filename or "?")
            end
        end
    end
    if collect then
        list[#list+1] = { "vbox", how, filename, first, last, how, detail }
    end
    statistics.feedback.setstate("vpack quality",true)
end

-- function builders.vpack.show(how,detail,n,first,last,filename) -- return value
--     if show then
--     end
-- end

local report  = true   trackers.register("builders.hpack.quality", function(v) report  = v end)
local collect = false  trackers.register("builders.hpack.collect", function(v) collect = v end)
local show    = false  trackers.register("builders.hpack.overflow",function(v) show    = v end)

function hpackbuilders.report(how,detail,n,first,last,filename) -- no return value
    local str = (report or collect) and listtoutf(getlist(n),"",true,nil,true)
    if report then
        local overfull = how == "overfull"
        if last <= 0 then
            report_quality(
                overfull and "%s hbox: %s (%p)" or "%s hbox: %s (badness %i)",
                how,str,detail
            )
        elseif first > 0 and first < last then
            report_quality(
                overfull and "%s hbox at line %i - %i in file %a: %s (%p)" or "%s hbox at line %i - %i in file %a: %s (badness %i)",
                how,first,last,filename or "?",str,detail
            )
        else
            report_quality(
                overfull and "%s hbox at line %i in file %a: %s (%p)" or "%s hbox at line %i in file %a: %s (badness %i)",
                how,last,filename or "?",str,detail
            )
        end
    end
    if collect then
        list[#list+1] = { "hbox", how, filename, first, last, str, detail }
    end
    statistics.feedback.setstate("hpack quality",true)
end

function hpackbuilders.show(how,detail,n,first,last,filename,rule) -- return value
    if show then
        local width     = 2*65536
        local height    = getheight(n)
        local depth     = getdepth(n)
        local direction = getdirection(n)
        if height < 4*65526 then
            height = 4*65526
        end
        if depth < 2*65526 then
            depth = 2*65526
        end
        if rule then
            flush(rule)
        end
        rule = new_rule(width,height,depth)
        setdirection(rule,direction)
        if how == "overfull" then
            setcolor(rule,"red")
            local kern = new_kern(-detail)
            setlink(kern,rule)
            rule = kern
        elseif how == "underfull" then
            setcolor(rule,"blue")
        elseif how == "loose" then
            setcolor(rule,"magenta")
        elseif how == "tight" then
            setcolor(rule,"cyan")
        end
        rule = hpack(rule)
        setwidth(rule,0) -- maybe better whd all zero
        setdirection(rule,direction)
    else
        local width = texget("overfullrule")
        if width > 0 then
            rule = new_rule(width)
        end
    end
    return rule
end

--

local hqualityactions = nodes.tasks.actions("hquality")
local vqualityactions = nodes.tasks.actions("vquality")

function hpackbuilders.qualityactions(how,detail,n,first,last,filename)
    local rul = nil
    rul = hqualityactions(how,detail,n,first,last,filename)
    return rul
end

function vpackbuilders.qualityactions(how,detail,n,first,last,filename)
    local rul = nil
    rul = vqualityactions(how,detail,n,first,last,filename)
    return rul
end

registercallback {
    name    = "hpack_quality",
    action  = hpackbuilders.qualityactions,
    comment = "report horizontal packing quality",
    frozen  = true,
}

registercallback {
    name    = "vpack_quality",
    action  = vpackbuilders.qualityactions,
    comment = "report vertical packing quality",
    frozen  = true,
}

statistics.register("quality reports", function()
    local n = #list
    if n > 0 then
        local t = table.setmetatableindex("number")
        local fw = 0
        local hw = 0
        for i=1,n do
            local f = list[i][1]
            local h = list[i][2]
            if #f > fw then
                fw = #f
            end
            if #h > hw then
                hw = #h
            end
            t[h] = t[h] + 1
        end
        logs.startfilelogging(report_quality)
        for i=1,n do
            local l   = list[i]
            local how = l[2]
            report_quality(
                how == "overfull"
                    and "%-" .. fw .. "s [%04i - %04i] : %-" .. hw .. "s %s : %s (%p)"
                     or "%-" .. fw .. "s [%04i - %04i] : %-" .. hw .. "s %s : %s (badness %i)",
                file.basename(l[3]),l[4],l[5],how,l[1],l[6],l[7]
            )
        end
        logs.stopfilelogging()
        report_quality()
        report_quality("%i entries added to the log file : %s",n,table.sequenced(t))
        report_quality()
    end
end)

do

    local loners = { }
    local report = logs.reporter("loners")

    local texgetcount = tex.getcount

    local function show_loners_callback(options,penalty)
        loners[#loners+1] = { texgetcount("realpageno"), options, penalty }
    end

    trackers.register("pages.loners",function(v)
        registercallback {
            name   = "show_loners",
            action = v and show_loners_callback or nil,
        }
    end)

    logs.registerfinalactions(function()
        local n = #loners
        if n > 0 then
            local options = tex.getpenaltyoptionvalues()
            options[0] = nil
            logs.startfilelogging(report,"page break penalties (loners)")
            for i=1,n do
                local l = loners[i]
                local t = { }
                local o = l[2]
                for k, v in next, options do
                    if o & k == k then t[#t+1] = v end
                end
                if #t > 0 then
                    sort(t)
                    report("  %4i: penalty %5i, % + t,",l[1],l[3],t)
                end
            end
            logs.stopfilelogging()
        end
    end)

    statistics.register("page breaks", function()
        local n = #loners
        if n > 0 then
            local pages = { }
            for i=1,n do
                pages[#pages+1] = loners[i][1]
            end
            return string.formatters("%i pages have loners reported: % t",n,pages)
        end
    end)

end
