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

-- todo: cleanup stack (structures.marks.reset(v_all) also does the job)
-- todo: only commands.* print to tex, native marks return values

local insert, concat = table.insert, table.concat
local tostring, next, rawget = tostring, next, rawget
local lpegmatch = lpeg.match
local match = string.match

local allocate           = utilities.storage.allocate
local setmetatableindex  = table.setmetatableindex

local nodecodes          = nodes.nodecodes
local glyph_code         = nodecodes.glyph
local hlist_code         = nodecodes.hlist
local vlist_code         = nodecodes.vlist

local getattribute       = nodes.getattribute
local traversenodes      = node.traverse
local texsetattribute    = tex.setattribute
local texbox             = tex.box

local a_marks            = attributes.private("structure","marks")

local trace_marks_set    = false  trackers.register("marks.set",    function(v) trace_marks_set = v end)
local trace_marks_get    = false  trackers.register("marks.get",    function(v) trace_marks_get = v end)
local trace_marks_all    = false  trackers.register("marks.detail", function(v) trace_marks_all = v end)

local report_marks       = logs.reporter("structure","marks")

local variables          = interfaces.variables

local v_first            = variables.first
local v_last             = variables.last
local v_previous         = variables.previous
local v_next             = variables.next
local v_top              = variables.top
local v_bottom           = variables.bottom
local v_current          = variables.current
local v_default          = variables.default
local v_page             = variables.page
local v_all              = variables.all
local v_keep             = variables.keep

local v_nocheck_suffix   = ":" .. variables.nocheck

local v_first_nocheck    = variables.first    .. v_nocheck_suffix
local v_last_nocheck     = variables.last     .. v_nocheck_suffix
local v_previous_nocheck = variables.previous .. v_nocheck_suffix
local v_next_nocheck     = variables.next     .. v_nocheck_suffix
local v_top_nocheck      = variables.top      .. v_nocheck_suffix
local v_bottom_nocheck   = variables.bottom   .. v_nocheck_suffix

local structures         = structures
local marks              = structures.marks
local lists              = structures.lists

local settings_to_array  = utilities.parsers.settings_to_array

marks.data               = marks.data or allocate()

storage.register("structures/marks/data", marks.data, "structures.marks.data")

local data = marks.data
local stack, topofstack = { }, 0

local ranges = {
    [v_page] = {
        first = 0,
        last  = 0,
    },
}

local function resolve(t,k)
    if k then
        if trace_marks_set or trace_marks_get then
            report_marks("undefined: name=%s",k)
        end
        local crap = { autodefined = true }
        t[k] = crap
        return crap
    else
        -- weird: k is nil
    end
end

setmetatableindex(data, resolve)

function marks.exists(name)
    return rawget(data,name) ~= nil
end

-- identify range

local function sweep(head,first,last)
    for n in traversenodes(head) do
        local id = n.id
        if id == glyph_code then
            local a = getattribute(n,a_marks)
            if not a then
                -- next
            elseif first == 0 then
                first, last = a, a
            elseif a > last then
                last = a
            end
        elseif id == hlist_code or id == vlist_code then
            local a = getattribute(n,a_marks)
            if not a then
                -- next
            elseif first == 0 then
                first, last = a, a
            elseif a > last then
                last = a
            end
            local list = n.list
            if list then
                first, last = sweep(list, first, last)
            end
        end
    end
    return first, last
end

local classes = { }

setmetatableindex(classes, function(t,k) local s = settings_to_array(k) t[k] = s return s end)

local lasts = { }

