if not modules then modules = { } end modules ['lpdf-xmp'] = {
    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",
    comment   = "with help from Peter Rolf",
}

local tostring, type = tostring, type
local format, gsub, match, rep, count = string.format, string.gsub, string.match, string.rep, string.count
local concat = table.concat
local utfchar = utf.char
local md5HEX = md5.HEX
local xmlfillin, xmldelete, xmltext = xml.fillin, xml.delete, xml.text
local osdate, ostime, ostimezone, osuuid = os.date, os.time, os.timezone, os.uuid
local settings_to_array = utilities.parsers.settings_to_array

local trace_xmp  = false  trackers.register("backend.xmp",  function(v) trace_xmp  = v end)
local trace_info = false  trackers.register("backend.info", function(v) trace_info = v end)

local report_xmp  = logs.reporter("backend","xmp")
local report_info = logs.reporter("backend","info")

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

local lpdf                 = lpdf
local pdfdictionary        = lpdf.dictionary
local pdfconstant          = lpdf.constant
local pdfunicode           = lpdf.unicode
local pdfstring            = lpdf.string
local pdfreference         = lpdf.reference
local pdfflushstreamobject = lpdf.flushstreamobject

-- The XMP packet wrapper is kind of fixed, see page 10 of XMPSpecificationsPart1.pdf from
-- XMP-Toolkit-SDK-CC201607.zip. So we hardcode the id.

local xpacket = format ( [[
<?xpacket begin="%s" id="W5M0MpCehiHzreSzNTczkc9d"?>

%%s

<?xpacket end="w"?>]], utfchar(0xFEFF) )

