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

-- when using rotation: \disabledirectives[refences.sharelinks] (maybe flag links)

-- todo: /AA << WC << ... >> >> : WillClose actions etc

-- Internal references are indicated by a number (and turned into <autoprefix><number>)
-- we only flush internal destinations that are referred.

-- In the end one can wonder if it was worth the effort to keep up with annotations.
-- Where acrobat at least supports most (can differ per version) other viewers
-- support partial. For instance sumatra (that I use for testing) can open an
-- external links but doesn't go to the page. So don't report issues before making
-- sure that it's not your browser that fails. One cannto complain about what comes
-- for free (browsers) but one should also not praise non commercial software to
-- much without looking critical at how it performs.

local next, tostring, tonumber, rawget, type = next, tostring, tonumber, rawget, type
local rep, format, find, match = string.rep, string.format, string.find, string.match
local min, max = math.min, math.max
local lpegmatch = lpeg.match
local formatters = string.formatters
local sortedkeys, concat, swapped = table.sortedkeys, table.concat, table.swapped

local trace_references        = false  trackers.register("references.references",   function(v) trace_references   = v end)
local trace_destinations      = false  trackers.register("references.destinations", function(v) trace_destinations = v end)
local trace_bookmarks         = false  trackers.register("references.bookmarks",    function(v) trace_bookmarks    = v end)
local trace_externals         = false  trackers.register("references.externals",    function(v) trace_externals    = v end)

local log_destinations        = false  directives.register("destinations.log",     function(v) log_destinations = v end)
local untex_urls              = true   directives.register("references.untexurls", function(v) untex_urls       = v end)

local report_references       = logs.reporter("backend","references")
local report_destinations     = logs.reporter("backend","destinations")
local report_bookmarks        = logs.reporter("backend","bookmarks")

local variables               = interfaces.variables
local v_auto                  <const> = variables.auto
local v_page                  <const> = variables.page
local v_name                  <const> = variables.name

local factor                  <const> = number.dimenfactors.bp

local settings_to_array       = utilities.parsers.settings_to_array

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

local pdfbackend              = backends.registered.pdf
local nodeinjections          = pdfbackend.nodeinjections
local codeinjections          = pdfbackend.codeinjections
local registrations           = pdfbackend.registrations

local javascriptcode          = interactions.javascripts.code

local references              = structures.references
local bookmarks               = structures.bookmarks

local flaginternals           = references.flaginternals
local usedinternals           = references.usedinternals
local usedviews               = references.usedviews

local runners                 = references.runners
local specials                = references.specials
local handlers                = references.handlers
local executers               = references.executers

local nodepool                = nodes.pool

local new_latelua             = nodepool.latelua

local texgetcount             = tex.getcount

local jobpositions            = job.positions
local getpos                  = jobpositions.getpos
local gethpos                 = jobpositions.gethpos
local getvpos                 = jobpositions.getvpos

local lpdf                    = lpdf
local pdfdictionary           = lpdf.dictionary
local pdfarray                = lpdf.array
local pdfreference            = lpdf.reference
local pdfunicode              = lpdf.unicode
local pdfstring               = lpdf.string
local pdfconstant             = lpdf.constant
local pdfnull                 = lpdf.null
local pdfaddtocatalog         = lpdf.addtocatalog
local pdfaddtonames           = lpdf.addtonames
local pdfaddtopageattributes  = lpdf.addtopageattributes
local pdfrectangle            = lpdf.rectangle
local pdfquads                = lpdf.quads

local pdfflushobject          = lpdf.flushobject
local pdfshareobjectreference = lpdf.shareobjectreference
local pdfreserveobject        = lpdf.reserveobject
local pdfpagereference        = lpdf.pagereference
local pdfdelayedobject        = lpdf.delayedobject
local pdfmajorversion         = lpdf.majorversion

-- defined later on:

local pdfregisterannotation

-- todo: 3dview

----- pdf_annot               = pdfconstant("Annot")
local pdf_uri                 = pdfconstant("URI")
local pdf_gotor               = pdfconstant("GoToR")
local pdf_goto                = pdfconstant("GoTo")
local pdf_launch              = pdfconstant("Launch")
local pdf_javascript          = pdfconstant("JavaScript")
local pdf_highlight           = pdfconstant("Highlight")
local pdf_link                = pdfconstant("Link")
local pdf_n                   = pdfconstant("N")
local pdf_t                   = pdfconstant("T")
local pdf_fit                 = pdfconstant("Fit")
local pdf_named               = pdfconstant("Named")

local autoprefix              = "#"
local usedautoprefixes        = { }

local crappytaggingmode       = 0

updaters.register("structures.tagging",function(version)
    if tex.conditionals.c_strc_tags_global then
        crappytaggingmode = tonumber(version) or 0
    end
end)

function codeinjections.setautoprefix(prefix)
    autoprefix = prefix ~= "" and prefix or autoprefix
end

local function registerautoprefix(name)
    local internal = autoprefix .. name
    if usedautoprefixes[internal] == nil then
        usedautoprefixes[internal] = false
    end
    return internal
end

local function useautoprefix(name)
    local internal = autoprefix .. name
    usedautoprefixes[internal] = true
    return internal
end

local function checkautoprefixes(destinations)
    for k, v in next, usedautoprefixes do
        if not v then
            if trace_destinations then
                report_destinations("flushing unused autoprefix %a",k)
            end
            destinations[k] = nil
        end
    end
end

-- officially newlines are supposed to be ignored in sorting and there might
-- be escapes but we can decide to unicode them

local pdfmakenametree  do

    local maxslice = 32 -- could be made configureable ... 64 is also ok
    local convert  = pdfstring

    directives.register("backend.pdf.unicodenames",function(v)
        convert = v and pdfunicode or pdfstring
    end)

    pdfmakenametree = function(list,apply)
        if not next(list) then
            return
        end
        local slices   = { }
        local sorted   = sortedkeys(list)
        local size     = #sorted
        local maxslice = maxslice
        if size <= 1.5*maxslice then
            maxslice = size
        end
        for i=1,size,maxslice do
            local amount = min(i+maxslice-1,size)
            local names  = pdfarray { }
            local n      = 0
            for j=i,amount do
                local name   = sorted[j]
                local target = list[name]
                n = n + 1 ; names[n] = convert(name)
                n = n + 1 ; names[n] = apply and apply(target) or target
            end
            local first = sorted[i]
            local last  = sorted[amount]
            local limits = pdfarray {
                convert(first),
                convert(last),
            }
            local d = pdfdictionary {
                Names  = names,
                Limits = limits,
            }
            slices[#slices+1] = {
                reference = pdfreference(pdfflushobject(d)),
                limits    = limits,
            }
        end
        local function collectkids(slices,first,last)
            local f = slices[first]
            local l = slices[last]
            if f and l then
                local k = pdfarray()
                local n = 0
                local d = pdfdictionary {
                    Kids   = k,
                    Limits = pdfarray {
                        f.limits[1],
                        l.limits[2],
                    },
                }
                for i=first,last do
                    n = n + 1 ; k[n] = slices[i].reference
                end
                return d
            end
        end
        if #slices == 1 then
            return slices[1].reference
        else
            while true do
                local size = #slices
                if size > maxslice then
                    local temp = { }
                    local n    = 0
                    for i=1,size,maxslice do
                        local kids = collectkids(slices,i,min(i+maxslice-1,size))
                        if kids then
                            n = n + 1
                            temp[n] = {
                                reference = pdfreference(pdfflushobject(kids)),
                                limits    = kids.Limits,
                            }
                        else
                            -- error
                        end
                    end
                    slices = temp
                else
                    local kids = collectkids(slices,1,size)
                    if kids then
                        return pdfreference(pdfflushobject(kids))
                    else
                        -- error
                        return
                    end
                end
            end
        end
    end

    lpdf.makenametree = pdfmakenametree

