if not modules then modules = { } end modules ['lpdf-sig'] = {
    version   = 1.001,
    optimize  = true,
    comment   = "companion to back-imp-pdf.mkxl",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- experiment and playground

local find, gsub, rep, format = string.find, string.gsub, string.rep, string.format

-- M         = "D:20231118193724Z",
-- Reference = pdfdictionary  {
--     Data            = catalog,
--     TransformMethod = pdfconstant("FieldMDP"),
--     Type            = pdfconstant("SigRef"),
--     TransformParams = pdfdictionary {
--         Action = pdfconstant("Include"),
--         Fields = pdfarray(),
--         Type   = pdfconstant("TransformParams"),
--         V      = 1.2,
--     }
-- },

local function updatesignature(signature,...)
    local updated = gsub(signature,
     -- "/ByteRange %[ 2000000000 2000000000 2000000000 2000000000 %]",
        "/ByteRange %[ .......... .......... .......... .......... %]",
        format("/ByteRange [ %010i %010i %010i %010i ]",...)
    )
    return updated ~= signature and updated or false
end

local function getbyteranges(signature,position,filesize)
    local leftangle  = find(signature,"<0")
    local rightangle = find(signature,"0>")
    local b1 = 0
 -- local n1 = position + leftangle  - 2
    local n1 = position + leftangle  - 1
    local b2 = position + rightangle + 1
    local n2 = filesize - b2
    return { b1, n1, b2, n2 }
end

if lpdf and context then

    local pdfreserveobject = lpdf.reserveobject
    local pdfflushobject   = lpdf.flushobject
    local pdfreference     = lpdf.reference
    local pdfconstant      = lpdf.constant
    local pdfdictionary    = lpdf.dictionary
    local pdfarray         = lpdf.array
    local pdfliteral       = lpdf.literal
    local pdfunicode       = lpdf.unicode

    local status    = {
        signature = false,
        position  = false,
        filename  = false,
        filesize  = false,
    }

    local reference = nil

    function lpdf.registersignature(value)
        if value == "pkcs7" then
            local signature = pdfreserveobject()
            status.signature = signature
         -- reference = pdfreference(pdfflushobject(pdfdictionary {
         --     DigestMethod = pdfconstant("SHA256"),
         -- }))
            return pdfreference(signature)
        end
    end

    function lpdf.preparesignature(flush,f,offset,objects)
        local signature = status.signature
        if signature then
            local d = pdfdictionary {
                ByteRange    = pdfarray { 2000000000, 2000000000, 2000000000, 2000000000 },
                Contents     = pdfliteral(rep("0",4096),true), -- should be enough
                Filter       = pdfconstant("Adobe.PPKLite"),
                SubFilter    = pdfconstant("adbe.pkcs7.detached"),
                Type         = pdfconstant("Sig"),
             -- Reasons      = pdfunicode("just to be sure"),
                Reference    = reference,
            }
            objects[signature] = offset
            local object = signature .. " 0 obj\n" .. tostring(d) .. "\nendobj\n"
            status.reference = signature
            status.position  = offset
            status.signature = object
            flush(f,object)
            offset = offset + #object
        end
        return offset
    end

    function lpdf.finalizesignature(filename,usedsize)
        if not filename then
            return
        end
        local signature = status.signature
        if not signature then
            return
        end
        local signame    = file.replacesuffix(filename,"sig")
        local filesize   = file.size(filename)
        local samesize   = usedsize ~= filesize
        local handle     = samesize and io.open(filename,"r+b")
        local position   = status.position
        status.ranges    = getbyteranges(signature,position,filesize)
        status.signature = signature
        status.length    = #signature
        status.filename  = filename
        status.filesize  = filesize
        table.save(signame, status)
        if handle then
            local updated = updatesignature(signature,b1,n1,b2,n2)
            if updated then
                handle:seek("set",position)
                handle:write(updated)
                handle:close()
                status.updated = true
            end
        end
    end

else

    -- "openssl cms -verify -noverify -cmsout -print -inform DER -in ".. binfile
    -- -binary is needed, otherwise \nopdfcompression output will be real slow

          lpdf    = lpdf or { }
    local report  = logs.reporter("sign pdf")

    local openssl = nil

    local runner = sandbox and sandbox.registerrunner {
        name     = "openssl",
        program  = "openssl",
        template = "cms -sign -binary -passin pass:%password% -in %datafile% -out %signature% -outform der -signer %certificate%",
        reporter = report,
    }

    local function checklibrary(uselibrary)
        if uselibrary and openssl == nil then
            dofile(resolvers.findfile("libs-imp-openssl.lmt","tex"))
            openssl = require("openssl")
            if not openssl then
                openssl = false
            end
        end
        return openssl
    end

    -- Here we use the status file so that we don't need to load the pdf file
    -- which saves time and memory.

    local function getsigdata(pdffile,f)
        if not lpdf.epdf then
            report("epdf support is not loaded")
            return
        end
        local pdf = lpdf.epdf.load(pdffile)
        if not pdf then
            report("file %a can't be loaded",pdffile)
            return
        end
        local widgets = pdf.widgets
        if not widgets then
            lpdf.close(pdf)
            return
        end
        local position = nil
        local length   = nil
        for i=1,#widgets do
            local annotation = widgets[i]
            local parent = annotation.Parent or { }
            local what   = annotation.FT or parent.FT
            if what == "Sig" then
                local obj = annotation.__raw__.V
                if obj and obj[1]== 10 then
                    position, length = lpdf.epdf.objectrange(pdf,obj[3])
                    break
                end
            end
        end
        lpdf.close(pdf)
        if position then
            local p = f:seek()
            f:seek("set",position)
            local signature = f:read(length),
            f:seek("set",p)
            local filesize = file.size(pdffile)
            return {
                position  = position,
                length    = length,
                signature = signature,
                filename  = pdffile,
                filesize  = filesize,
                ranges    = getbyteranges(signature,position,filesize)
            }
        end
    end

    function lpdf.sign(specification)
        local report      = specification.report      or report
        local pdffile     = specification.filename    or ""
        local certificate = specification.certificate or ""
        local password    = specification.password    or ""
        if pdffile == "" or certificate == "" or password == "" then
            report("invalid specification")
            return
        end
        --
        local openssl = checklibrary(specification.uselibrary or false)
        --
        local sigfile = file.replacesuffix(pdffile,"sig")
        local tmpfile = file.replacesuffix(pdffile,"tmp")
        local binfile = file.replacesuffix(pdffile,"bin")
        --
        local f = io.open(pdffile,"r+b")
        if not f then
            report("unable to open %a for reading and writing",pdffile)
            return
        end
        --
        local data = table.load(sigfile)
        if not data then
            data = getsigdata(pdffile,f)
        end
        if not data then
            report("unable to open %a",sigfile)
            return
        end
        local filename  = data.filename
        if filename ~= pdffile then
            report("file mismatch %a",pdffile)
            return
        end
        --
        local signature = data.signature
        local position  = data.position
        local ranges    = data.ranges
        local filesize  = data.filesize
        statistics.starttiming("sign pdf")
        local before = f:read(position)
        local insert = f:seek()
        local middle = f:read(#signature)
        local after  = f:read(filesize) -- no problem if we overshoot
        if not before or not after then
            report("invalid status")
            return
        end
        if middle ~= signature then
            report("no pristine signature")
            return
        end
        --
     -- if not data.updated then
            signature = updatesignature(signature,unpack(ranges)) or signature
     -- end
        --
        middle = string.gsub(signature," <0.-0> ","  ")
        local data = before .. middle .. after -- we can nil before and after
        statistics.starttiming("sign ssh")
        report("signing %a",pdffile)
        local digest = nil
        if openssl then
            local result, message = openssl.sign {
                data     = data,
                certfile = certificate,
                password = password,
            }
            if result then
                digest = message
            else
                report("ssh error: %a",message)
            end
        else
            io.savedata(tmpfile,data)
            runner {
                password    = password,
                datafile    = tmpfile,
                signature   = binfile,
                certificate = certificate,
            }
            statistics.stoptiming("sign ssh")
            digest = io.loaddata(binfile)
            if not digest or digest == "" then
                report("invalid digest from ssh")
                return
            end
            if status.purge then
                os.remove(tmpfile)
                os.remove(binfile)
            end
        end
        if digest then
            digest = string.tohex(digest)
            signature = string.gsub(signature,
                "(/Contents <)([^>]+)(>)",
                function(b,s,e) return b .. digest .. string.rep("0",#s-#digest) .. e end
            )
            f:seek("set",insert)
            f:write(signature)
            f:close()
        end
        if status.purge then
            os.remove(sigfile)
        end
        statistics.stoptiming("sign pdf")
        status.sshtime = statistics.elapsedtime("sign ssh")
        status.pdftime = statistics.elapsedtime("sign pdf")
        report("filesize %i bytes, ssh time %0.3f seconds, total time %0.3f seconds",
            filesize,
            statistics.elapsedtime("sign ssh"),
            statistics.elapsedtime("sign pdf")
        )
        return status
    end

 -- local runner = sandbox and sandbox.registerrunner {
 --     name     = "openssl",
 --     program  = "openssl",
 --     template = "...", -- signature and datafile
 --     reporter = report,
 -- }

    -- Here we load the to be checked pdf file because the status file is not present
    -- and the pdf file can be diferent anyway.

    function lpdf.verify(specification)
        if not lpdf.epdf then
            report("epdf support is not loaded")
            return
        end
        --
        local report      = specification.report or report
        local pdffile     = specification.filename or ""
        local certificate = specification.certificate or ""
        local password    = specification.password or ""
        if pdffile == "" or certificate == "" or password == "" then
            report("invalid specification")
            return
        end
        --
        local openssl = checklibrary(specification.uselibrary or false)
        --
        local pdf = lpdf.epdf.load(pdffile)
        if not pdf then
            report("file %a can't be loaded",pdffile)
            return
        end
        --
        local widgets = pdf.widgets
        if not widgets then
            report("file %a has no signature widgets",pdffile)
            lpdf.close(pdf)
            return
        end
        local signature = nil
        local byterange = nil
        for i=1,#widgets do
            local annotation = widgets[i]
            local parent = annotation.Parent or { }
            local what   = annotation.FT or parent.FT
            if what == "Sig" then
                local value = annotation.V
                if value then
                    signature = string.tobytes(tostring(value.Contents) or "")
                    byterange = value.ByteRange
                end
            end
        end
        lpdf.close(pdf)
        if not signature or signature == "" or not byterange then
            report("file %a has no signature",pdffile)
            return
        end
        --
        local f = io.open(pdffile,"rb")
        if not f then
            report("unable to open %a for reading",pdffile)
            return
        end
        f:seek("set",byterange[1])
        local before = f:read(byterange[2] + 1)
     -- if true then
     --     f:seek("set",byterange[2] + 2)
     --     local existing = f:read(byterange[3] - byterange[2] - 3)
     --     print(#existing,existing)
     -- end
        f:seek("set",byterange[3])
        local after = f:read(byterange[4])
        -- we could check the size
        if not before or not after then
            report("invalid byteranges in %a",pdffile)
            return
        end
        local okay = false
        local data = before .. after
        if openssl then
            okay = openssl.verify {
                data      = data,
                certfile  = certificate,
                password  = password,
                signature = signature,
            }
        else
            report("verifying with the binary is not yet implemented")
         -- local tmpfile = file.replacesuffix(pdffile,"tmp")
         -- local binfile = file.replacesuffix(pdffile,"bin")
         --
         -- io.savedata(binfile,signature)
         -- io.savedata(tmpfile,data)
         -- runner {
         --     datafile  = tmpfile,
         --     signature = binfile,
         -- }
         -- os.remove(tmpfile)
         -- os.remove(binfile)
        end
        if okay then
            report("signature in file %a matches the content",pdffile)
        else
            report("signature in file %a doesn't match the content",pdffile)
        end
    end

end