local unknown = { false, false }
local mapping = table.setmetatableindex ( {
    -- user defined keys (pdfx:)
    ["ConTeXt.Jobname"]      = { "context",  "rdf:Description/pdfx:ConTeXt.Jobname" },
    ["ConTeXt.Time"]         = { "date",     "rdf:Description/pdfx:ConTeXt.Time" },
    ["ConTeXt.Url"]          = { "context",  "rdf:Description/pdfx:ConTeXt.Url" },
    ["ConTeXt.Support"]      = { "context",  "rdf:Description/pdfx:ConTeXt.Support" },
    ["ConTeXt.Version"]      = { "context",  "rdf:Description/pdfx:ConTeXt.Version" },
    ["TeX.Support"]          = { "metadata", "rdf:Description/pdfx:TeX.Support" },
    ["LuaTeX.Version"]       = { "metadata", "rdf:Description/pdfx:LuaTeX.Version" },
    ["LuaTeX.Functionality"] = { "metadata", "rdf:Description/pdfx:LuaTeX.Functionality" },
    ["LuaTeX.LuaVersion"]    = { "metadata", "rdf:Description/pdfx:LuaTeX.LuaVersion" },
    ["LuaTeX.Platform"]      = { "metadata", "rdf:Description/pdfx:LuaTeX.Platform" },
    ["ID"]                   = { "id",       "rdf:Description/pdfx:ID" },                         -- has date
    -- Adobe PDF schema
    ["Keywords"]             = { "metadata", "rdf:Description/pdf:Keywords", true },
    ["Producer"]             = { "metadata", "rdf:Description/pdf:Producer", true },
 -- ["Trapped"]              = { "pdf",      "rdf:Description/pdf:Trapped" },                     -- '/False' in /Info, but 'False' in XMP
    -- Dublin Core schema
    ["Format"]               = { "metadata", "rdf:Description/dc:format" },                       -- optional, but nice to have
    -- see xml file for comment:
 -- ["Author"]               = { "metadata", "rdf:Description/dc:creator" },
 -- ["Subject"]              = { "metadata", "rdf:Description/dc:description" },
 -- ["Title"]                = { "metadata", "rdf:Description/dc:title" },
    ["Author"]               = { "metadata", "rdf:Description/dc:creator/rdf:Seq/rdf:li", true },
    ["Subject"]              = { "metadata", "rdf:Description/dc:description/rdf:Alt/rdf:li", true },
    ["Title"]                = { "metadata", "rdf:Description/dc:title/rdf:Alt/rdf:li", true },
    -- XMP Basic schema
    ["CreateDate"]           = { "date",     "rdf:Description/xmp:CreateDate" },
    ["CreationDate"]         = { "date",     "rdf:Description/xmp:CreationDate" },                -- dummy
    ["CreatorTool"]          = { "metadata", "rdf:Description/xmp:CreatorTool" },
 -- ["Creator"]              = { "metadata", "rdf:Description/xmp:CreatorTool" },
    ["MetadataDate"]         = { "date",     "rdf:Description/xmp:MetadataDate" },
    ["ModDate"]              = { "date",     "rdf:Description/xmp:ModDate" },                     -- dummy
    ["ModifyDate"]           = { "date",     "rdf:Description/xmp:ModifyDate" },
    -- XMP Media Management schema
    ["DocumentID"]           = { "id",       "rdf:Description/xmpMM:DocumentID" },                -- uuid
    ["InstanceID"]           = { "id",       "rdf:Description/xmpMM:InstanceID" },                -- uuid
    ["RenditionClass"]       = { "pdf",      "rdf:Description/xmpMM:RenditionClass" },            -- PDF/X-4
    ["VersionID"]            = { "pdf",      "rdf:Description/xmpMM:VersionID" },                 -- PDF/X-4
    -- additional entries
    -- PDF/X
    ["GTS_PDFXVersion"]      = { "pdf",      "rdf:Description/pdfxid:GTS_PDFXVersion" },
    -- optional entries
    -- all what is visible in the 'document properties --> additional metadata' window
    -- XMP Rights Management schema (optional)
    ["Marked"]               = { "pdf",      "rdf:Description/xmpRights:Marked" },
 -- ["Owner"]                = { "metadata", "rdf:Description/xmpRights:Owner/rdf:Bag/rdf:li" }, -- maybe useful (not visible)
 -- ["UsageTerms"]           = { "metadata", "rdf:Description/xmpRights:UsageTerms" },           -- maybe useful (not visible)
 -- ["WebStatement"]         = { "metadata", "rdf:Description/xmpRights:WebStatement" },
    -- Photoshop PDF schema (optional)
 -- ["AuthorsPosition"]      = { "metadata", "rdf:Description/photoshop:AuthorsPosition" },
 -- ["Copyright"]            = { "metadata", "rdf:Description/photoshop:Copyright" },
 -- ["CaptionWriter"]        = { "metadata", "rdf:Description/photoshop:CaptionWriter" },
    --
    ["Placeholder1"]          = { "metadata", "pdfaid-placeholder",  true },
    ["Placeholder2"]          = { "metadata", "pdfuaid-placeholder", true },
    ["Placeholder3"]          = { "metadata", "wtpdf-placeholder",   true },
    ["Placeholder4"]          = { "metadata", "user-placeholder",    true },
}, function() return unknown end )


local metadata         = nil
local trailerid        = true
local creationdate     = false
local modificationdate = false

local function pdftimestamp(str)
    local t = type(str)
    if t == "string" then
        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)
    else
        return osdate("D:%Y%m%d%H%M%S",t == "number" and str or ostime()) -- maybe "!D..." : universal time
    end
end

local function pdfgetmetadata()
    if not metadata then
        local contextversion      = environment.version
        local luatexversion       = LUATEXVERBOSE
        local luatexfunctionality = tostring(LUATEXFUNCTIONALITY)
        local jobname             = environment.jobname or tex.jobname or "unknown"
        local documentid          = trailerid and ("uuid:" .. osuuid()) or "no unique document id here"
        local instanceid          = trailerid and ("uuid:" .. osuuid()) or "no unique instance id here"
        local producer            = "LuaMetaTeX"
        local creator             = format("LuaMetaTeX %s %s + ConTeXt LMTX %s",luatexversion,luatexfunctionality,contextversion)
        metadata = creationdate and {
            producer            = producer,
            creator             = creator,
            id                  = format("%s | %s",jobname,creationdate),
            documentid          = documentid,
            instanceid          = instanceid,
            jobname             = jobname,
            --
            luatexversion       = luatexversion,
            contextversion      = contextversion,
            luatexfunctionality = luatexfunctionality,
            luaversion          = tostring(LUAVERSION),
            platform            = os.platform,
            creationdate        = creationdate,
            modificationdate    = modificationdate,
        } or {
            producer            = producer,
            creator             = creator,
            id                  = jobname,
            documentid          = documentid,
            instanceid          = instanceid,
            jobname             = jobname,
        }
     -- inspect(metadata)
    end
    return metadata
