if not modules then modules = { } end modules ['lpdf-ini'] = {
    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"
}

-- beware of "too many locals" here

local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset
local char, byte, format, gsub, concat, match, sub, gmatch = string.char, string.byte, string.format, string.gsub, table.concat, string.match, string.sub, string.gmatch
local utfchar, utfbyte, utfvalues = utf.char, utf.byte, utf.values
local sind, cosd, floor, max, min = math.sind, math.cosd, math.floor, math.max, math.min
local lpegmatch, P, C, R, S, Cc, Cs = lpeg.match, lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs
local formatters = string.formatters
local isboolean = string.is_boolean

local report_objects    = logs.reporter("backend","objects")
local report_finalizing = logs.reporter("backend","finalizing")
local report_blocked    = logs.reporter("backend","blocked")

local implement         = interfaces.implement
local two_strings       = interfaces.strings[2]

-- In ConTeXt MkIV we use utf8 exclusively so all strings get mapped onto a hex
-- encoded utf16 string type between <>. We could probably save some bytes by using
-- strings between () but then we end up with escaped ()\ too.

-- gethpos              : used
-- getpos               : used
-- getvpos              : used
--
-- getmatrix            : used
-- hasmatrix            : used
--
-- mapfile              : used in font-ctx.lua
-- mapline              : used in font-ctx.lua
--
-- maxobjnum            : not used
-- obj                  : used
-- immediateobj         : used
-- objtype              : not used
-- pageref              : used
-- print                : can be used
-- refobj               : used
-- registerannot        : not to be used
-- reserveobj           : used

-- pdf.catalog          : used
-- pdf.info             : used
-- pdf.trailer          : used
-- pdf.names            : not to be used

-- pdf.setinfo          : used
-- pdf.setcatalog       : used
-- pdf.setnames         : not to be used
-- pdf.settrailer       : used

-- pdf.getinfo          : used
-- pdf.getcatalog       : used
-- pdf.getnames         : not to be used
-- pdf.gettrailer       : used

local pdf    = pdf
local factor = number.dimenfactors.bp

if pdf.setinfo then
 -- table.setmetatablenewindex(pdf,function(t,k,v)
 --     report_blocked("'pdf.%s' is not supported",k)
 -- end)
    -- the getters are harmless
end

if not pdf.setinfo then
    function pdf.setinfo   (s) pdf.info    = s end
    function pdf.setcatalog(s) pdf.catalog = s end
    function pdf.setnames  (s) pdf.names   = s end
    function pdf.settrailer(s) pdf.trailer = s end
end

if not pdf.getpos then
    function pdf.getpos   () return pdf.h, pdf.v     end
    function pdf.gethpos  () return pdf.h            end
    function pdf.getvpos  () return pdf.v            end
    function pdf.hasmatrix() return false            end
    function pdf.getmatrix() return 1, 0, 0, 1, 0, 0 end
end

if not pdf.setpageresources then
    function pdf.setpageresources  (s) pdf.pageresources   = s end
    function pdf.setpageattributes (s) pdf.pageattributes  = s end
    function pdf.setpagesattributes(s) pdf.pagesattributes = s end
end

local pdfsetinfo            = pdf.setinfo
local pdfsetcatalog         = pdf.setcatalog
local pdfsetnames           = pdf.setnames
local pdfsettrailer         = pdf.settrailer

local pdfsetpageresources   = pdf.setpageresources
local pdfsetpageattributes  = pdf.setpageattributes
local pdfsetpagesattributes = pdf.setpagesattributes

local pdfgetpos             = pdf.getpos
local pdfgethpos            = pdf.gethpos
local pdfgetvpos            = pdf.getvpos
local pdfgetmatrix          = pdf.getmatrix
local pdfhasmatrix          = pdf.hasmatrix

local pdfreserveobject      = pdf.reserveobj
local pdfimmediateobject    = pdf.immediateobj
local pdfdeferredobject     = pdf.obj
local pdfreferenceobject    = pdf.refobj

-- function pdf.setinfo           () report_blocked("'pdf.%s' is not supported","setinfo")            end -- use lpdf.addtoinfo etc
-- function pdf.setcatalog        () report_blocked("'pdf.%s' is not supported","setcatalog")         end
-- function pdf.setnames          () report_blocked("'pdf.%s' is not supported","setnames")           end
-- function pdf.settrailer        () report_blocked("'pdf.%s' is not supported","settrailer")         end
-- function pdf.setpageresources  () report_blocked("'pdf.%s' is not supported","setpageresources")   end
-- function pdf.setpageattributes () report_blocked("'pdf.%s' is not supported","setpageattributes")  end
-- function pdf.setpagesattributes() report_blocked("'pdf.%s' is not supported","setpagesattributes") end
-- function pdf.registerannot     () report_blocked("'pdf.%s' is not supported","registerannot")      end

local function pdfdisablecommand(command)
    pdf[command] = function() report_blocked("'pdf.%s' is not supported",command) end
end