end

-- Bah, I hate this kind of features .. anyway, as we have delayed resolving we
-- only support a document-wide setup and it has to be set before the first one
-- is used. Also, we default to a non-intrusive gray and the outline is kept
-- thin without dashing lines. This is as far as I'm prepared to go. This way
-- it can also be used as a debug feature.
--
-- A bit of granularity is possible by specifying colors per kind (+special) but
-- it assumes that one knows what categories there are (we report unknown ones
-- once). This is an undocumented feature (at least in manuals). Keep in mind that
-- after decades of pdf being around borders are not supported widely. They are
-- ugly anyway and they basically contradict quality typesetting. Directives are:
--
-- references.border=color
--
-- references.border=inner:red
-- references.border=special operation:blue
-- references.border=special operation+url:green

-- This will become a plugin once I have time for it: the directive can load the
-- code then: lpdf-ano-imp-border.lmt or so.

local pdf_border_style = pdfarray { 0, 0, 0 } -- radius radius linewidth
local pdf_border_color = nil
local set_border       = false
local use_borders      = false

local function pdfborder(actions)
    set_border = true
    if use_borders and actions then
        local action  = actions[1]
        if action then
            local kind    = action.kind
            local special = action.special or ""
            local border  = use_borders[kind]
            if not border then
                report_references("no border color defined for %a",kind)
                use_borders[kind] = { [""] = pdf_border_color }
            else
                border = border[special]
                if border then
                    return pdf_border_style, border
                else
                    report_references("no border color defined for %a : %a",kind,special)
                    use_borders[kind] = { [special] = pdf_border_color }
                end
            end
        end
    end
    return pdf_border_style, pdf_border_color
end

lpdf.border = pdfborder

-- This can be a more generic lpdf helper:

local pdfcolorarray do

    local colorlist  = attributes.list[attributes.private('color')]
    local colorvalue = attributes.colors.value

    pdfcolorarray = function(v)
        if v then
            v = colorlist and colorlist[v]
            if v then
                v = colorvalue(v)
                if v then
                    v = pdfarray { v[3], v[4], v[5] } -- always rgb
                end
            end
        end
        if not v then
            v = pdfarray { .8, .8, .8 }
        end
        return v
    end

end

directives.register("references.border",function(v)
    if v and not set_border then
        if type(v) == "string" then
            if find(v,":") then
                if not use_borders then
                    use_borders = { }
                end
                local kind, special, color = match(v,"^([a-z%s]+)+*([a-z%s]*):(.+)$")
                if kind then
                    local c = pdfcolorarray(color)
                    if c then
                        local k = use_borders[kind]
                        if k then
                            k[special] = c
                        else
                            use_borders[kind] = { [special] = c }
                        end
                    end
                end
            else
                pdf_border_color = pdfcolorarray(v)
            end
        end
        if not pdf_border_color then
            pdf_border_color = pdfarray { .6, .6, .6 } -- no reduce to { 0.6 } as there are buggy viewers out there
        end
        pdf_border_style = pdfarray { 0, 0, .5 } -- < 0.5 is not show by acrobat (at least not in my version)
    end
end)

-- the used and flag code here is somewhat messy in the sense
-- that it belongs in strc-ref but at the same time depends on
-- the backend so we keep it here

-- the caching is somewhat memory intense on the one hand but
-- it saves many small temporary tables so it might pay off

local pagedestinations = setmetatableindex(function(t,k)
    k = tonumber(k)
    if not k or k <= 0 then
        return pdfnull()
    end
    local v = rawget(t,k)
    if not v then
        v = k > 0 and pdfarray {
            pdfreference(pdfpagereference(k)),
            pdf_fit,
        } or pdfnull()
        t[k] = v
    end
    return v
end)

local crappypagedestinations = setmetatableindex(function(t,k)
    k = tonumber(k)
    if not k or k <= 0 then
        return pdfnull()
    end
    local v = rawget(t,k)
    if not v then
        local o = codeinjections.getreferencestructureobject(false,k) or 0
        v = o > 0 and pdfarray {
            pdfreference(o),
            pdf_fit,
        } or pdfnull()
        t[k] = v
    end
    return v
end)

local pagereferences = setmetatableindex(function(t,k)
    k = tonumber(k)
    if not k or k <= 0 then
        return nil
    end
    local v = rawget(t,k)
    if v then
        return v
    end
    local v = pdfdictionary { -- can be cached
        S  = pdf_goto,
        D  = pagedestinations[k],
        SD = crappytaggingmode > 1 and crappypagedestinations[k] or nil,
    }
    t[k] = v
    return v
end)

local defaultdestination = pdfarray { 0, pdf_fit }

-- fit is default (see lpdf-nod)

local destinations = { }
local reported     = setmetatableindex("table")

local function pdfregisterdestination(name,reference)
    local d = destinations[name]
    if d then
        if not reported[name][reference] then
            report_destinations("ignoring duplicate destination %a with reference %a",name,reference)
            reported[name][reference] = true
        end
    else
        destinations[name] = reference
    end
end

lpdf.registerdestination = pdfregisterdestination

logs.registerfinalactions(function()
    if log_destinations and next(destinations) then
        local report = logs.startfilelogging("references","used destinations")
        local n = 0
        for destination, pagenumber in table.sortedhash(destinations) do
            report("% 4i : %-5s : %s",pagenumber,usedviews[destination] or defaultview,destination)
            n = n + 1
        end
        logs.stopfilelogging()
        report_destinations("%s destinations saved in log file",n)
    end
end)

local function pdfdestinationspecification()
    if next(destinations) then -- safeguard
        checkautoprefixes(destinations)
        local r = pdfmakenametree(destinations,pdfreference)
        if r then
            pdfaddtonames("Dests",r)
        end
        if not log_destinations then
            destinations = nil
        end
    end
end

lpdf.destinationspecification = pdfdestinationspecification

lpdf.registerdocumentfinalizer(pdfdestinationspecification,"collect destinations")

-- todo

local destinations = { }

local v_standard  <const> = variables.standard
local v_frame     <const> = variables.frame
local v_width     <const> = variables.width
local v_minwidth  <const> = variables.minwidth
local v_height    <const> = variables.height
local v_minheight <const> = variables.minheight
local v_fit       <const> = variables.fit
local v_tight     <const> = variables.tight

local mapping = {
    [v_standard]  = v_standard,  xyz   = v_standard,
    [v_frame]     = v_frame,     fitr  = v_frame,
    [v_width]     = v_width,     fith  = v_width,
    [v_minwidth]  = v_minwidth,  fitbh = v_minwidth,
    [v_height]    = v_height,    fitv  = v_height,
    [v_minheight] = v_minheight, fitbv = v_minheight,
    [v_fit]       = v_fit,       fit   = v_fit,
    [v_tight]     = v_tight,     fitb  = v_tight,
}

local defaultview   = v_fit
local offset        = 0 -- 65536*5

local c_realpageno  <const> = tex.iscount("realpageno")

-- nicer is to create dictionaries and set properties but it's a bit overkill