end

local function pdfsetmetadate(n,both)
    if n then
        n = converters.totime(n)
        if n then
            creationdate = osdate("%Y-%m-%dT%H:%M:%S",ostime(n)) .. ostimezone()
            if both then
                modificationdate = creationdate
            end
        end
    end
    return creationdate
end

lpdf.pdftimestamp = pdftimestamp

function lpdf.gettrailerid()
    if trailerid == true then
        return md5.HEX(osuuid())
    elseif type(trailerid) == "string" then
        return md5.HEX(trailerid)
    else
        return false
    end
end

-- string: use that, true: uuid, false: nothing

directives.register("backend.trailerid", function(v)
    trailerid = type(v) and v or toboolean(v)
end)

-- year-mm-dd : use that for creation and modification

local function setdates(v)
    local t = type(v)
    if t == "number" or t == "string" then
        local d = converters.totime(v)
        if d then
            report_info("forced date/time information %a will be used",pdfsetmetadate(d,true))
            return
        end
    end
    if toboolean(v) then
        creationdate     = osdate("%Y-%m-%dT%H:%M:%S") .. ostimezone()
        modificationdate = creationdate
    else
        creationdate     = false
        modificationdate = false
    end
end

setdates(true)

directives.register("backend.date", setdates)

-- maybe some day we will load the xmp file at runtime

local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml"

local function setxmpfile(name)
    if xmp then
        report_xmp("discarding loaded file %a",xmpfile)
        xmp = nil
    end
    xmpfile = name ~= "" and name
end

codeinjections.setxmpfile = setxmpfile

interfaces.implement {
    name      = "setxmpfile",
    arguments = "string",
    actions   = setxmpfile
}

local function valid_xmp()
    if not xmp then
     -- local xmpfile = xmpfile or resolvers.findfile(xmpname) or ""
        if xmpfile and xmpfile ~= "" then
            xmpfile = resolvers.findfile(xmpfile) or ""
        end
        if not xmpfile or xmpfile == "" then
            xmpfile = resolvers.findfile(xmpname) or ""
        end
        if xmpfile ~= "" then
            report_xmp("using file %a",xmpfile)
        end
        local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or ""
        xmp = xml.convert(xmpdata, { strip_cm_and_dt = true })
    end
    return xmp
end

function lpdf.addxmpinfo(tag,value,check)
    local pattern = mapping[tag][2]
    if type(pattern) == "string" then
        if not xmp then
            xmp = valid_xmp()
        end
        if xmp and value then
            xmlfillin(xmp,pattern,value,check)
        end
    end
end

-- redefined

local pdfaddtoinfo  = lpdf.addtoinfo
local pdfaddxmpinfo = lpdf.addxmpinfo

function lpdf.addtoinfo(tag,pdfvalue,strvalue)
    local pattern = mapping[tag][2]
    if pattern or strvalue == true then
        pdfaddtoinfo(tag,pdfvalue)
    end
    if type(pattern) == "string" then
        local value = (type(strvalue) == "string" and strvalue) or gsub(tostring(pdfvalue),"^%((.*)%)$","%1") -- hack
        if trace_info then
            report_info("set %a to %a",tag,value)
        end
        xmlfillin(xmp or valid_xmp(),pattern,value,check)
    end
end

local pdfaddtoinfo = lpdf.addtoinfo -- used later

-- for the do-it-yourselvers