function marks.synchronize(class,n,option)
    local box = texbox[n]
    if box then
        local first, last = sweep(box.list,0,0)
        if option == v_keep and first == 0 and last == 0 then
            if trace_marks_get or trace_marks_set then
                report_marks("synchronize: class=%s, box=%s, retaining",class,n)
            end
            -- todo: check if still valid firts/last in range
            first = lasts[class] or 0
            last = first
        else
            lasts[class] = last
            local classlist = classes[class]
            for i=1,#classlist do
                local class = classlist[i]
                local range = ranges[class]
                if not range then
                    range = { }
                    ranges[class] = range
                end
                range.first, range.last = first, last
                if trace_marks_get or trace_marks_set then
                    report_marks("synchronize: class=%s, first=%s, last=%s",class,range.first,range.last)
                end
            end
        end
    elseif trace_marks_get or trace_marks_set then
        report_marks("synchronize: class=%s, box=%s, no content",class,n)
    end
end

-- define etc

local function resolve(t,k)
    if k == "fullchain" then
        local fullchain = { }
        local chain = t.chain
        while chain and chain ~= "" do
            insert(fullchain,1,chain)
            chain = data[chain].chain
        end
        t[k] = fullchain
        return fullchain
    elseif k == "chain" then
        t[k] = ""
        return ""
    elseif k == "reset" or k == "set" then
        t[k] = 0
        return 0
    elseif k == "parent" then
        t[k] = false
        return false
    end
end

function marks.define(name,settings)
    settings = settings or { }
    data[name] = settings
    local parent = settings.parent
    if parent == nil or parent == "" or parent == name then
        settings.parent = false
    else
        local dp = data[parent]
        if not dp then
            settings.parent = false
        elseif dp.parent then
            settings.parent = dp.parent
        end
    end
    setmetatableindex(settings, resolve)
end

for k, v in next, data do
    setmetatableindex(v,resolve) -- runtime loaded table
end

local function parentname(name)
    local dn = data[name]
    return dn and dn.parent or name
end