-- The problem with the following settings is that they are guesses: we never know
-- if a box is part of something larger that needs to be in view, or that we are
-- dealing with a vbox or vtop so the used h/d values cannot be trusted in a tight
-- view. Of course some decent additional offset would be nice so maybe i'll add
-- that some day. I never use anything else than 'fit' anyway as I think that the
-- document should fit the device (and vice versa). In fact, with todays swipe
-- and finger zooming this whole view is rather useless and as with any zooming
-- one looses the overview and keeps zooming.

local destinationactions, defaultaction  do

    local f_xyz   = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] >>"]
    local f_fit   = formatters["<< /D [ %i 0 R /Fit ] >>"]
    local f_fitb  = formatters["<< /D [ %i 0 R /FitB ] >>"]
    local f_fith  = formatters["<< /D [ %i 0 R /FitH %.6N ] >>"]
    local f_fitv  = formatters["<< /D [ %i 0 R /FitV %.6N ] >>"]
    local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] >>"]
    local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] >>"]
    local f_fitr  = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"]

    destinationactions = {
        -- local left,top with no zoom
        [v_standard] = function(r,w,h,d,o)
            local tx, ty = getpos()
            return f_xyz(r,tx*factor,(ty+h+2*o)*factor)
        end,
        -- fit rectangle in window
        [v_frame] = function(r,w,h,d,o)
            return f_fitr(r,pdfrectangle(w,h,d,o))
        end,
        -- top coordinate, fit width of page in window
        [v_width] = function(r,w,h,d,o)
            return f_fith(r,(getvpos()+h+o)*factor)
        end,
        -- top coordinate, fit width of content in window
        [v_minwidth] = function(r,w,h,d,o)
            return f_fitbh(r,(getvpos()+h+o)*factor)
        end,
        -- left coordinate, fit height of page in window
        [v_height] = function(r,w,h,d,o)
            return f_fitv(r,(gethpos())*factor)
        end,
        -- left coordinate, fit height of content in window
        [v_minheight] = function(r,w,h,d,o)
            return f_fitbv(r,(gethpos())*factor)
        end,
        -- fit content in window
        [v_tight] = f_fitb,
        -- fit content in window
        [v_fit] = f_fit,
    }

    defaultaction = destinationactions[defaultview]

end