pdfdisablecommand("setinfo")
pdfdisablecommand("setcatalog")
pdfdisablecommand("setnames")
pdfdisablecommand("settrailer")
pdfdisablecommand("setpageresources")
pdfdisablecommand("setpageattributes")
pdfdisablecommand("setpagesattributes")
pdfdisablecommand("registerannot")

local trace_finalizers = false  trackers.register("backend.finalizers", function(v) trace_finalizers = v end)
local trace_resources  = false  trackers.register("backend.resources",  function(v) trace_resources  = v end)
local trace_objects    = false  trackers.register("backend.objects",    function(v) trace_objects    = v end)
local trace_detail     = false  trackers.register("backend.detail",     function(v) trace_detail     = v end)

local backends   = backends
local pdfbackend = {
    comment        = "backend for directly generating pdf output",
    nodeinjections = { },
    codeinjections = { },
    registrations  = { },
    tables         = { },
}
backends.pdf     = pdfbackend
lpdf             = lpdf or { }
local lpdf       = lpdf

local codeinjections = pdfbackend.codeinjections
local nodeinjections = pdfbackend.nodeinjections

codeinjections.getpos    = pdfgetpos     lpdf.getpos    = pdfgetpos
codeinjections.gethpos   = pdfgethpos    lpdf.gethpos   = pdfgethpos
codeinjections.getvpos   = pdfgetvpos    lpdf.getvpos   = pdfgetvpos
codeinjections.hasmatrix = pdfhasmatrix  lpdf.hasmatrix = pdfhasmatrix
codeinjections.getmatrix = pdfgetmatrix  lpdf.getmatrix = pdfgetmatrix

function lpdf.transform(llx,lly,urx,ury)
    if pdfhasmatrix() then
        local sx, rx, ry, sy = pdfgetmatrix()
        local w, h = urx - llx, ury - lly
        return llx, lly, llx + sy*w - ry*h, lly + sx*h - rx*w
    else
        return llx, lly, urx, ury
    end
end

-- function lpdf.rectangle(width,height,depth)
--     local h, v = pdfgetpos()
--     local llx, lly, urx, ury
--     if pdfhasmatrix() then
--         local sx, rx, ry, sy = pdfgetmatrix()
--         llx = 0
--         lly = -depth
--      -- llx =                ry * depth
--      -- lly = -sx * depth
--         urx =  sy * width  - ry * height
--         ury =  sx * height - rx * width
--     else
--         llx = 0
--         lly = -depth
--         urx = width
--         ury = height
--         return (h+llx)*factor, (v+lly)*factor, (h+urx)*factor, (v+ury)*factor
--     end
-- end

function lpdf.rectangle(width,height,depth)
    local h, v = pdfgetpos()
    if pdfhasmatrix() then
        local sx, rx, ry, sy = pdfgetmatrix()
     -- return (h+ry*depth)*factor, (v-sx*depth)*factor, (h+sy*width-ry*height)*factor, (v+sx*height-rx*width)*factor
        return h           *factor, (v-   depth)*factor, (h+sy*width-ry*height)*factor, (v+sx*height-rx*width)*factor
    else
        return h           *factor, (v-   depth)*factor, (h+   width          )*factor, (v+   height         )*factor
    end
end

-- we could use a hash of predefined unicodes

-- local function tosixteen(str) -- an lpeg might be faster (no table)
--     if not str or str == "" then
--         return "<feff>" -- not () as we want an indication that it's unicode
--     else
--         local r, n = { "<feff" }, 1
--         for b in utfvalues(str) do
--             n = n + 1
--             if b < 0x10000 then
--                 r[n] = format("%04x",b)
--             else
--              -- r[n] = format("%04x%04x",b/1024+0xD800,b%1024+0xDC00)
--                 r[n] = format("%04x%04x",floor(b/1024),b%1024+0xDC00) --bit32.rshift(b,10)
--             end
--         end
--         n = n + 1
--         r[n] = ">"
--         return concat(r)
--     end
-- end

local cache = table.setmetatableindex(function(t,k) -- can be made weak
    local v = utfbyte(k)
    if v < 0x10000 then
        v = format("%04x",v)
    else
     -- v = format("%04x%04x",v/1024+0xD800,v%1024+0xDC00)
        v = format("%04x%04x",floor(v/1024),v%1024+0xDC00)
    end
    t[k] = v
    return v
end)

local escaped = Cs(Cc("(") * (S("\\()")/"\\%0" + P(1))^0 * Cc(")"))
local unified = Cs(Cc("<feff") * (lpeg.patterns.utf8character/cache)^1 * Cc(">"))

local function tosixteen(str) -- an lpeg might be faster (no table)
    if not str or str == "" then
        return "<feff>" -- not () as we want an indication that it's unicode
    else
        return lpegmatch(unified,str)
    end
end

local more = 0

local pattern = C(4) / function(s) -- needs checking !
    local now = tonumber(s,16)
    if more > 0 then
        now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000 -- the 0x10000 smells wrong
        more = 0
        return utfchar(now)
    elseif now >= 0xD800 and now <= 0xDBFF then
        more = now
        return "" -- else the c's end up in the stream
    else
        return utfchar(now)
    end