function marks.relate(name,chain)
    local dn = data[name]
    if dn and not dn.parent then
        if chain and chain ~= "" then
            dn.chain = chain
            local dc = data[chain]
            if dc then
                local children = dc.children
                if not children then
                    children = { }
                    dc.children = children
                end
                children[#children+1] = name
            end
        elseif trace_marks_set then
            report_marks("invalid relation: name=%s, chain=%s",name,chain or "-")
        end
    end
end

local function resetchildren(new,name)
    local dn = data[name]
    if dn and not dn.parent then
        local children = dn.children
        if children then
            for i=1,#children do
                local ci = children[i]
                new[ci] = false
                if trace_marks_set then
                    report_marks("reset: parent=%s, child=%s",name,ci)
                end
                resetchildren(new,ci)
            end
        end
    end
end

function marks.set(name,value)
    local dn = data[name]
    if dn then
        local child = name
        local parent = dn.parent
        if parent then
            name = parent
            dn = data[name]
        end
        dn.set = topofstack
        local top = stack[topofstack]
        local new = { }
        if top then
            for k, v in next, top do
                local d = data[k]
                local r = d.reset
                local s = d.set
                if r <= topofstack and s < r then
                    new[k] = false
                else
                    new[k] = v
                end
            end
        end
        resetchildren(new,name)
        new[name] = value
        topofstack = topofstack + 1
        stack[topofstack] = new
        if trace_marks_set then
            if name == child then
                report_marks("set: name=%s, index=%s, value=%s",name,topofstack,value)
            else
                report_marks("set: parent=%s, child=%s, index=%s, value=%s",parent,child,topofstack,value)
            end
        end
        texsetattribute("global",a_marks,topofstack)
    end
end

local function reset(name)
    if v_all then
        if trace_marks_set then
            report_marks("reset: all")
        end
        stack = { }
        for name, dn in next, data do
            local parent = dn.parent
            if parent then
                dn.reset = 0
                dn.set = 0
            end
        end
    else
        local dn = data[name]
        if dn then
            local parent = dn.parent
            if parent then
                name = parent
                dn = data[name]
            end
            if trace_marks_set then
                report_marks("reset: name=%s, index=%s",name,topofstack)
            end
            dn.reset = topofstack
            local children = dn.children
            if children then
                for i=1,#children do
                    local ci = children[i]
                    reset(ci)
                end
            end
        end
    end
end

marks.reset = reset

function marks.get(n,name,value)
    local dn = data[name]
    if dn then
        name = dn.parent or name
        local top = stack[n]
        if top then
            context(top[name])
        end
    end
end

function marks.show(first,last)
    if first and last then
        for k=first,last do
            local v = stack[k]
            if v then
                report_marks("% 4i: %s",k,table.sequenced(v))
            end
        end
    else
        for k, v in table.sortedpairs(stack) do
            report_marks("% 4i: %s",k,table.sequenced(v))
        end
    end
end

local function resolve(name,first,last,strict,quitonfalse,notrace)
    local dn = data[name]
    if dn then
        local child = name
        local parent = dn.parent
        name = parent or child
        dn = data[name]
        local step, method
        if first > last then
            step, method = -1, "bottom-up"
        else
            step, method = 1, "top-down"
        end
        if trace_marks_get and not notrace then
            report_marks("request: strategy=%s, name=%s, parent=%s, strict=%s",method,child,parent or "",tostring(strict or false))
        end
        if trace_marks_all and not notrace then
            marks.show(first,last)
        end
        local r = dn.reset
        local s = dn.set
        if first <= last and first <= r then
            if trace_marks_get and not notrace then
                report_marks("reset (first case): name=%s, first=%s, last=%s, reset=%s, index=%s",name,first,last,r,first)
            end
        elseif first >= last and last <= r then
            if trace_marks_get and not notrace then
                report_marks("reset (last case): name=%s, first=%s, last=%s, reset=%s, index=%s",name,first,last,r,last)
            end
        elseif not stack[first] or not stack[last] then
            if trace_marks_get and not notrace then
                -- a previous or next method can give an out of range, which is valid
                report_marks("out of range: name=%s, reset=%s, index=%s",name,r,first)
            end
        elseif strict then
            local top = stack[first]
            local fullchain = dn.fullchain
            if not fullchain or #fullchain == 0 then
                if trace_marks_get and not notrace then
                    report_marks("no full chain, trying: name=%s, first=%s, last=%s",name,first,last)
                end
                return resolve(name,first,last)
            else
                if trace_marks_get and not notrace then
                    report_marks("found chain: %s",concat(fullchain," => "))
                end
                local chaindata, chainlength = { }, #fullchain
                for i=1,chainlength do
                    local cname = fullchain[i]
                    if data[cname].set > 0 then
                        local value = resolve(cname,first,last,false,false,true)
                        if value == "" then
                            if trace_marks_get and not notrace then
                                report_marks("quit chain: name=%s, reset=%s, start=%s",name,r,first)
                            end
                            return ""
                        else
                            chaindata[i] = value
                        end
                    end
                end
                if trace_marks_get and not notrace then
                    report_marks("chain list: %s",concat(chaindata," => "))
                end
                local value, index, found = resolve(name,first,last,false,false,true)
                if value ~= ""  then
                    if trace_marks_get and not notrace then
                        report_marks("following chain: %s",concat(fullchain," => "))
                    end
                    for i=1,chainlength do
                        local cname = fullchain[i]
                        if data[cname].set > 0 and chaindata[i] ~= found[cname] then
                            if trace_marks_get and not notrace then
                                report_marks("empty in chain: name=%s, reset=%s, index=%s",name,r,first)
                            end
                            return ""
                        end
                    end
                    if trace_marks_get and not notrace then
                        report_marks("found: name=%s, reset=%s, start=%s, index=%s, value=%s",name,r,first,index,value)
                    end
                    return value, index, found
                elseif trace_marks_get and not notrace then
                    report_marks("not found: name=%s, reset=%s",name,r)
                end
            end
        else
            for i=first,last,step do
                local current = stack[i]
                local value = current and current[name]
                if value == nil then
                    -- search on
                elseif value == false then
                    if quitonfalse then
                        return ""
                    end
                elseif value == true then
                    if trace_marks_get and not notrace then
                        report_marks("quit: name=%s, reset=%s, start=%s, index=%s",name,r,first,i)
                    end
                    return ""
                elseif value ~= "" then
                    if trace_marks_get and not notrace then
                        report_marks("found: name=%s, reset=%s, start=%s, index=%s, value=%s",name,r,first,i,value)
                    end
                    return value, i, current
                end
            end
            if trace_marks_get and not notrace then
                report_marks("not found: name=%s, reset=%s",name,r)
            end
        end
    end
    return ""
end

-- todo: column:first column:last

local methods  = { }

local function doresolve(name,rangename,swap,df,dl,strict)
    local range = ranges[rangename] or ranges[v_page]
    local first, last = range.first, range.last
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, swap=%s, first=%s, last=%s, df=%s, dl=%s, strict=%s",
            name,rangename,tostring(swap or false),first,last,df,dl,tostring(strict or false))
    end
    if swap then
        first, last = last + df, first + dl
    else
        first, last = first + df, last + dl
    end
    local value, index, found = resolve(name,first,last,strict)
    -- maybe something more
    return value, index, found