local xdestinationactions, xdefaultaction  do

    local f_xyz   = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] /SD [ %i 0 R /XYZ %.6N %.6N null ] >>"]
    local f_fit   = formatters["<< /D [ %i 0 R /Fit ] /SD [ %i 0 R /Fit ] >>"]
    local f_fitb  = formatters["<< /D [ %i 0 R /FitB ] /SD [ %i 0 R /FitB ] >>"]
    local f_fith  = formatters["<< /D [ %i 0 R /FitH %.6N ] /SD [ %i 0 R /FitH %.6N ] >>"]
    local f_fitv  = formatters["<< /D [ %i 0 R /FitV %.6N ] /SD[ %i 0 R /FitV %.6N ] >>"]
    local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] /SD [ %i 0 R /FitBH %.6N ] >>"]
    local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] /SD [ %i 0 R /FitBV %.6N ] >>"]
    local f_fitr  = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] /SD [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"]

    xdestinationactions = {
        [v_standard] = function(r,w,h,d,o,s)
            local tx, ty = getpos()
            local x = tx*factor
            local y = (ty+h+2*o)*factor
            return f_xyz(r,x,y,s,x,y)
        end,
        [v_frame] = function(r,w,h,d,o,s)
            return f_fitr(r,pdfrectangle(w,h,d,o),s,pdfrectangle(w,h,d,o))
        end,
        [v_width] = function(r,w,h,d,o,s)
            local v = (getvpos()+h+o)*factor
            return f_fith(r,v,s,v)
        end,
        [v_minwidth] = function(r,w,h,d,o,s)
            local h = (getvpos()+h+o)*factor
            return f_fitbh(r,h,s,h)
        end,
        [v_height] = function(r,w,h,d,o,s)
            local v = gethpos()*factor
            return f_fitv(r,v,s,v)
        end,
        [v_minheight] = function(r,w,h,d,o,s)
            local h = gethpos()*factor
            return f_fitbv(r,h,s,h)
        end,
        [v_tight] = function(r,w,h,d,o,s)
            return f_fitb(r,s)
        end,
        [v_fit] = function(r,w,h,d,o,s)
            return f_fit(r,s)
        end,
    }

    xdefaultaction = xdestinationactions[defaultview]

end

directives.register("destinations.offset", function(v)
    offset = string.todimen(v) or 0
end)

-- A complication is that we need to use named destinations when we have views so we
-- end up with a mix. A previous versions just output multiple destinations but now
-- that we moved all to here we can be more sparse.

local f_fit = formatters["<< /D [ %i 0 R /Fit ] >>"]

local pagedestinations = setmetatableindex(function(t,k) -- not the same as the one above!
    local v = pdfdelayedobject(f_fit(k))
    t[k] = v
    return v
end)

local function flushdestination(specification)
    local names = specification.names
    local view  = specification.view
    local page  = texgetcount(c_realpageno)
    local r     = pdfpagereference(page)
    if (crappytaggingmode < 2) and (references.innermethod ~= v_name) and (view == defaultview or not view or view == "") then
        r = pagedestinations[r]
    else
        local action, o
        if crappytaggingmode > 1 then
            local internal = specification.internal -- actually attribute
            if internal or page then
                o = codeinjections.getreferencestructureobject(internal,page) -- actually attribute
                if o and o > 0 then
                    action = view and xdestinationactions[view] or xdefaultaction
local d = autoprefix .. internal
local f = false
for i=1,#names do
    if names[i] == d then
        f = true
        break
    end
end
if not f then
    names[#names+1] = d
end
                else
                    o = nil
                end
            end
        end
        if not action then
            action = view and destinationactions[view] or defaultaction
        end
-- if o then
--     local objref = pdfreserveobject()
--     local specifier = action(r,specification.width,specification.height,specification.depth,offset,o)
--     pdfdelayedobject(specifier(),objref)
--     r = pdfregisterannotation(objref)
-- else
        r = pdfdelayedobject(action(r,specification.width,specification.height,specification.depth,offset,o))
-- end
    end
    for n=1,#names do
        local name = names[n]
        if name then
            pdfregisterdestination(name,r)
        end
    end
end

function nodeinjections.destination(width,height,depth,names,view,internal)
    -- todo check if begin end node / was comment
    view = view and mapping[view] or defaultview
    if trace_destinations then
        report_destinations("width %p, height %p, depth %p, names %|t, view %a",width,height,depth,names,view)
    end
    local method = references.innermethod
    local noview = view == defaultview
    local doview = false
    -- we could save some aut's by using a name when given but it doesn't pay off apart
    -- from making the code messy and tracing hard .. we only save some destinations
    -- which we already share anyway
    if method == v_page then
        for n=1,#names do
            local name = names[n]
            if name then
                local used = usedviews[name]
                if used and used ~= true then
                    -- already done, maybe a warning
                elseif type(name) == "number" then
                 -- if noview then
                 --     usedviews[name] = view
                 --     names[n] = false
                 -- else
                        usedviews[name] = view
                        names[n] = false
                 -- end
                else
                    usedviews[name] = view
                end
            end
        end
    elseif method == v_name then
        for n=1,#names do
            local name = names[n]
            if name then
                local used = usedviews[name]
                if used and used ~= true then
                    -- already done, maybe a warning
                elseif type(name) == "number" then
                    local used = usedinternals[name]
                    usedviews[name] = view
                    names[n] = registerautoprefix(name)
                    doview = true
                else
                    usedviews[name] = view
                    doview = true
                end
            end
        end
    else
        for n=1,#names do
            local name = names[n]
            if name then
                if usedviews[name] then
                    -- already done, maybe a warning
                elseif type(name) == "number" then
                    if noview then
                        usedviews[name] = view
                        names[n] = false
                    else
                        local used = usedinternals[name]
                        if used and used ~= defaultview then
                            usedviews[name] = view
                            names[n] = registerautoprefix(name)
                            doview = true
                        else
                            names[n] = false
                        end
                    end
                else
                    usedviews[name] = view
                    doview = true
                end
            end
        end
    end
    if doview or crappytaggingmode > 0 then
        return new_latelua {
--             kind   = "destination",
            action   = flushdestination,
            width    = width,
            height   = height,
            depth    = depth,
            names    = names,
            view     = view,
            internal = internal,
        }
    end
end

-- we could share dictionaries ... todo

local function pdflinkpage(page)
    return pagereferences[page]
end

local function pdflinkinternal(internal,page)
 -- local method = references.innermethod
    if internal then
        flaginternals[internal] = true -- for bookmarks and so
        if crappytaggingmode > 1 then
            local o = codeinjections.getreferencestructureobject(internal,page)
            if o and o > 0 then
                if type(internal) ~= "string" then
                    internal = useautoprefix(internal)
                end
                return pdfdictionary {
                    S  = pdf_goto,
                    D  = internal, -- redundant here (needs checking)
                    SD = pdfarray { pdfreference(o), pdf_fit } -- link or nested
                }
            end
        end
        local used = usedinternals[internal]
        if type(internal) ~= "string" then
            internal = useautoprefix(internal)
        end
        if used == defaultview or used == true then
            return pagereferences[page]
        else
            if type(internal) ~= "string" then
                internal = useautoprefix(internal)
            end
            return pdfdictionary {
                S  = pdf_goto,
                D  = internal,
                SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
            }
        end
    else
        return pagereferences[page]
    end
end

local function pdflinkname(destination,internal,page)
    local method = references.innermethod
    if method == v_auto then
        local used = defaultview
        if internal then
            flaginternals[internal] = true -- for bookmarks and so
            used = usedinternals[internal] or defaultview
        end
        if used == defaultview then -- or used == true then
            return pagereferences[page]
        else
            return pdfdictionary {
                S  = pdf_goto,
                D  = destination,
                SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
            }
        end
    elseif method == v_name then
     -- flaginternals[internal] = true -- for bookmarks and so
        return pdfdictionary {
            S  = pdf_goto,
            D  = destination,
            SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
        }
    else
        return pagereferences[page]
    end
end

-- annotations

local pdffilelink  do

    local valid = setmetatableindex(function(t,filename)
        local found = false
        if lfs.isfile(filename) then
            report_destinations("loading destinations from file %a",filename)
            local pdffile = lpdf.epdf.load(filename)
            if pdffile then
                local pages        = pdffile.pages
                local nofpages     = pdffile.nofpages
                local destinations = pdffile.destinations
                if pages and nofpages > 0 and destinations then
                    local reverse = swapped(pages)
                    local total   = 0
                          found   = { }
                    for k, v in next, destinations do
                        local D = v.D
                        if D then
                            found[k] = reverse[D[1]]
                            total    = total + 1
                        end
                    end
                    t[filename] = found
                    report_destinations("%i destinations on %i pages found",total,nofpages)
                end
            end
        end
        return found
    end)

    local pagefromhash = structures.references.pagefromhash

    pdffilelink = function(filename,destination,page,actions)
        if not filename or filename == "" or file.basename(filename) == tex.jobname then
            return false
        end
        filename = file.addsuffix(filename,"pdf")
        -- page auto name
        local forcepage = false
        if not destination or destination == "" then
            forcepage = true
        elseif references.outermethod == v_page then
            if not page then
                local hash = valid[filename]
                page = hash and hash[destination]
                if not page or trace_externals then
                    report_destinations("no %s destination %a in file %a","page",destination,filename)
                end
            end
            forcepage = true
        else -- name or auto, maybe only check with auto
            local hash = valid[filename]
            if hash then
                local p = nil
                p, destination = pagefromhash(hash,destination,page,actions)
                if p then
                    if references.outermethod == v_name then
                        -- keep destination string
                    elseif page then
                        if p ~= page then
                            report_destinations("page %i for destination %a in %a conflicts, %i expected",page,destination,filename,p)
                            page = p
                        end
                        forcepage = true
                    elseif p then
                        page = p
                        forcepage = true
                    end
                else
                    if not page or trace_externals then
                        report_destinations("no %s destination %a in file %a","name",destination,filename)
                    end
                    forcepage = true
                end
         -- else
                -- keep destination string
            end
        end
        if forcepage then
            destination = pdfarray { (page or 1) - 1, pdf_fit }
        end
        return pdfdictionary {
            S         = pdf_gotor, -- can also be pdf_launch
            F         = filename,
            D         = destination or defaultdestination,
            NewWindow = actions.newwindow and true or nil,
        }
    end

end

local untex = references.urls.untex

local function pdfurllink(url,destination,page)
    if not url or url == "" then
        return false
    end
    if untex_urls then
        url = untex(url) -- last minute cleanup of \* and spaces
    end
    if destination and destination ~= "" then
        url = url .. "#" .. destination
    end
    return pdfdictionary {
        S   = pdf_uri,
        URI = url,
    }
end

local function pdflaunch(program,parameters)
    if not program or program == "" then
        return false
    end
    return pdfdictionary {
        S = pdf_launch,
        F = program,
        D = ".",
        P = parameters ~= "" and parameters or nil
    }
end

local function pdfjavascript(name,arguments)
    local script = javascriptcode(name,arguments) -- make into object (hash)
    if script then
        return pdfdictionary {
            S  = pdf_javascript,
            JS = script,
        }
    end
end

-- local function pdfhighlight(name,arguments)
--     return pdfdictionary {
--         S       = pdf_highlight,
--         C       = pdfarray { 1, 1, 0 },
--         Title   = name and pdfunicode(name) or nil,
--         Content = arguments and pdfunicode(arguments) or nil
--     }
-- end

local function pdfaction(actions)
    local nofactions = #actions
    if nofactions > 0 then
        local a = actions[1]
        local action = runners[a.kind]
        if action then
            action = action(a,actions)
        end
        if action then
            local first = action
            for i=2,nofactions do
                local a = actions[i]
                local what = runners[a.kind]
                if what then
                    what = what(a,actions)
                end
                if action == what then
                    -- ignore this one, else we get a loop
                elseif what then
                    action.Next = what
                    action = what
                else
                    -- error
                    return nil
                end
            end
            return first, actions.n or #actions
        end
    end
end

lpdf.action = pdfaction

-- Because a highlight annotation is actually an area we cheat a bit and implement it here instead
-- of in the widget module (like text and attachments).

local nofhighlights = 0
local fixhighlight  = false

directives.register("backend.pdf.fixhighlight", function(v)
    fixhighlight = v
end)

function codeinjections.prerollreference(actions,index) -- share can become option
    if actions then
        local action = actions[1]
        if action and action.special == "highlight" then
            nofhighlights = nofhighlights + 1
         -- local creation = lpdf.pdftimestamp(os.date("%Y-%m-%dT%H:%M:%S") .. os.timezone())
            local author   = action.operation
            local contents = action.arguments
            local color    = pdfcolorarray(author and "pdfhighlight:" .. author or nil)
            local bs, bc   = pdfborder(actions)
            local main = pdfdictionary {
                Subtype     = pdf_highlight,
                Border      = pdfshareobjectreference(bs),
                C           = color or pdfarray { .8, .8, .8 },
                T           = author and pdfunicode(author) or nil,
                Contents    = contents and contents ~= "" and pdfunicode(contents) or nil,
                F           = 4, -- print (mandate in pdf/a)
             -- M           = pdfstring(creation),
                NM          = pdfstring("LMTX:" .. nofhighlights), -- also makes for a unique mesh reference
                LMTX_QP1243 = fixhighlight and true or nil,
            }
            actions.forcemesh = true
            return main, 1
        else
            local main, n = pdfaction(actions)
            if main then
                local bs, bc = pdfborder(actions)
                main = pdfdictionary {
                    Subtype = pdf_link,
                    Border  = pdfshareobjectreference(bs),
                    C       = bc,
                    H       = (not actions.highlight and pdf_n) or nil,
                    A       = pdfshareobjectreference(main),
                    F       = 4, -- print (mandate in pdf/a)
                }
                return main, n
            end
        end
    end
end

-- local function use_normal_annotations()
--
--     local function reference(width,height,depth,prerolled) -- keep this one
--         if prerolled then
--             if trace_references then
--                 report_references("width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
--             end
--             return pdfannotation_node(width,height,depth,prerolled)
--         end
--     end
--
--     local function finishreference()
--     end
--
--     return reference, finishreference
--
-- end

-- eventually we can do this for special refs only

local hashed     = { }
local nofunique  = 0
local nofused    = 0
local nofspecial = 0
local share      = true

local refobjects = { }

local f_annot    = formatters["<< /Type /Annot %s /Rect [ %.6N %.6N %.6N %.6N ] >>"]
local f_quadp    = formatters["<< /Type /Annot %s /QuadPoints [ %s ] /Rect [ %.6N %.6N %.6N %.6N ] >>"]

directives.register("references.sharelinks", function(v)
    share = v
end)

setmetatableindex(hashed,function(t,k)
    local v = pdfdelayedobject(k)
    if share then
        t[k] = v
    end
    nofunique = nofunique + 1
    return v
end)

local function toquadpoints(paths,bugged)
    local t, n = { }, 0
    if bugged then
        -- According to the specification we go ll lr ur ul. This seems to be okay for links but not for e.g.
        -- highlights. In links the viewer looks at the areas as links and that's a different code path than
        -- rendering the highlight and obviously that code expects a different order: tl tr bl br otherwise
        -- (2024) Acrobat, Sumatra, Okular etc. give weird results (also depending on them drawing straiht
        -- lines or curves. It looks like an acrobat bug became a viewer specification but the specification
        -- doesn't mention it.
        for i=1,#paths do
            local p = paths[i]
            p[3], p[4] = p[4], p[3]
        end
    end
    --
    for i=1,#paths do
        local path = paths[i]
        local size = #path
        for j=1,size do
            local p = path[j]
            n = n + 1 ; t[n] = p[1]
            n = n + 1 ; t[n] = p[2]
        end
        local m = size % 4
        if m > 0 then
            local p = path[size]
            for j=size+1,m do
                n = n + 1 ; t[n] = p[1]
                n = n + 1 ; t[n] = p[2]
            end
        end
    end
    return concat(t," ")
end

local finishreference  do

    -- Just a gimmick but one that adds runtime and memory usage. it is only needed
    -- when we have many long references spanning lines.

    local collected = allocate()
    local tobesaved = allocate()

    local jobmeshedup = {
        collected = collected,
        tobesaved = tobesaved,
    }

    job.meshedup = jobmeshedup

    local function initializer()
        tobesaved = jobmeshedup.tobesaved
        collected = jobmeshedup.collected
    end

    job.register('job.meshedup.collected',tobesaved,initializer)

    local checkmesh     = false
    local lastprerolled = false
    local lastmeshedup  = false
    local lastmeshed    = false
    local lastrealpage  = 0

    local count = 0
    local total = 0
    local valid = 0
    local more  = 0

    local function meshup(prerolled,llx,lly,urx,ury)
        local q = nil
        local r = texgetcount(c_realpageno)
        local p = collected[r] and collected[r][prerolled]
        if r ~= lastrealpage then
            lastrealpage  = r
            lastprerolled = false
            lastmeshedup  = false
            lastmeshed    = false
        end
        if prerolled == lastprerolled then
            if not more then
                valid = valid + 1
                more  = true
            end
            total = total + 1
            if #lastmeshed == 4 then
                local t = tobesaved[r]
                if not t then
                    t = { }
                    tobesaved[r] = t
                end
                t[prerolled] = lastmeshed
            end
            lastmeshed[#lastmeshed+1] = llx
            lastmeshed[#lastmeshed+1] = lly
            lastmeshed[#lastmeshed+1] = urx
            lastmeshed[#lastmeshed+1] = ury
        else
            lastprerolled = prerolled
            lastmeshed    = { llx, lly, urx, ury }
            count         = count + 1
            more          = false
        end
        if p then
            if p.done then
                q = true
            else
                q = { }
                if #p > 0 then
                    for i=1,#p,4 do
                        local lx = p[i]
                        local ly = p[i+1]
                        local ux = p[i+2]
                        local uy = p[i+3]
                        if i == 1 then
                            llx = min(lx,ux)
                            lly = min(ly,uy)
                            urx = max(lx,ux)
                            ury = max(ly,uy)
                        else
                            llx = min(llx,lx,ux)
                            lly = min(lly,ly,uy)
                            urx = max(urx,lx,ux)
                            ury = max(ury,ly,uy)
                        end
                        q[#q+1] = { { lx, ly }, { ux, ly }, { ux, uy }, { lx, uy } }
                    end
                end
                p.done = true
            end
        end
        return llx, lly, urx, ury, q
    end

    -- Maybe some day: when we want a mesh then we need to move the lines according to
    -- the transformation which means that we need to keep track of the vertical
    -- progression in a paragraph and that is something I don't want to do right now.
    -- After all, these features are often somwehat fragile in viewers anyway.

 -- local function qmeshup(prerolled,x1,y1,x2,y2,x3,y3,x4,y4)
 --     local q = nil
 --     local r = texgetcount(c_realpageno)
 --     local p = collected[r] and collected[r][prerolled]
 --     if r ~= lastrealpage then
 --         lastrealpage  = r
 --         lastprerolled = false
 --         lastmeshedup  = false
 --         lastmeshed    = false
 --     end
 --     if prerolled == lastprerolled then
 --         if not more then
 --             valid = valid + 1
 --             more  = true
 --         end
 --         total = total + 1
 --         if #lastmeshed == 8 then
 --             local t = tobesaved[r]
 --             if not t then
 --                 t = { }
 --                 tobesaved[r] = t
 --             end
 --             t[prerolled] = lastmeshed
 --         end
 --         lastmeshed[#lastmeshed+1] = x1
 --         lastmeshed[#lastmeshed+1] = y1
 --         lastmeshed[#lastmeshed+1] = x2
 --         lastmeshed[#lastmeshed+1] = y2
 --         lastmeshed[#lastmeshed+1] = x3
 --         lastmeshed[#lastmeshed+1] = y3
 --         lastmeshed[#lastmeshed+1] = x4
 --         lastmeshed[#lastmeshed+1] = y4
 --     else
 --         lastprerolled = prerolled
 --         lastmeshed    = { x1, y1, x2, y2, x3, y3, x4, y4 }
 --         count         = count + 1
 --         more          = false
 --     end
 --     if p then
 --         if p.done then
 --             q = true
 --         else
 --             q = { }
 --             if #p > 0 then
 --                 for i=1,#p,8 do
 --                     local x1 = p[i]
 --                     local y1 = p[i+1]
 --                     local x2 = p[i+2]
 --                     local y2 = p[i+3]
 --                     local x3 = p[i+4]
 --                     local y3 = p[i+5]
 --                     local x4 = p[i+6]
 --                     local y4 = p[i+7]
 --                     if i == 1 then
 --                         llx = min(x1,x2,x3,x4)
 --                         lly = min(y1,y2,y3,y4)
 --                         urx = max(x1,x2,x3,x4)
 --                         ury = max(y1,y2,y3,y4)
 --                     else
 --                         llx = min(llx,x1,x2,x3,x4)
 --                         lly = min(lly,y1,y2,y3,y4)
 --                         urx = max(urx,x1,x2,x3,x4)
 --                         ury = max(ury,y1,y2,y3,y4)
 --                     end
 --                     q[#q+1] = { { x1, y1 }, { x2, y2 }, { x3, y3 }, { x4, y4 } }
 --                 end
 --             end
 --             p.done = true
 --         end
 --     end
 --     return llx, lly, urx, ury, q
 -- end

    local function identify(prerolled,llx,lly,urx,ury)
        local r = texgetcount(c_realpageno)
        if r ~= lastrealpage then
            lastrealpage  = r
            lastprerolled = false
            lastmeshedup  = false
            lastmeshed    = false
        end
        if prerolled == lastprerolled then
            if not more then
                valid = valid + 1
                more  = true
            end
            total = total + 1
        else
            lastprerolled = prerolled
            count         = count + 1
            more          = false
        end
        return llx, lly, urx, ury, nil
    end

    statistics.register("meshed up references", function()
        if count > 0 then
            return format("%i seen, %i valid, %i total",count,valid,total)
        else
            return nil
        end
    end)

    directives.register("references.meshup", function(v)
        checkmesh = (v == "identify" and identify) or (v and meshup) or nil
    end)

    -- End of gimmick.

    local function checked(prerolled,specification)
        local llx, lly, urx, ury = pdfrectangle(specification.width,specification.height,specification.depth)
        local quadpoints = specification.mesh
        local forcemesh = specification.forcemesh
        local rotated = specification.rotated
        local bugged = false
        if quadpoints then
            -- this always wins when set (normally only happens with mp graphics)
        elseif checkmesh then
            llx, lly, urx, ury, quadpoints = checkmesh(prerolled,llx,lly,urx,ury)
            if quadpoints == true then
                return
            end
        elseif forcemesh then
            llx, lly, urx, ury, quadpoints = meshup(prerolled,llx,lly,urx,ury)
            if quadpoints == true then
                return
            end
        end
        if forcemesh then
            if fixhighlight and specification.prerolled and tostring(specification.prerolled.Subtype) == "/Highlight" then
                bugged = true
            end
            if rotated or not quadpoints then
                quadpoints = { { { llx, lly }, { urx, lly }, { urx, ury }, { llx, ury } } }
            end
        elseif rotated then
            local x1, y1, x2, y2, x3, y3, x4, y4, llx, lly, urx, ury = pdfquads(0,llx/factor,urx/factor,ury/factor)
            quadpoints = { { { x1, y1 }, { x2, y2 }, { x3, y3 }, { x4, y4 } } }
        end
        if quadpoints and #quadpoints > 0 then
            prerolled = f_quadp(prerolled,toquadpoints(quadpoints,bugged),llx,lly,urx,ury)
        else
            prerolled = f_annot(prerolled,llx,lly,urx,ury)
        end
        return prerolled
    end

    finishreference = function(specification)
        local prerolled = specification.prerolled
        local refatt    = specification.reference
        if crappytaggingmode > 0 then
            if type(prerolled) ~= "string" then
                -- validators want this but what to put in it that is not redundant
                prerolled.Contents     = pdfunicode(specification.description or "link")
                prerolled.StructParent = codeinjections.getlinkstructureparent(refatt)
                prerolled = prerolled()
            end
            local specifier = checked(prerolled,specification)
            if not specifier then
                return
            end
            local objref = pdfreserveobject()
            specification.objref = objref
         -- nofunique = nofunique + 1
            nofused = nofused + 1
            if refatt then
                refobjects[refatt] = objref
            end
            pdfdelayedobject(specifier,objref)
            return pdfregisterannotation(objref)
        else
            if type(prerolled) ~= "string" then
                prerolled = prerolled()
            end
            local specifier = checked(prerolled,specification)
            if not specifier then
                return
            end
            local objref = hashed[specifier]
            specification.objref = objref
            nofused = nofused + 1
            if refatt then
                refobjects[refatt] = objref
            end
            return pdfregisterannotation(objref)
        end
    end

end

function codeinjections.getrefobj(refatt) -- bad name but experiment anyway
    return refobjects[refatt]
end

local function finishannotation(specification)
    local prerolled = specification.prerolled
    local objref    = specification.objref
    if type(prerolled) == "function" then
        prerolled = prerolled()
    end
    if type(prerolled) ~= "string" then
        prerolled = prerolled()
    end
    local annot = f_annot(prerolled,pdfrectangle(specification.width,specification.height,specification.depth))
    if objref then
        pdfdelayedobject(annot,objref)
    else
        objref = pdfdelayedobject(annot)
        specification.objref = objref
    end
    nofspecial = nofspecial + 1
    return pdfregisterannotation(objref)
end

function nodeinjections.reference(reference,width,height,depth,prerolled,mesh,description,forcemesh,rotated)
    if prerolled then
        if trace_references then
            report_references("link: width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
        end
        return new_latelua {
         -- kind      = "reference",
            action      = finishreference,
            reference   = reference,
            width       = width,
            height      = height,
            depth       = depth,
            prerolled   = prerolled,
            mesh        = mesh,
            description = description,
            forcemesh   = forcemesh,
            rotated     = rotated,
        }
    end
end

function nodeinjections.annotation(width,height,depth,prerolled,objref)
    if prerolled then
        if trace_references then
            report_references("special: width %p, height %p, depth %p, prerolled %a",width,height,depth,
                type(prerolled) == "string" and prerolled or "-")
        end
        return new_latelua {
         -- kind      = "annotation",
            action    = finishannotation,
            width     = width,
            height    = height,
            depth     = depth,
            prerolled = prerolled,
            objref    = objref or false,
        }
    end
end

-- beware, we register during a latelua sweep so we have to make sure that
-- we finalize after that (also in a latelua for the moment as we have no
-- callback yet)

local annotations = nil

pdfregisterannotation = function(n)
    if annotations then
        annotations[#annotations+1] = pdfreference(n)
    else
        annotations = pdfarray { pdfreference(n) } -- no need to use lpdf.array cum suis
    end
    return n
end

lpdf.registerannotation = pdfregisterannotation

-- Another weird version 2 demand: there should be a Tabs entry because otherwise a viewer
-- can choose its own order. So why not just have a recommended default? If a viewer wants
-- to be compliant it will obey that.

function lpdf.annotationspecification()
    if annotations then
        local r = pdfdelayedobject(tostring(annotations)) -- delayed so okay in latelua
        if r then
            pdfaddtopageattributes("Annots",pdfreference(r))
         -- if pdfmajorversion() > 1 then
                pdfaddtopageattributes("Tabs",pdfconstant("S")) -- structure
         -- end
        end
        annotations = nil
    end
end

lpdf.registerpagefinalizer(lpdf.annotationspecification,"finalize annotations")

statistics.register("pdf annotations", function()
    if nofused > 0 or nofspecial > 0 then
        return format("%s links (%s unique), %s special",nofused,nofunique,nofspecial)
    else
        return nil
    end
end)

-- runners and specials

local splitter = lpeg.splitat(",",true)

runners["inner"] = function(var,actions)
    local internal = false
    local name     = nil
    local method   = references.innermethod
    local vi       = var.i
    local page     = var.r
    if vi then
        local vir = vi.references
        if vir then
            -- todo: no need for it when we have a real reference ... although we need
            -- this mess for prefixes anyway
            local reference = vir.reference
            if reference and reference ~= "" then
                reference = lpegmatch(splitter,reference) or reference
                var.inner = reference
                local prefix = var.p
                if prefix and prefix ~= "" then
                    var.prefix = prefix
                    name = prefix .. ":" .. reference
                else
                    name = reference
                end
            end
            internal = vir.internal
            if internal then
                flaginternals[internal] = true
            end
        end
    end
    if name then
        return pdflinkname(name,internal,page)
--     elseif internal or crappytaggingmode > 1 then
    elseif internal then
        return pdflinkinternal(internal,page,true)
    elseif page then
        return pdflinkpage(page)
    else
        -- real bad
    end
end

runners["inner with arguments"] = function(var,actions)
    report_references("todo: inner with arguments")
    return false
end

runners["outer"] = function(var,actions)
    local file, url = references.checkedfileorurl(var.outer,var.outer)
    if file  then
        return pdffilelink(file,var.arguments,nil,actions)
    elseif url then
        return pdfurllink(url,var.arguments,nil,actions)
    end
end

runners["outer with inner"] = function(var,actions)
    if var.r then
        actions.realpage = var.r
    end
    return pdffilelink(references.checkedfile(var.outer),var.inner,var.r,actions)
end

runners["special outer with operation"] = function(var,actions)
    local handler = specials[var.special]
    return handler and handler(var,actions)
end

runners["special outer"] = function(var,actions)
    report_references("todo: special outer")
    return false
end

runners["special"] = function(var,actions)
    local handler = specials[var.special]
    return handler and handler(var,actions)
end

runners["outer with inner with arguments"] = function(var,actions)
    report_references("todo: outer with inner with arguments")
    return false
end

runners["outer with special and operation and arguments"] = function(var,actions)
    report_references("todo: outer with special and operation and arguments")
    return false
end

runners["outer with special"] = function(var,actions)
    report_references("todo: outer with special")
    return false
end

runners["outer with special and operation"] = function(var,actions)
    report_references("todo: outer with special and operation")
    return false
end

runners["special operation"]                = runners["special"]
runners["special operation with arguments"] = runners["special"]

local reported = { }

function specials.internal(var,actions) -- better resolve in strc-ref
    local o = var.operation
    local i = o and tonumber(o)
    local v = i and references.internals[i]
    if v then
        flaginternals[i] = true -- also done in pdflinkinternal
        return pdflinkinternal(i,v.references.realpage)
    end
    local v = i or o or "<unset>"
    if not reported[v] then
        report_references("no internal reference %a",v)
        reported[v] = true
    end
end

-- realpage already resolved

specials.i = specials.internal

local pages = references.pages

function specials.page(var,actions)
    local file = var.f
    if file then
        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
    else
        local p = var.r
        if not p then -- todo: call special from reference code
            p = pages[var.operation]
            if type(p) == "function" then -- double
                p = p()
            else
                p = references.realpageofpage(tonumber(p))
            end
        end
        return pdflinkpage(p or var.operation)
    end
end

function specials.realpage(var,actions)
    local file = var.f
    if file then
        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
    else
        return pdflinkpage(var.operation)
    end
end

function specials.userpage(var,actions)
    local file = var.f
    if file then
        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
    else
        local p = var.r
        if not p then -- todo: call special from reference code
            p = var.operation
            if p then -- no function and special check here. only numbers
                p = references.realpageofpage(tonumber(p))
            end
         -- if p then
         --     var.r = p
         -- end
        end
        return pdflinkpage(p or var.operation)
    end
end

function specials.deltapage(var,actions)
    local p = tonumber(var.operation)
    if p then
        p = references.checkedrealpage(p + texgetcount(c_realpageno))
        return pdflinkpage(p)
    end
end

-- sections

function specials.section(var,actions)
    -- a bit duplicate
    local sectionname = var.arguments
    local destination = var.operation
    local internal    = structures.sections.internalreference(sectionname,destination)
    if internal then
        var.special   = "internal"
        var.operation = internal
        var.arguments = nil
        return specials.internal(var,actions)
    end
end

-- todo, do this in references namespace ordered instead (this is an experiment)

local splitter = lpeg.splitat(":")

function specials.order(var,actions) -- references.specials !
    local operation = var.operation
    if operation then
        local kind, name, n = lpegmatch(splitter,operation)
        local order = structures.lists.ordered[kind]
        order = order and order[name]
        local v = order[tonumber(n)]
        local r = v and v.references.realpage
        if r then
            var.operation = r -- brrr, but test anyway
            return specials.page(var,actions)
        end
    end
end

function specials.url(var,actions)
    return pdfurllink(references.checkedurl(var.operation),var.arguments,nil,actions)
end

function specials.file(var,actions)
    return pdffilelink(references.checkedfile(var.operation),var.arguments,nil,actions)
end

function specials.fileorurl(var,actions)
    local file, url = references.checkedfileorurl(var.operation,var.operation)
    if file then
        return pdffilelink(file,var.arguments,nil,actions)
    elseif url then
        return pdfurllink(url,var.arguments,nil,actions)
    end
end

function specials.program(var,content)
    local program = references.checkedprogram(var.operation)
    return pdflaunch(program,var.arguments)
end

function specials.javascript(var)
    return pdfjavascript(var.operation,var.arguments)
end

specials.JS = specials.javascript

function specials.highlight(var)
    -- this is just a signal (for now as we piggy back on normal references)
 -- return pdfhighlight(var.operation,var.arguments)
end

-- The 32000-2-2020 specification mentions (12.6.4.12) four actions (since 1.2) that
-- processors need (!) to support: NextPage, PrevPage, FirstPage and LastPage. To me
-- these four look easy to implement in viewers. It also tells us that that further
-- names may be added in the future. The note that non standard named actions can be
-- there refers to the fact that Acrobat supports actions like going backward,
-- forward close document etc. all very handy for presentations, and also very
-- efficient in terms of generating. because we can expect validators to cry about
-- unknown ones we need to disable them.

-- Still permitted, but who knows:

executers.first    = pdfdictionary { S = pdf_named, N = pdfconstant("FirstPage") }
executers.previous = pdfdictionary { S = pdf_named, N = pdfconstant("PrevPage") }
executers.next     = pdfdictionary { S = pdf_named, N = pdfconstant("NextPage") }
executers.last     = pdfdictionary { S = pdf_named, N = pdfconstant("LastPage") }

-- Only acrobat as no more additions, real bad:

-- executers.backward    = pdfdictionary { S = pdf_named, N = pdfconstant("GoBack") }
-- executers.forward     = pdfdictionary { S = pdf_named, N = pdfconstant("GoForward") }

-- Only acrobat, but not installed so no testing. We only keep a few for now, but we can
-- keep more via directives.

-- executers.importform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ImportFDF") }
-- executers.exportform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ExportFDF") }
-- executers.print       = pdfdictionary { S = pdf_named, N = pdfconstant("Print") }
-- executers.exit        = pdfdictionary { S = pdf_named, N = pdfconstant("Quit") }
-- executers.close       = pdfdictionary { S = pdf_named, N = pdfconstant("Close") }
-- executers.save        = pdfdictionary { S = pdf_named, N = pdfconstant("Save") }
-- executers.savenamed   = pdfdictionary { S = pdf_named, N = pdfconstant("SaveAs") }
-- executers.opennamed   = pdfdictionary { S = pdf_named, N = pdfconstant("Open") }
-- executers.help        = pdfdictionary { S = pdf_named, N = pdfconstant("HelpUserGuide") }
-- executers.toggle      = pdfdictionary { S = pdf_named, N = pdfconstant("FullScreen") }
-- executers.search      = pdfdictionary { S = pdf_named, N = pdfconstant("Find") }
-- executers.searchagain = pdfdictionary { S = pdf_named, N = pdfconstant("FindAgain") }
-- executers.gotopage    = pdfdictionary { S = pdf_named, N = pdfconstant("GoToPage") }
-- executers.query       = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:Query") }
-- executers.queryagain  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:NextHit") }
-- executers.fitwidth    = pdfdictionary { S = pdf_named, N = pdfconstant("FitWidth") }
-- executers.fitheight   = pdfdictionary { S = pdf_named, N = pdfconstant("FitHeight") }

setmetatableindex(executers, function(t,k)
    setmetatableindex(executers,nil)
    if codeinjections.getformatoption("acrobat") ~= false then
        report_references("loading (unofficial) acrobat actions")
        local fullname = resolvers.findfile("lpdf-ano-imp-acrobat.lmt") or ""
        if fullname ~= "" then
            dofile(fullname)
        end
    end
    local v = rawget(executers,k)
    if v == nil then
        report_references("(acrobat) action %a is not supported",k)
        v = false
        executers[k] = v
    end
    return v
end)

local function fieldset(arguments)
    -- [\dogetfieldset{#1}]
    return nil
end

function executers.resetform(arguments)
    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
    return pdfdictionary {
        S     = pdfconstant("ResetForm"),
        Field = fieldset(arguments[1])
    }
end

local formmethod = "post" -- "get" "post"
local formformat = "xml"  -- "xml" "html" "fdf"

-- bit 3 = html bit 6 = xml bit 4 = get

local flags = {
    get = {
        html = 12, fdf = 8, xml = 40,
    },
    post = {
        html = 4, fdf = 0, xml = 32,
    }
}

function executers.submitform(arguments)
    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
    local flag = flags[formmethod] or flags.post
    flag = (flag and (flag[formformat] or flag.xml)) or 32 -- default: post, xml
    return pdfdictionary {
        S     = pdfconstant("SubmitForm"),
        F     = arguments[1],
        Field = fieldset(arguments[2]),
        Flags = flag,
    -- \PDFsubmitfiller
    }
end

local pdf_hide = pdfconstant("Hide")

function executers.hide(arguments)
    return pdfdictionary {
        S = pdf_hide,
        H = true,
        T = arguments,
    }
end

function executers.show(arguments)
    return pdfdictionary {
        S = pdf_hide,
        H = false,
        T = arguments,
    }
end

function specials.action(var)
    local operation = var.operation
    if var.operation and operation ~= "" then
        local e = executers[operation]
        if type(e) == "table" then
            return e
        elseif type(e) == "function" then
            return e(var.arguments)
        end
    end
end

local function build(levels,start,parent,method,nested)
    local startlevel = levels[start].level
    local noflevels  = #levels
    local i = start
    local n = 0
    local child, entry, m, prev, first, last, f, l, realpage
    while i and i <= noflevels do
        local current = levels[i]
        if current.usedpage == false then
            -- safeguard
            i = i + 1
        else
            local level     = current.level
            local title     = current.title
            local reference = current.reference
            local opened    = current.opened
            local reftype   = type(reference)
            local block     = nil
            local variant   = "unknown"
            if reftype == "table" then
                -- we're okay
                variant  = "list"
                block    = reference.block
                realpage = reference.realpage
            elseif reftype == "string" then
                local resolved = references.identify("",reference)
                realpage = resolved and structures.references.setreferencerealpage(resolved) or 0
                if realpage > 0 then
                    variant   = "realpage"
                    realpage  = realpage
                    reference = structures.pages.collected[realpage]
                    block     = reference and reference.block
                end
            elseif reftype == "number" then
                if reference > 0 then
                    variant   = "realpage"
                    realpage  = reference
                    reference = structures.pages.collected[realpage]
                    block     = reference and reference.block
                end
            else
                -- error
            end
            current.block = block
            if variant == "unknown" then
                -- error, ignore
                i = i + 1
         -- elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].reference.block) then
            elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].block) then
                if nested then -- could be an option but otherwise we quit too soon
                    if entry then
                        pdfflushobject(child,entry)
                    else
                        report_bookmarks("error 1")
                    end
                    return i, n, first, last
                else
                    if i > 1 and block == levels[i-1].block then
                        -- or maybe never report
                        report_bookmarks("confusing level change at level %a around %a",level,title)
                    end
                    startlevel = level
                end
            end
            if level == startlevel then
                if trace_bookmarks then
                    report_bookmarks("%3i %w%s %s",realpage,(level-1)*2,(opened and "+") or "-",title)
                end
                local prev = child
                child = pdfreserveobject()
                if entry then
                    entry.Next = child and pdfreference(child)
                    pdfflushobject(prev,entry)
                end
                local action = nil
                if variant == "list" then
                    action = pdflinkinternal(reference.internal,reference.realpage)
                elseif variant == "realpage" then
                    action = pagereferences[realpage]
                else
                    -- hm, what to do
                end
                entry = pdfdictionary {
                    Title  = pdfunicode(title),
                    Parent = parent,
                    Prev   = prev and pdfreference(prev),
                    A      = action,
                }
             -- entry.Dest = pdflinkinternal(reference.internal,reference.realpage)
                if not first then
                    first, last = child, child
                end
                prev = child
                last = prev
                n = n + 1
                i = i + 1
            elseif i < noflevels and level > startlevel then
                i, m, f, l = build(levels,i,pdfreference(child),method,true)
                if entry then
                    entry.Count = (opened and m) or -m
                    if m > 0 then
                        entry.First = pdfreference(f)
                        entry.Last  = pdfreference(l)
                    end
                else
                    report_bookmarks("error 2")
                end
            else
                -- missing intermediate level but ok
                i, m, f, l = build(levels,i,pdfreference(child),method,true)
                if entry then
                    entry.Count = (opened and m) or -m
                    if m > 0 then
                        entry.First = pdfreference(f)
                        entry.Last  = pdfreference(l)
                    end
                    pdfflushobject(child,entry)
                else
                    report_bookmarks("error 3")
                end
                return i, n, first, last
            end
        end
    end
    pdfflushobject(child,entry)
    return nil, n, first, last
end

function codeinjections.addbookmarks(levels,method)
    if levels and #levels > 0 then
        local parent = pdfreserveobject()
        local _, m, first, last = build(levels,1,pdfreference(parent),method or "internal",false)
        local dict = pdfdictionary {
            Type  = pdfconstant("Outlines"),
            First = pdfreference(first),
            Last  = pdfreference(last),
            Count = m,
        }
        pdfflushobject(parent,dict)
        pdfaddtocatalog("Outlines",lpdf.reference(parent))
    end
end

-- this could also be hooked into the frontend finalizer

lpdf.registerdocumentfinalizer(function() bookmarks.place() end,1,"bookmarks") -- hm, why indirect call