end

local pattern = P(true) / function() more = 0 end * Cs(pattern^0)

local function fromsixteen(str)
    if not str or str == "" then
        return ""
    else
        return lpegmatch(pattern,str)
    end
end

local toregime   = regimes.toregime
local fromregime = regimes.fromregime

local function topdfdoc(str,default)
    if not str or str == "" then
        return ""
    else
        return lpegmatch(escaped,toregime("pdfdoc",str,default)) -- could be combined if needed
    end
end

local function frompdfdoc(str)
    if not str or str == "" then
        return ""
    else
        return fromregime("pdfdoc",str)
    end
end

if not toregime   then topdfdoc   = function(s) return s end end
if not fromregime then frompdfdoc = function(s) return s end end

local function toeight(str)
    if not str or str == "" then
        return "()"
    else
        return lpegmatch(escaped,str)
    end
end

lpdf.tosixteen   = tosixteen
lpdf.toeight     = toeight
lpdf.topdfdoc    = topdfdoc
lpdf.fromsixteen = fromsixteen
lpdf.frompdfdoc  = frompdfdoc

local function merge_t(a,b)
    local t = { }
    for k,v in next, a do t[k] = v end
    for k,v in next, b do t[k] = v end
    return setmetatable(t,getmetatable(a))
end

local f_key_null       = formatters["/%s null"]
local f_key_value      = formatters["/%s %s"]
local f_key_dictionary = formatters["/%s << % t >>"]
local f_dictionary     = formatters["<< % t >>"]
local f_key_array      = formatters["/%s [ % t ]"]
local f_array          = formatters["[ % t ]"]
local f_key_number     = formatters["/%s %F"]
local f_tonumber       = formatters["%F"]

-- local f_key_value      = formatters["/%s %s"]
-- local f_key_dictionary = formatters["/%s <<% t>>"]
-- local f_dictionary     = formatters["<<% t>>"]
-- local f_key_array      = formatters["/%s [% t]"]
-- local f_array          = formatters["[% t]"]

local tostring_a, tostring_d

tostring_d = function(t,contentonly,key)
    if next(t) then
        local r, rn = { }, 0
        for k, v in next, t do
            rn = rn + 1
            local tv = type(v)
            if tv == "string" then
                r[rn] = f_key_value(k,toeight(v))
            elseif tv == "number" then
                r[rn] = f_key_number(k,v)
         -- elseif tv == "unicode" then -- can't happen
         --     r[rn] = f_key_value(k,tosixteen(v))
            elseif tv == "table" then
                local mv = getmetatable(v)
                if mv and mv.__lpdftype then
                 -- if v == t then
                 --     report_objects("ignoring circular reference in dirctionary")
                 --     r[rn] = f_key_null(k)
                 -- else
                        r[rn] = f_key_value(k,tostring(v))
                 -- end
                elseif v[1] then
                    r[rn] = f_key_value(k,tostring_a(v))
                else
                    r[rn] = f_key_value(k,tostring_d(v))
                end
            else
                r[rn] = f_key_value(k,tostring(v))
            end
        end
        if contentonly then
            return concat(r," ")
        elseif key then
            return f_key_dictionary(key,r)
        else
            return f_dictionary(r)
        end
    elseif contentonly then
        return ""
    else
        return "<< >>"
    end
end

tostring_a = function(t,contentonly,key)
    local tn = #t
    if tn ~= 0 then
        local r = { }
        for k=1,tn do
            local v = t[k]
            local tv = type(v)
            if tv == "string" then
                r[k] = toeight(v)
            elseif tv == "number" then
                r[k] = f_tonumber(v)
         -- elseif tv == "unicode" then
         --     r[k] = tosixteen(v)
            elseif tv == "table" then
                local mv = getmetatable(v)
                local mt = mv and mv.__lpdftype
                if mt then
                 -- if v == t then
                 --     report_objects("ignoring circular reference in array")
                 --     r[k] = "null"
                 -- else
                        r[k] = tostring(v)
                 -- end
                elseif v[1] then
                    r[k] = tostring_a(v)
                else
                    r[k] = tostring_d(v)
                end
            else
                r[k] = tostring(v)
            end
        end
        if contentonly then
            return concat(r, " ")
        elseif key then
            return f_key_array(key,r)
        else
            return f_array(r)
        end
    elseif contentonly then
        return ""
    else
        return "[ ]"
    end
end

local tostring_x = function(t) return concat(t," ")       end
local tostring_s = function(t) return toeight(t[1])       end
local tostring_p = function(t) return topdfdoc(t[1],t[2]) end
local tostring_u = function(t) return tosixteen(t[1])     end
----- tostring_n = function(t) return tostring(t[1])      end -- tostring not needed
local tostring_n = function(t) return f_tonumber(t[1])    end -- tostring not needed
local tostring_c = function(t) return t[1]                end -- already prefixed (hashed)
local tostring_z = function()  return "null"              end
local tostring_t = function()  return "true"              end
local tostring_f = function()  return "false"             end
local tostring_r = function(t) local n = t[1] return n and n > 0 and (n .. " 0 R") or "null" end