end

-- previous : last before sync
-- next     : first after sync

-- top      : first in sync
-- bottom   : last in sync

-- first    : first not top in sync
-- last     : last not bottom in sync

methods[v_previous]         = function(name,range) return doresolve(name,range,false,-1,0,true ) end -- strict
methods[v_top]              = function(name,range) return doresolve(name,range,false, 0,0,true ) end -- strict
methods[v_bottom]           = function(name,range) return doresolve(name,range,true , 0,0,true ) end -- strict
methods[v_next]             = function(name,range) return doresolve(name,range,true , 0,1,true ) end -- strict

methods[v_previous_nocheck] = function(name,range) return doresolve(name,range,false,-1,0,false) end
methods[v_top_nocheck]      = function(name,range) return doresolve(name,range,false, 0,0,false) end
methods[v_bottom_nocheck]   = function(name,range) return doresolve(name,range,true , 0,0,false) end
methods[v_next_nocheck]     = function(name,range) return doresolve(name,range,true , 0,1,false) end

local function do_first(name,range,check)
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, resolve first",name,range)
    end
    local f_value, f_index, f_found = doresolve(name,range,false,0,0,check)
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, resolve last",name,range)
    end
    local l_value, l_index, l_found = doresolve(name,range,true ,0,0,check)
    if f_found and l_found and l_index > f_index then
        local name = parentname(name)
        for i=f_index,l_index,1 do
            local si = stack[i]
            local sn = si[name]
            if sn and sn ~= false and sn ~= true and sn ~= "" and sn ~= f_value then
                if trace_marks_get then
                    report_marks("resolve: name=%s, range=%s, index=%s, value=%s",name,range,i,sn)
                end
                return sn, i, si
            end
        end
    end
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, using first",name,range)
    end
    return f_value, f_index, f_found
end

local function do_last(name,range,check)
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, resolve first",name,range)
    end
    local f_value, f_index, f_found = doresolve(name,range,false,0,0,check)
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, resolve last",name,range)
    end
    local l_value, l_index, l_found = doresolve(name,range,true ,0,0,check)
    if f_found and l_found and l_index > f_index then
        local name = parentname(name)
        for i=l_index,f_index,-1 do
            local si = stack[i]
            local sn = si[name]
            if sn and sn ~= false and sn ~= true and sn ~= "" and sn ~= l_value then
                if trace_marks_get then
                    report_marks("resolve: name=%s, range=%s, index=%s, value=%s",name,range,i,sn)
                end
                return sn, i, si
            end
        end
    end
    if trace_marks_get then
        report_marks("resolve: name=%s, range=%s, using last",name,range)
    end
    return l_value, l_index, l_found
end