function lpdf.insertxmpinfo(pattern,whatever,prepend)
    xml.insert(xmp or valid_xmp(),pattern,whatever,prepend)
end

function lpdf.injectxmpinfo(pattern,whatever,prepend)
    xml.inject(xmp or valid_xmp(),pattern,whatever,prepend)
end

function lpdf.replacexmpinfo(pattern,whatever)
    local xmp = xmp or valid_xmp()
    if whatever == "" then
        xml.delete(xmp,pattern)
    else
        xml.replace(xmp,pattern,whatever)
    end
end

-- flushing

local add_xmp_blob   = true
--   indentity_done = false  -- using "setupidentity = function() end" fails as the meaning is frozen in register

local checkidentity

checkidentity = function(metadata)
    local identity    = interactions.general.getidentity()
    metadata.title    = identity.title
    metadata.subtitle = identity.subtitle
    metadata.author   = identity.author
    metadata.date     = identity.date
    metadata.keywords = identity.keywords
    checkidentity     = false
end

local function setupidentity()
 -- if not identity_done then
        --
        local metadata = pdfgetmetadata()

        if checkidentity then
            checkidentity(metadata)
        end

        local title    = metadata.title
        local subtitle = metadata.subtitle
        local author   = metadata.author
        local date     = metadata.date
        local keywords = metadata.keywords
        --
        if date and date ~= "" then
            pdfsetmetadate(date)
        end
        if keywords then
            keywords = concat(settings_to_array(keywords), " ")
        end
        --
        local creator        = metadata.creator
        local contextversion = metadata.contextversion
        local id             = metadata.id
        local jobname        = metadata.jobname
        local creation       = metadata.creationdate
        local modification   = metadata.modificationdate
        --
        if creator then
            pdfaddtoinfo("Creator",pdfunicode(creator),creator)
        end
        if creation then
            pdfaddtoinfo("CreationDate",pdfstring(pdftimestamp(creation)),creation)
        end
        if modification then
            pdfaddtoinfo("ModDate",pdfstring(pdftimestamp(modification)),modification)
        end
        if id then
            pdfaddtoinfo("ID",pdfstring(id),id) -- needed for pdf/x
        end
        --
        if title and title ~= "" then
            pdfaddtoinfo("Title",pdfunicode(title),title)
        end
        if subtitle and subtitle ~= "" then
            pdfaddtoinfo("Subject",pdfunicode(subtitle),subtitle)
        end
        if author and author ~= "" then
            pdfaddtoinfo("Author",pdfunicode(author),author)
        end
        if keywords and keywords ~= "" then
            pdfaddtoinfo("Keywords",pdfunicode(keywords),keywords)
        end
        --
        if contextversion then
            pdfaddtoinfo("ConTeXt.Version",contextversion)
        end
        if creation then
            pdfaddtoinfo("ConTeXt.Time",creation)
        end
        if jobname then
            pdfaddtoinfo("ConTeXt.Jobname",jobname)
        end
        --
     -- pdfaddtoinfo("ConTeXt.Url","www.pragma-ade.com")
        pdfaddtoinfo("ConTeXt.Url","github.com/contextgarden/context")
        pdfaddtoinfo("ConTeXt.Support","contextgarden.net")
        pdfaddtoinfo("TeX.Support","tug.org")
        --
 --     identity_done = true
 -- else
 --     -- no need for a message
 -- end
    return metadata
end