local tostring_v = function(t)
    local s = t[1]
    if type(s) == "table" then
        return concat(s)
    else
        return s
    end
end

local function value_x(t) return t                  end
local function value_s(t) return t[1]               end
local function value_p(t) return t[1]               end
local function value_u(t) return t[1]               end
local function value_n(t) return t[1]               end
local function value_c(t) return sub(t[1],2)        end
local function value_d(t) return tostring_d(t,true) end
local function value_a(t) return tostring_a(t,true) end
local function value_z()  return nil                end
local function value_t(t) return t.value or true    end
local function value_f(t) return t.value or false   end
local function value_r()  return t[1] or 0          end -- null
local function value_v()  return t[1]               end

local function add_x(t,k,v) rawset(t,k,tostring(v)) end

local mt_x = { __lpdftype = "stream",     __tostring = tostring_x, __call = value_x, __newindex = add_x }
local mt_d = { __lpdftype = "dictionary", __tostring = tostring_d, __call = value_d }
local mt_a = { __lpdftype = "array",      __tostring = tostring_a, __call = value_a }
local mt_u = { __lpdftype = "unicode",    __tostring = tostring_u, __call = value_u }
local mt_s = { __lpdftype = "string",     __tostring = tostring_s, __call = value_s }
local mt_p = { __lpdftype = "docstring",  __tostring = tostring_p, __call = value_p }
local mt_n = { __lpdftype = "number",     __tostring = tostring_n, __call = value_n }
local mt_c = { __lpdftype = "constant",   __tostring = tostring_c, __call = value_c }
local mt_z = { __lpdftype = "null",       __tostring = tostring_z, __call = value_z }
local mt_t = { __lpdftype = "true",       __tostring = tostring_t, __call = value_t }
local mt_f = { __lpdftype = "false",      __tostring = tostring_f, __call = value_f }
local mt_r = { __lpdftype = "reference",  __tostring = tostring_r, __call = value_r }
local mt_v = { __lpdftype = "verbose",    __tostring = tostring_v, __call = value_v }

local function pdfstream(t) -- we need to add attributes
    if t then
        for i=1,#t do
            t[i] = tostring(t[i])
        end
    end
    return setmetatable(t or { },mt_x)
end

local function pdfdictionary(t)
    return setmetatable(t or { },mt_d)
end

local function pdfarray(t)
    if type(t) == "string" then
        return setmetatable({ t },mt_a)
    else
        return setmetatable(t or { },mt_a)
    end
end

local function pdfstring(str,default)
    return setmetatable({ str or default or "" },mt_s)
end

local function pdfdocstring(str,default,defaultchar)
    return setmetatable({ str or default or "", defaultchar or " " },mt_p)
end

local function pdfunicode(str,default)
    return setmetatable({ str or default or "" },mt_u) -- could be a string
end

local cache = { } -- can be weak

local function pdfnumber(n,default) -- 0-10
    n = n or default
    local c = cache[n]
    if not c then
        c = setmetatable({ n },mt_n)
    --  cache[n] = c -- too many numbers
    end
    return c
end

for i=-1,9 do cache[i] = pdfnumber(i) end

local cache = { } -- can be weak

local forbidden, replacements = "\0\t\n\r\f ()[]{}/%%#\\", { } -- table faster than function

for s in gmatch(forbidden,".") do
    replacements[s] = format("#%02x",byte(s))
end

local escaped = Cs(Cc("/") * (S(forbidden)/replacements + P(1))^0)

local function pdfconstant(str,default)
    str = str or default or ""
    local c = cache[str]
    if not c then
     -- c = setmetatable({ "/" .. str },mt_c)
        c = setmetatable({ lpegmatch(escaped,str) },mt_c)
        cache[str] = c
    end
    return c
end

local p_null  = { } setmetatable(p_null, mt_z)
local p_true  = { } setmetatable(p_true, mt_t)
local p_false = { } setmetatable(p_false,mt_f)

local function pdfnull()
    return p_null
end

--~ print(pdfboolean(false),pdfboolean(false,false),pdfboolean(false,true))
--~ print(pdfboolean(true),pdfboolean(true,false),pdfboolean(true,true))
--~ print(pdfboolean(nil,true),pdfboolean(nil,false))

local function pdfboolean(b,default)
    if type(b) == "boolean" then
        return b and p_true or p_false
    else
        return default and p_true or p_false
    end
end

local r_zero = setmetatable({ 0 },mt_r)

local function pdfreference(r)  -- maybe make a weak table
    if r and r ~= 0 then
        return setmetatable({ r },mt_r)
    else
        return r_zero
    end
end

local v_zero  = setmetatable({ 0  },mt_v)
local v_empty = setmetatable({ "" },mt_v)

local function pdfverbose(t) -- maybe check for type
    if t == 0 then
        return v_zero
    elseif t == "" then
        return v_empty
    else
        return setmetatable({ t },mt_v)
    end
end