methods[v_first        ] = function(name,range) return do_first(name,range,true ) end
methods[v_last         ] = function(name,range) return do_last (name,range,true ) end
methods[v_first_nocheck] = function(name,range) return do_first(name,range,false) end
methods[v_last_nocheck ] = function(name,range) return do_last (name,range,false) end

methods[v_current] = function(name,range) -- range is ignored here
    local top = stack[topofstack]
    return top and top[parentname(name)] or ""
end

local function fetched(name,range,method)
    local value = (methods[method] or methods[v_first])(name,range) or ""
    if not trace_marks_get then
        -- no report
    elseif value == "" then
        report_marks("nothing fetched: name=%s, range=%s, method=%s",name,range,method)
    else
        report_marks("marking fetched: name=%s, range=%s, method=%s, value=%s",name,range,method,value)
    end
    return value or ""
end

-- can be used at the lua end:

marks.fetched = fetched

-- this will move to a separate runtime modules

marks.tracers = marks.tracers or { }

function marks.tracers.showtable()
    context.starttabulate { "|l|l|l|lp|lp|" }
    context.tabulaterowbold("name","parent","chain","children","fullchain")
    context.ML()
    for k, v in table.sortedpairs(data) do
        local parent, chain, children, fullchain = v.parent or "", v.chain or "", v.children or { }, v.fullchain or { }
        table.sort(children) -- in-place but harmless
        context.tabulaterowtyp(k,parent,chain,concat(children," "),concat(fullchain," "))
    end
    context.stoptabulate()
end

-- pushing to context:

local separator = context.nested.markingseparator
local command   = context.nested.markingcommand
local ctxconcat = context.concat

local function fetchonemark(name,range,method)
    context(command(name,fetched(name,range,method)))
end

local function fetchtwomarks(name,range)
    ctxconcat( {
        command(name,fetched(name,range,v_first)),
        command(name,fetched(name,range,v_last)),
    }, separator(name))
end

local function fetchallmarks(name,range)
    ctxconcat( {
        command(name,fetched(name,range,v_previous)),
        command(name,fetched(name,range,v_first)),
        command(name,fetched(name,range,v_last)),
    }, separator(name))
end

function marks.fetch(name,range,method) -- chapter page first | chapter column:1 first
    if trace_marks_get then
        report_marks("marking asked: name=%s, range=%s, method=%s",name,range,method)
    end
    if method == "" or method == v_default then
        fetchonemark(name,range,v_first)
    elseif method == v_both then
        fetchtwomarks(name,range)
    elseif method == v_all then
        fetchallmarks(name,range)
    else
        fetchonemark(name,range,method)
    end
end

function marks.fetchonemark (name,range,method) fetchonemark (name,range,method) end
function marks.fetchtwomarks(name,range)        fetchtwomarks(name,range       ) end
function marks.fetchallmarks(name,range)        fetchallmarks(name,range       ) end

-- here we have a few helpers .. will become commands.*

function marks.title(tag,n)
    local listindex = match(n,"^li::(.-)$")
    if listindex then
        commands.savedlisttitle(tag,listindex,"marking")
    else
        context(n)
    end
end

function marks.number(tag,n) -- no spec
    local listindex = match(n,"^li::(.-)$")
    if listindex then
        commands.savedlistnumber(tag,listindex)
    else
        -- no prefix (as it is the prefix)
        context(n)
    end
end

-- interface

commands.definemarking      = marks.define
commands.relatemarking      = marks.relate
commands.setmarking         = marks.set
commands.resetmarking       = marks.reset
commands.synchronizemarking = marks.synchronize
commands.getmarking         = marks.fetch
commands.fetchonemark       = marks.fetchonemark
commands.fetchtwomarks      = marks.fetchtwomarks
commands.fetchallmarks      = marks.fetchallmarks

function commands.doifelsemarking(str) -- can be shortcut
    commands.testcase(marks.exists(str))
end