local pdfaddusermetadata  do

    local usermetadata   = { }
    local usermetaprefix = "Private"

    interfaces.implement {
        name      = "setupusermetadata",
        arguments = "hash",
        public    = true,
        protected = true,
        actions   = function(t)
                        table.merge(usermetadata,t)
                    end,
    }

    pdfaddusermetadata = function()
        if next(usermetadata) then
            local t = { }
            t[#t+1] = '<rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/">'
            for k, v in table.sortedhash(usermetadata) do
                t[#t+1] = string.formatters["      <pdfx:%s.%s>%s</pdfx:%1Z.%2Z>"](usermetaprefix,k,v)
            end
            t[#t+1] = '    </rdf:Description>'
            lpdf.replacexmpinfo("user-placeholder",concat(t,"\n"))
        end
    end

end

local function flushxmpinfo()
    commands.pushrandomseed()
    commands.setrandomseed(ostime())

    local metadata = setupidentity()            -- tod: merge into here and save code
 -- local metadata     = pdfgetmetadata()
 -- if checkidentity then
 --     checkidentity(metadata)
 -- end
    local creation     = metadata.time or metadata.creationdate     or creationdate
    local modification = metadata.time or metadata.modificationdate or modificationdate or creation
    local producer     = metadata.producer
    local creator      = metadata.creator
    local documentid   = metadata.documentid
    local instanceid   = metadata.instanceid

    pdfaddtoinfo("Producer",producer)
    pdfaddtoinfo("Creator",creator)
    pdfaddtoinfo("CreationDate",creation)
    pdfaddtoinfo("ModDate",modification)

    if add_xmp_blob then

        if not xmp then xmp = valid_xmp() end

        pdfaddxmpinfo("DocumentID",documentid)
        pdfaddxmpinfo("InstanceID",instanceid)
        pdfaddxmpinfo("Producer",producer)
        pdfaddxmpinfo("CreatorTool",creator)
        pdfaddxmpinfo("CreateDate",creation)
        pdfaddxmpinfo("ModifyDate",modification)
        pdfaddxmpinfo("MetadataDate",creation)
        pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion)
        pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality)
        pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion)
        pdfaddxmpinfo("LuaTeX.Platform",metadata.platform)

        local title    = metadata.title
        local subtitle = metadata.subtitle
        local author   = metadata.author
        local keywords = metadata.keywords

        -- We need to wipe some fields in the xml because otherwise validators
        -- complain ... they don't see an empty (nonexistent, default) info field
        -- as being the same as an empty element.

        if title and title ~= "" then
            pdfaddxmpinfo("Title",pdfunicode(title),title)
        end
        if subtitle and subtitle ~= "" then
            pdfaddxmpinfo("Subject",pdfunicode(subtitle),subtitle)
        end
        if author and author ~= "" then
            pdfaddxmpinfo("Author",pdfunicode(author),author)
        end
        if keywords and keywords ~= "" then
            pdfaddxmpinfo("Keywords",pdfunicode(keywords),keywords)
        end

        pdfaddusermetadata()

        -- checks for empty:

        for tag, map in next, mapping do
            if map[3] == true then
                local pattern = map[2]
                if type(pattern) == "string" and xmltext(xmp,pattern) == "" then
                    xmldelete(xmp,pattern .. rep("/..",count(pattern,"/")-1))
                end
            end
        end

        local blob = xml.tostring(xml.first(xmp,"/x:xmpmeta"))
        local md = pdfdictionary {
            Subtype = pdfconstant("XML"),
            Type    = pdfconstant("Metadata"),
        }
        if trace_xmp then
            report_xmp("data flushed, see log file")
            logs.pushtarget("logfile")
            report_xmp("start xmp blob")
            logs.newline()
            logs.writer(blob)
            logs.newline()
            report_xmp("stop xmp blob")
            logs.poptarget()
        end
        blob = format(xpacket,blob)
        if not verbose and lpdf.compresslevel() > 0 then
            blob = gsub(blob,">%s+<","><")
        else
            -- todo: lpeg
            while true do
                local b = gsub(blob,"\n +\n( +<)","\n%1")
                if b == blob then
                    break
                else
                    blob = b
                end
            end
        end
        local r = pdfflushstreamobject(blob,md,false) -- uncompressed
        lpdf.addtocatalog("Metadata",pdfreference(r))

    end

    commands.poprandomseed() -- hack
end

-- lpdf.registerpagefinalizer(setupidentity,"identity")
lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata")

directives.register("backend.xmp",        function(v) add_xmp_blob = v end)
directives.register("backend.verbosexmp", function(v) verbose = v end)