lpdf.stream      = pdfstream -- THIS WILL PROBABLY CHANGE
lpdf.dictionary  = pdfdictionary
lpdf.array       = pdfarray
lpdf.docstring   = pdfdocstring
lpdf.string      = pdfstring
lpdf.unicode     = pdfunicode
lpdf.number      = pdfnumber
lpdf.constant    = pdfconstant
lpdf.null        = pdfnull
lpdf.boolean     = pdfboolean
lpdf.reference   = pdfreference
lpdf.verbose     = pdfverbose

local names, cache = { }, { }

function lpdf.reserveobject(name)
    local r = pdfreserveobject() -- we don't support "annot"
    if name then
        names[name] = r
        if trace_objects then
            report_objects("reserving number %a under name %a",r,name)
        end
    elseif trace_objects then
        report_objects("reserving number %a",r)
    end
    return r
end

-- lpdf.immediateobject = pdfimmediateobject
-- lpdf.deferredobject  = pdfdeferredobject
-- lpdf.object          = pdfdeferredobject
-- lpdf.referenceobject = pdfreferenceobject

local pagereference = pdf.pageref -- tex.pdfpageref is obsolete
local nofpages      = 0

function lpdf.pagereference(n)
    if nofpages == 0 then
        nofpages = structures.pages.nofpages
        if nofpages == 0 then
            nofpages = 1
        end
    end
    if n > nofpages then
        return pagereference(nofpages) -- or 1, could be configureable
    else
        return pagereference(n)
    end
end

function lpdf.delayedobject(data,n)
    if n then
        pdfdeferredobject(n,data)
    else
        n = pdfdeferredobject(data)
    end
    pdfreferenceobject(n)
    return n
end

function lpdf.flushobject(name,data)
    if data then
        local named = names[name]
        if named then
            if not trace_objects then
            elseif trace_detail then
                report_objects("flushing data to reserved object with name %a, data: %S",name,data)
            else
                report_objects("flushing data to reserved object with name %a",name)
            end
            return pdfimmediateobject(named,tostring(data))
        else
            if not trace_objects then
            elseif trace_detail then
                report_objects("flushing data to reserved object with number %s, data: %S",name,data)
            else
                report_objects("flushing data to reserved object with number %s",name)
            end
            return pdfimmediateobject(name,tostring(data))
        end
    else
        if trace_objects and trace_detail then
            report_objects("flushing data: %S",name)
        end
        return pdfimmediateobject(tostring(name))
    end
end


function lpdf.flushstreamobject(data,dict,compressed) -- default compressed
    if trace_objects then
        report_objects("flushing stream object of %s bytes",#data)
    end
    local dtype = type(dict)
    return pdfdeferredobject {
        immediate     = true,
        compresslevel = compressed == false and 0 or nil,
        type          = "stream",
        string        = data,
        attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
    }
end

function lpdf.flushstreamfileobject(filename,dict,compressed) -- default compressed
    if trace_objects then
        report_objects("flushing stream file object %a",filename)
    end
    local dtype = type(dict)
    return pdfdeferredobject {
        immediate     = true,
        compresslevel = compressed == false and 0 or nil,
        type          = "stream",
        file          = filename,
        attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
    }
end

local shareobjectcache, shareobjectreferencecache = { }, { }

function lpdf.shareobject(content)
    if content == nil then
        -- invalid object not created
    else
        content = tostring(content)
        local o = shareobjectcache[content]
        if not o then
            o = pdfimmediateobject(content)
            shareobjectcache[content] = o
        end
        return o
    end
end

function lpdf.shareobjectreference(content)
    if content == nil then
        -- invalid object not created
    else
        content = tostring(content)
        local r = shareobjectreferencecache[content]
        if not r then
            local o = shareobjectcache[content]
            if not o then
                o = pdfimmediateobject(content)
                shareobjectcache[content] = o
            end
            r = pdfreference(o)
            shareobjectreferencecache[content] = r
        end
        return r
    end
end

-- three priority levels, default=2

local pagefinalizers     = { { }, { }, { } }
local documentfinalizers = { { }, { }, { } }

local pageresources, pageattributes, pagesattributes

local function resetpageproperties()
    pageresources   = pdfdictionary()
    pageattributes  = pdfdictionary()
    pagesattributes = pdfdictionary()
end

resetpageproperties()

local function setpageproperties()
    pdfsetpageresources  (pageresources  ())
    pdfsetpageattributes (pageattributes ())
    pdfsetpagesattributes(pagesattributes())
end

local function addtopageresources  (k,v) pageresources  [k] = v end
local function addtopageattributes (k,v) pageattributes [k] = v end
local function addtopagesattributes(k,v) pagesattributes[k] = v end

lpdf.addtopageresources   = addtopageresources
lpdf.addtopageattributes  = addtopageattributes
lpdf.addtopagesattributes = addtopagesattributes

local function set(where,what,f,when,comment)
    if type(when) == "string" then
        when, comment = 2, when
    elseif not when then
        when = 2
    end
    local w = where[when]
    w[#w+1] = { f, comment }
    if trace_finalizers then
        report_finalizing("%s set: [%s,%s]",what,when,#w)
    end
end

local function run(where,what)
    if trace_finalizers then
        report_finalizing("start backend, category %a, n %a",what,#where)
    end
    for i=1,#where do
        local w = where[i]
        for j=1,#w do
            local wj = w[j]
            if trace_finalizers then
                report_finalizing("%s finalizer: [%s,%s] %s",what,i,j,wj[2] or "")
            end
            wj[1]()
        end
    end
    if trace_finalizers then
        report_finalizing("stop finalizing")
    end
end

local function registerpagefinalizer(f,when,comment)
    set(pagefinalizers,"page",f,when,comment)
end

local function registerdocumentfinalizer(f,when,comment)
    set(documentfinalizers,"document",f,when,comment)
end

lpdf.registerpagefinalizer     = registerpagefinalizer
lpdf.registerdocumentfinalizer = registerdocumentfinalizer

function lpdf.finalizepage(shipout)
    if shipout and not environment.initex then
     -- resetpageproperties() -- maybe better before
        run(pagefinalizers,"page")
        setpageproperties()
        resetpageproperties() -- maybe better before
    end
end

function lpdf.finalizedocument()
    if not environment.initex then
        run(documentfinalizers,"document")
        function lpdf.finalizedocument()
            report_finalizing("serious error: the document is finalized multiple times")
            function lpdf.finalizedocument() end
        end
    end
end

-- codeinjections.finalizepage = lpdf.finalizepage -- no longer triggered at the tex end

if not callbacks.register("finish_pdfpage", lpdf.finalizepage) then

    local find_tail    = nodes.tail
    local latelua_node = nodes.pool.latelua

    function nodeinjections.finalizepage(head)
        local t = find_tail(head.list)
        if t then
            local n = latelua_node("lpdf.finalizepage(true)") -- last in the shipout
            t.next = n
            n.prev = t
        end
        return head, true
    end

    nodes.tasks.appendaction("shipouts","normalizers","backends.pdf.nodeinjections.finalizepage")

end

callbacks.register("finish_pdffile", lpdf.finalizedocument)


do

    -- some minimal tracing, handy for checking the order

    local function trace_set(what,key)
        if trace_resources then
            report_finalizing("setting key %a in %a",key,what)
        end
    end

    local function trace_flush(what)
        if trace_resources then
            report_finalizing("flushing %a",what)
        end
    end

    lpdf.protectresources = true

    local catalog = pdfdictionary { Type = pdfconstant("Catalog") } -- nicer, but when we assign we nil the Type
    local info    = pdfdictionary { Type = pdfconstant("Info")    } -- nicer, but when we assign we nil the Type
    ----- names   = pdfdictionary { Type = pdfconstant("Names")   } -- nicer, but when we assign we nil the Type

    local function flushcatalog()
        if not environment.initex then
            trace_flush("catalog")
            catalog.Type = nil
            pdfsetcatalog(catalog())
        end
    end

    local function flushinfo()
        if not environment.initex then
            trace_flush("info")
            info.Type = nil
            pdfsetinfo(info())
        end
    end

    -- local function flushnames()
    --     if not environment.initex then
    --         trace_flush("names")
    --         names.Type = nil
    --         pdfsetnames(names())
    --     end
    -- end

    function lpdf.addtocatalog(k,v)
        if not (lpdf.protectresources and catalog[k]) then
            trace_set("catalog",k)
            catalog[k] = v
        end
    end

    function lpdf.addtoinfo(k,v)
        if not (lpdf.protectresources and info[k]) then
            trace_set("info",k)
            info[k] = v
        end
    end

    -- local function lpdf.addtonames(k,v)
    --     if not (lpdf.protectresources and names[k]) then
    --         trace_set("names",k)
    --         names[k] = v
    --     end
    -- end

    local names = pdfdictionary {
     -- Type = pdfconstant("Names")
    }

    local function flushnames()
        if next(names) and not environment.initex then
            names.Type = pdfconstant("Names")
            trace_flush("names")
            lpdf.addtocatalog("Names",pdfreference(pdfimmediateobject(tostring(names))))
        end
    end

    function lpdf.addtonames(k,v)
        if not (lpdf.protectresources and names[k]) then
            trace_set("names",  k)
            names  [k] = v
        end
    end

    local r_extgstates,  d_extgstates  = pdfreserveobject(), pdfdictionary()  local p_extgstates  = pdfreference(r_extgstates)
    local r_colorspaces, d_colorspaces = pdfreserveobject(), pdfdictionary()  local p_colorspaces = pdfreference(r_colorspaces)
    local r_patterns,    d_patterns    = pdfreserveobject(), pdfdictionary()  local p_patterns    = pdfreference(r_patterns)
    local r_shades,      d_shades      = pdfreserveobject(), pdfdictionary()  local p_shades      = pdfreference(r_shades)

    local function checkextgstates () if next(d_extgstates ) then addtopageresources("ExtGState", p_extgstates ) end end
    local function checkcolorspaces() if next(d_colorspaces) then addtopageresources("ColorSpace",p_colorspaces) end end
    local function checkpatterns   () if next(d_patterns   ) then addtopageresources("Pattern",   p_patterns   ) end end
    local function checkshades     () if next(d_shades     ) then addtopageresources("Shading",   p_shades     ) end end

    local function flushextgstates () if next(d_extgstates ) then trace_flush("extgstates")  pdfimmediateobject(r_extgstates, tostring(d_extgstates )) end end
    local function flushcolorspaces() if next(d_colorspaces) then trace_flush("colorspaces") pdfimmediateobject(r_colorspaces,tostring(d_colorspaces)) end end
    local function flushpatterns   () if next(d_patterns   ) then trace_flush("patterns")    pdfimmediateobject(r_patterns,   tostring(d_patterns   )) end end
    local function flushshades     () if next(d_shades     ) then trace_flush("shades")      pdfimmediateobject(r_shades,     tostring(d_shades     )) end end

    function lpdf.collectedresources()
        local ExtGState  = next(d_extgstates ) and p_extgstates
        local ColorSpace = next(d_colorspaces) and p_colorspaces
        local Pattern    = next(d_patterns   ) and p_patterns
        local Shading    = next(d_shades     ) and p_shades
        if ExtGState or ColorSpace or Pattern or Shading then
            local collected = pdfdictionary {
                ExtGState  = ExtGState,
                ColorSpace = ColorSpace,
                Pattern    = Pattern,
                Shading    = Shading,
             -- ProcSet    = pdfarray { pdfconstant("PDF") },
            }
            return collected()
        else
            return ""
        end
    end

    function lpdf.adddocumentextgstate (k,v) d_extgstates [k] = v end
    function lpdf.adddocumentcolorspace(k,v) d_colorspaces[k] = v end
    function lpdf.adddocumentpattern   (k,v) d_patterns   [k] = v end
    function lpdf.adddocumentshade     (k,v) d_shades     [k] = v end

    registerdocumentfinalizer(flushextgstates,3,"extended graphic states")
    registerdocumentfinalizer(flushcolorspaces,3,"color spaces")
    registerdocumentfinalizer(flushpatterns,3,"patterns")
    registerdocumentfinalizer(flushshades,3,"shades")

    registerdocumentfinalizer(flushnames,3,"names") -- before catalog
    registerdocumentfinalizer(flushcatalog,3,"catalog")
    registerdocumentfinalizer(flushinfo,3,"info")

    registerpagefinalizer(checkextgstates,3,"extended graphic states")
    registerpagefinalizer(checkcolorspaces,3,"color spaces")
    registerpagefinalizer(checkpatterns,3,"patterns")
    registerpagefinalizer(checkshades,3,"shades")

end

-- in strc-bkm: lpdf.registerdocumentfinalizer(function() structures.bookmarks.place() end,1)

function lpdf.rotationcm(a)
    local s, c = sind(a), cosd(a)
    return format("%0.6F %0.6F %0.6F %0.6F 0 0 cm",c,s,-s,c)
end

-- ! -> universaltime

do

    local timestamp = os.date("%Y-%m-%dT%X") .. os.timezone(true)

    function lpdf.timestamp()
        return timestamp
    end

    function lpdf.pdftimestamp(str)
        local Y, M, D, h, m, s, Zs, Zh, Zm = match(str,"^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)([%+%-])(%d%d):(%d%d)$")
        return Y and format("D:%s%s%s%s%s%s%s%s'%s'",Y,M,D,h,m,s,Zs,Zh,Zm)
    end

    function lpdf.id()
        return format("%s.%s",tex.jobname,timestamp)
    end

end

-- return nil is nicer in test prints

function lpdf.checkedkey(t,key,variant)
    local pn = t and t[key]
    if pn ~= nil then
        local tn = type(pn)
        if tn == variant then
            if variant == "string" then
                if pn ~= "" then
                    return pn
                end
            elseif variant == "table" then
                if next(pn) then
                    return pn
                end
            else
                return pn
            end
        elseif tn == "string" then
            if variant == "number" then
                return tonumber(pn)
            elseif variant == "boolean" then
                return isboolean(pn,nil,true)
            end
        end
    end
 -- return nil
end

function lpdf.checkedvalue(value,variant) -- code not shared
    if value ~= nil then
        local tv = type(value)
        if tv == variant then
            if variant == "string" then
                if value ~= "" then
                    return value
                end
            elseif variant == "table" then
                if next(value) then
                    return value
                end
            else
                return value
            end
        elseif tv == "string" then
            if variant == "number" then
                return tonumber(value)
            elseif variant == "boolean" then
                return isboolean(value,nil,true)
            end
        end
    end
 -- return nil
end

function lpdf.limited(n,min,max,default)
    if not n then
        return default
    else
        n = tonumber(n)
        if not n then
            return default
        elseif n > max then
            return max
        elseif n < min then
            return min
        else
            return n
        end
    end
end

-- lpdf.addtoinfo("ConTeXt.Version", environment.version)
-- lpdf.addtoinfo("ConTeXt.Time",    os.date("%Y.%m.%d %H:%M")) -- :%S
-- lpdf.addtoinfo("ConTeXt.Jobname", environment.jobname)
-- lpdf.addtoinfo("ConTeXt.Url",     "www.pragma-ade.com")
-- lpdf.addtoinfo("ConTeXt.Support", "contextgarden.net")

-- if not pdfreferenceobject then
--
--     local delayed = { }
--
--     local function flush()
--         local n = 0
--         for k,v in next, delayed do
--             pdfimmediateobject(k,v)
--             n = n + 1
--         end
--         if trace_objects then
--             report_objects("%s objects flushed",n)
--         end
--         delayed = { }
--     end
--
--     lpdf.registerdocumentfinalizer(flush,3,"objects") -- so we need a final flush too
--     lpdf.registerpagefinalizer    (flush,3,"objects") -- somehow this lags behind .. I need to look into that some day
--
--     function lpdf.delayedobject(data)
--         local n = pdfreserveobject()
--         delayed[n] = data
--         return n
--     end
--
-- end

-- setmetatable(pdf, {
--     __index = function(t,k)
--         if     k == "info"           then return pdf.getinfo()
--         elseif k == "catalog"        then return pdf.getcatalog()
--         elseif k == "names"          then return pdf.getnames()
--         elseif k == "trailer"        then return pdf.gettrailer()
--         elseif k == "pageattribute"  then return pdf.getpageattribute()
--         elseif k == "pageattributes" then return pdf.getpageattributes()
--         elseif k == "pageresources"  then return pdf.getpageresources()
--         elseif
--             return nil
--         end
--     end,
--     __newindex = function(t,k,v)
--         if     k == "info"           then return pdf.setinfo(v)
--         elseif k == "catalog"        then return pdf.setcatalog(v)
--         elseif k == "names"          then return pdf.setnames(v)
--         elseif k == "trailer"        then return pdf.settrailer(v)
--         elseif k == "pageattribute"  then return pdf.setpageattribute(v)
--         elseif k == "pageattributes" then return pdf.setpageattributes(v)
--         elseif k == "pageresources"  then return pdf.setpageresources(v)
--         else
--             rawset(t,k,v)
--         end
--     end,
-- })


-- The next variant of ActualText is what Taco and I could come up with
-- eventually. As of September 2013 Acrobat copies okay, Sumatra copies a
-- question mark, pdftotext injects an extra space and Okular adds a
-- newline plus space.

-- return formatters["BT /Span << /ActualText (CONTEXT) >> BDC [<feff>] TJ % t EMC ET"](code)

do

    local f_actual_text_one = formatters["BT /Span << /ActualText <feff%04x> >> BDC [<feff>] TJ %s EMC ET"]
    local f_actual_text_two = formatters["BT /Span << /ActualText <feff%04x%04x> >> BDC [<feff>] TJ %s EMC ET"]
    local f_actual_text     = formatters["/Span <</ActualText %s >> BDC"]

    local context           = context
    local pdfdirect         = nodes.pool.pdfdirect

    function codeinjections.unicodetoactualtext(unicode,pdfcode)
        if unicode < 0x10000 then
            return f_actual_text_one(unicode,pdfcode)
        else
            return f_actual_text_two(unicode/1024+0xD800,unicode%1024+0xDC00,pdfcode)
        end
    end

    implement {
        name      = "startactualtext",
        arguments = "string",
        actions   = function(str)
            context(pdfdirect(f_actual_text(tosixteen(str))))
        end
    }

    implement {
        name      = "stopactualtext",
        actions   = function()
            context(pdfdirect("EMC"))
        end
    }

end

-- interface

local lpdfverbose = lpdf.verbose

implement { name = "lpdf_collectedresources",                             actions = { lpdf.collectedresources, context } }
implement { name = "lpdf_addtocatalog",          arguments = two_strings, actions = lpdf.addtocatalog }
implement { name = "lpdf_addtoinfo",             arguments = two_strings, actions = lpdf.addtoinfo }
implement { name = "lpdf_addtonames",            arguments = two_strings, actions = lpdf.addtonames }
implement { name = "lpdf_addpageattributes",     arguments = two_strings, actions = lpdf.addtopageattributes }
implement { name = "lpdf_addpagesattributes",    arguments = two_strings, actions = lpdf.addtopagesattributes }
implement { name = "lpdf_addpageresources",      arguments = two_strings, actions = lpdf.addtopageresources }
implement { name = "lpdf_adddocumentextgstate",  arguments = two_strings, actions = function(a,b) lpdf.adddocumentextgstate (a,lpdfverbose(b)) end }
implement { name = "lpdf_adddocumentcolorspace", arguments = two_strings, actions = function(a,b) lpdf.adddocumentcolorspace(a,lpdfverbose(b)) end }
implement { name = "lpdf_adddocumentpattern",    arguments = two_strings, actions = function(a,b) lpdf.adddocumentpattern   (a,lpdfverbose(b)) end }
implement { name = "lpdf_adddocumentshade",      arguments = two_strings, actions = function(a,b) lpdf.adddocumentshade     (a,lpdfverbose(b)) end }

