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

-- cleaned up, e.g. no longer older viewers
-- always kids so no longer explicit main / clone / copy
-- some optimizations removed (will come bakc if needed)

local gmatch, lower, format = string.gmatch, string.lower, string.format

local texsprint, ctxcatcodes = tex.sprint, tex.ctxcatcodes

local variables = interfaces.variables

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

local registeredsymbol = codeinjections.registeredsymbol

local pdfstream     = lpdf.stream
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 pdftoeight    = lpdf.toeight

local pdfimmediateobj = pdf.immediateobj
local pdfreserveobj   = pdf.reserveobj

local submitoutputformat = 0 --  0=unknown 1=HTML 2=FDF 3=XML   => not yet used, needs to be checked

local splitter = lpeg.splitat("=>")

local formats = {
    html = 1, fdf = 2, xml = 3,
}

function codeinjections.setformsmethod(name)
    submitoutputformat = formats[lower(name)] or 3
end

local flag = {
    MultiLine       =     4096, --  13
    NoToggleToOff   =    16384, --  15
    Radio           =    32768, --  16
    PushButton      =    65536, --  17
    PopUp           =   131072, --  18
    Edit            =   262144, --  19
    RadiosInUnison  = 33554432, --  26
    DoNotSpellCheck =  4194304, --  23
    DoNotScroll     =  8388608, --  24
    ReadOnly        =        1, --   1
    Required        =        2, --   2
    NoExport        =        4, --   3
    Password        =     8192, --  14
    Sort            =   524288, --  20
    FileSelect      =  1048576, --  21
}

local plus = {
    Invisible       =        1, --   1
    Hidden          =        2, --   2
    Printable       =        4, --   3
    NoView          =       32, --   6
    ToggleNoView    =      256, --   9
    AutoView        =      256, -- 288 (6+9)
}

-- todo: check what is interfaced

flag.readonly    = flag.ReadOnly
flag.required    = flag.Required
flag.protected   = flag.Password
flag.sorted      = flag.Sort
flag.unavailable = flag.NoExport
flag.nocheck     = flag.DoNotSpellCheck
flag.fixed       = flag.DoNotScroll
flag.file        = flag.FileSelect

plus.hidden      = plus.Hidden
plus.printable   = plus.Printable
plus.auto        = plus.AutoView

-- some day .. lpeg with function or table

local function fieldflag(specification)
    local o, n = specification.options, 0
    if o and o ~= "" then
        for f in gmatch(o,"[^, ]+") do
            n = n + (flag[f] or 0)
        end
    end
    return n
end

local function fieldplus(specification)
    local o, n = specification.options, 0
    if o and o ~= "" then
        for p in gmatch(o,"[^, ]+") do
            n = n + (plus[p] or 0)
        end
    end
    return n
end


local function checked(what)
    if what and what ~= "" then
        local set, bug = jobreferences.identify("",what)
        return not bug and #set > 0 and lpdf.pdfaction(set)
    end
end

local function fieldactions(specification) -- share actions
--~ print(table.serialize(specification))
    local d, a = { }, nil
    a = specification.mousedown         if a and a ~= "" then d.D  = checked(a) end
    a = specification.mouseup           if a and a ~= "" then d.U  = checked(a) end
    a = specification.regionin          if a and a ~= "" then d.E  = checked(a) end -- Enter
    a = specification.regionout         if a and a ~= "" then d.X  = checked(a) end -- eXit
    a = specification.afterkeystroke    if a and a ~= "" then d.K  = checked(a) end
    a = specification.formatresult      if a and a ~= "" then d.F  = checked(a) end
    a = specification.validateresult    if a and a ~= "" then d.V  = checked(a) end
    a = specification.calculatewhatever if a and a ~= "" then d.C  = checked(a) end
    a = specification.focusin           if a and a ~= "" then d.Fo = checked(a) end
    a = specification.focusout          if a and a ~= "" then d.Bl = checked(a) end
 -- a = specification.openpage          if a and a ~= "" then d.PO = checked(a) end
 -- a = specification.closepage         if a and a ~= "" then d.PC = checked(a) end
 -- a = specification.visiblepage       if a and a ~= "" then d.PV = checked(a) end
 -- a = specification.invisiblepage     if a and a ~= "" then d.PI = checked(a) end
    return next(d) and pdfdictionary(d)
end

-- fonts and color

local fontnames = {
    rmtf = "Times-Roman",           rmbf = "Times-Bold",
    rmit = "Times-Italic",          rmsl = "Times-Italic",
    rmbi = "Times-BoldItalic",      rmbs = "Times-BoldItalic",
    sstf = "Helvetica",             ssbf = "Helvetica-Bold",
    ssit = "Helvetica-Oblique",     sssl = "Helvetica-Oblique",
    ssbi = "Helvetica-BoldOblique", ssbs = "Helvetica-BoldOblique",
    tttf = "Courier",               ttbf = "Courier-Bold",
    ttit = "Courier-Oblique",       ttsl = "Courier-Oblique",
    ttbi = "Courier-BoldOblique",   ttbs = "Courier-BoldOblique",
}

local usedfonts = { }

local function fieldsurrounding(specification)
    local tag = (specification.fontstyle or "tt") .. (specification.fontalternative or "tf")
    if not fontnames[tag] then
        tag = "tttf"
    end
    local size = specification.fontsize
    local stream = pdfstream {
        pdfconstant(tag),
        format("%s Tf",(size and (number.dimenfactors.bp * size)) or 12),
    }
    usedfonts[tag] = true
    -- add color to stream: 0 g
    -- move up with "x.y Ts"
    return tostring(stream)
end

local function registerfonts()
    if next(usedfonts) then
        local d = pdfdictionary()
        for tag, _ in next, usedfonts do
            local f = pdfdictionary {
                Type     = pdfconstant("Font"),
                Subtype  = pdfconstant("Type1"), -- todo
                Name     = pdfconstant(tag),
                BaseFont = pdfconstant(fontnames[tag]),
            }
            d[tag] = pdfreference(pdfimmediateobj(tostring(f)))
        end
        return d
    end
end

-- cache

local function fieldattributes(specification)
--~     return pdfarray {
--~     --    BG = -- backgroundcolor
--~     --    BC = -- framecolor
--~     }
    return nil
end

-- symbols

local function fieldappearances(specification)
    -- todo: caching
    local values = specification.values
    local default = specification.default
    if not values then
        -- error
        return
    end
    local v = aux.settings_to_array(values)
    local n, r, d
    if #v == 1 then
        n, r, d = v[1], v[1], v[1]
    elseif #v == 2 then
        n, r, d = v[1], v[1], v[2]
    else
        n, r, d = v[1], v[2], v[3]
    end
    local appearance = pdfdictionary { -- cache this one
        N = registeredsymbol(n), R = registeredsymbol(r), D = registeredsymbol(d),
    }
    return lpdf.sharedobj(tostring(appearance))
end

local function fieldstates(specification,forceyes)
    -- we don't use Opt here (too messy for radio buttons)
    local values, default = specification.values, specification.default
    if not values then
        -- error
        return
    end
    local v = aux.settings_to_array(values)
    local yes, off
    if #v == 1 then
        yes, off = v[1], v[1]
    else
        yes, off = v[1], v[2]
    end
    local yesshown, yesvalue = splitter:match(yes)
    if not (yesshown and yesvalue) then
        yesshown = yes, yes
    end
    yes = aux.settings_to_array(yesshown)
    local offshown, offvalue = splitter:match(off)
    if not (offshown and offvalue) then
        offshown = off, off
    end
    off = aux.settings_to_array(offshown)
    if #yes == 1 then
        yesn, yesr, yesd = yes[1], yes[1], yes[1]
    elseif #yes == 2 then
        yesn, yesr, yesd = yes[1], yes[1], yes[2]
    else
        yesn, yesr, yesd = yes[1], yes[2], yes[3]
    end
    if #off == 1 then
        offn, offr, offd = off[1], off[1], off[1]
    elseif #off == 2 then
        offn, offr, offd = off[1], off[1], off[2]
    else
        offn, offr, offd = off[1], off[2], off[3]
    end
    if not yesvalue then
        yesvalue = yesn
    end
    if not offvalue then
        offvalue = offn
    end
    forceyes = forceyes and "On" -- spec likes Yes more but we've used On for ages now
    if default == yesn then
        default = pdfconstant(forceyes or yesn)
    else
        default = pdfconstant("Off")
    end
    local appearance = pdfdictionary { -- maybe also cache components
        N = pdfdictionary { [forceyes or yesn] = registeredsymbol(yesn), Off = registeredsymbol(offn) },
        R = pdfdictionary { [forceyes or yesr] = registeredsymbol(yesr), Off = registeredsymbol(offr) },
        D = pdfdictionary { [forceyes or yesd] = registeredsymbol(yesd), Off = registeredsymbol(offd) }
    }
    local appearanceref = lpdf.sharedobj(tostring(appearance))
    return appearanceref, default
end

local function fieldoptions(specification)
    local values = specification.values
    local default = specification.default
    if values then
        local v = aux.settings_to_array(values)
        for i=1,#v do
            local vi = v[i]
            local shown, value = splitter:match(vi)
            if shown and value then
                v[i] = pdfarray { pdfunicode(value), shown }
            else
                v[i] = pdfunicode(v[i])
            end
        end
        return pdfarray(v)
    end
end

local function radiodefault(parent,field)
    local default, values = parent.default, parent.values
    if not default or default == "" then
        values = aux.settings_to_array(values)
        default = values[1]
    end
    local name = field.name
    local fieldvalues = aux.settings_to_array(field.values)
    local yes, off = fieldvalues[1], fieldvalues[2] or fieldvalues[1]
    if not default then
        return pdfconstant(yes)
    elseif default == name then
        return pdfconstant(default)
    else
        return pdfconstant("Off")
    end
end

-- layers

local function fieldlayer(specification) -- we can move this in line
    local layer = specification.layer
    return (layer and lpdf.layerreferences[layer]) or nil
end

-- defining

local fields, radios, fieldsets, calculationset = { }, { }, { }, nil

function codeinjections.definefieldset(tag,list)
    fieldsets[tag] = list
end

function codeinjections.getfieldset(tag)
    return fieldsets[tag]
end

local function fieldsetlist(tag)
    if tag then
        local ft = fieldsets[tag]
        if ft then
            local a = pdfarray()
            for name in gmatch(list,"[^, ]+") do
                local f = field[name]
                if f and f.pobj then
                    a[#a+1] = pdfreference(f.pobj)
                end
            end
            return a
        end
    end
end

function codeinjections.setfieldcalculationset(tag)
    calculationset = tag
end

local function predefinesymbols(specification)
    local values = specification.values
    if values then
        local a, b = splitter:match(values)
        codeinjections.presetsymbollist(a or values)
    end
end

function codeinjections.getdefaultfieldvalue(name)
    local f = fields[name]
    if f then
        local values  = f.values
        local default = f.default
        if not default or default == "" then
            local a, b = splitter:match(values)
            values = a or values
            for name in gmatch(list,"[^, ]+") do
                default = name
                break
            end
        end
        if default then
            tex.sprint(ctxcatcodes,default)
        end
    end
end


function codeinjections.definefield(specification)
    local n = specification.name
    local f = fields[n]
    if not f then
        local kind = specification.kind
        if not kind then
            -- name and kind are mandate
        elseif kind == "radio" then
            local values = specification.values
            if values and values ~= "" then
                values = aux.settings_to_array(values)
                for v=1,#values do
                    radios[values[v]] = { parent = n }
                end
                fields[n] = specification
            else
                -- invalid radio specification
            end
        elseif kind == "sub" then
            -- not in main field list !
            local radio = radios[n]
            if radio then
                -- merge specification
                for key, value in next, specification do
                    radio[key] = value
                end
            end
            predefinesymbols(specification)
        else
            fields[n] = specification
            predefinesymbols(specification)
        end
    else
        -- already done
    end
end

function codeinjections.clonefield(specification)
    local p = specification.parent
    local c = specification.children
    if not p or not c then
        -- parent and children are mandate
    else
        for n in gmatch(c,"[^, ]+") do
            local f = fields[n]
            if f and not f.done then
                -- already done
            else
                fields[n] = specification
            end
        end
    end
end

function codeinjections.getfieldgroup(name)
    local f = fields[name]
    if f and f.group then
        texsprint(ctxcatcodes,f.group)
    end
end

--

function codeinjections.doiffieldset(tag)
    commands.testcase(fieldsets[tag])
end

function codeinjections.doiffieldelse(name)
    commands.testcase(fields[name])
end

--

local alignments = {
    flushleft  = 0, right  = 0,
    center     = 1, middle = 1,
    flushright = 2, left   = 2,
}

local function fieldalignment(specification)
    return alignments[specification.align] or 0
end

local function enhance(specification,option)
    local so = specification.options
    if so and so ~= "" then
        specification.options = so .. "," .. option
    else
        specification.options = option
    end
    return specification
end

-- finish

local collected = pdfarray()

function codeinjections.finishfields()
    for name, field in next, fields do
        local kids = field.kids
        if kids then
            pdfimmediateobj(field.kobj,tostring(kids))
        end
        local pobj = field.pobj
    end
    for name, field in next, radios do
        local kids = field.kids
        if kids then
            pdfimmediateobj(field.kobj,tostring(kids))
        end
    end
    if #collected > 0 then
        local acroform = pdfdictionary {
            NeedAppearances = true,
            Fields = pdfreference(pdfimmediateobj(tostring(collected))),
            DR     = pdfdictionary { Font = registerfonts() },
            CO     = fieldsetlist(calculationset),
            DA     = "/tttf 12 Tf 0 g",
        }
        lpdf.addtocatalog("AcroForm",pdfreference(pdfimmediateobj(tostring(acroform))))
    end
    lpdf.finishfields = function() end
end

local pdf_widget = pdfconstant("Widget")
local pdf_tx     = pdfconstant("Tx")
local pdf_ch     = pdfconstant("Ch")
local pdf_btn    = pdfconstant("Btn")
local pdf_yes    = pdfconstant("Yes")
local pdf_p      = pdfconstant("P") -- None Invert Outline Push
local pdf_n      = pdfconstant("N") -- None Invert Outline Push
--
local pdf_no_rect = pdfarray { 0, 0, 0, 0 }

local methods = { }

function codeinjections.typesetfield(name,specification)
    local field = fields[name] or radios[name]
    if not field then
        tex.write("error: " .. name)
        -- unknown field
        return
    end
    local method = methods[field.kind]
    if method then
        method(name,specification)
    end
end

-- can be optional multipass optimization (share objects)

local function save_parent(field,specification,d)
    local kn = pdfreserveobj()
    d.Kids = pdfreference(kn)
    field.kobj = kn
    field.kids = pdfarray()
    local pn = pdfimmediateobj(tostring(d))
    field.pobj = pn
    collected[#collected+1] = pdfreference(pn)
end

local function save_kid(field,specification,d)
    local kn = pdfreserveobj()
    field.kids[#field.kids+1] = pdfreference(kn)
    node.write(nodes.pdfannot(specification.width,specification.height,0,d(),kn))
end

function methods.line(name,specification,extras)
    local field = fields[name]
    local kind = field.kind
    if not field.pobj then
        if extras then
            enhance(specification,extras)
        end
        local text = pdfunicode(specification.default)
        local d = pdfdictionary {
            Subtype  = pdf_widget,
            T        = pdfunicode(specification.title),
            F        = fieldplus(specification),
            Ff       = fieldflag(specification),
            OC       = fieldlayer(specification),
            MK       = fieldsurrounding(specification),
            AA       = fieldactions(specification),
            FT       = pdf_tx,
            Q        = fieldalignment(specification),
            MaxLen   = (specification.length == 0 and 1000) or specification.length,
            DV       = text,
            V        = text,
        }
        save_parent(field,specification,d)
        field.specification = specification
    end
    specification = field.specification or { } -- todo: radio spec
    local d = pdfdictionary {
        Subtype = pdf_widget,
        Parent  = pdfreference(field.pobj),
        F       = fieldplus(specification),
        DA      = fieldattributes(specification),
        OC      = fieldlayer(specification),
        MK      = fieldsurrounding(specification),
        AA      = fieldactions(specification),
        Q       = fieldalignment(specification)
    }
    save_kid(field,specification,d)
end

function methods.text(name,specification)
    methods.line(name,specification,"MultiLine")
end

function methods.choice(name,specification,extras)
    local field = fields[name]
    local kind = field.kind
    local d
    if not field.pobj then
        if extras then
            enhance(specification,extras)
        end
        local d = pdfdictionary {
            Subtype  = pdf_widget,
            T        = pdfunicode(specification.title),
            F        = fieldplus(specification),
            Ff       = fieldflag(specification),
            OC       = fieldlayer(specification),
            AA       = fieldactions(specification),
            FT       = pdf_ch,
            Opt      = fieldoptions(field),
        }
        save_parent(field,specification,d)
        field.specification = specification
    end
    specification = field.specification or { }
    local d = pdfdictionary {
        Subtype = pdf_widget,
        Parent  = pdfreference(field.pobj),
        F       = fieldplus(specification),
        DA      = fieldattributes(specification),
        OC      = fieldlayer(specification),
        AA      = fieldactions(specification),
    }
    save_kid(field,specification,d)
end

function methods.popup(name,specification)
    methods.choice(name,specification,"PopUp")
end
function methods.combo(name,specification)
    methods.choice(name,specification,"PopUp,Edit")
end

-- Probably no default appearance needed for first kid and no javascripts for the
-- parent ... I will look into it when I have to make a complex document.

function methods.check(name,specification)
    -- no /Opt because (1) it's messy - see pdf spec, (2) it discouples kids and
    -- contrary to radio there is no way to associate then
    local field = fields[name]
    local kind = field.kind
    local appearance, default = fieldstates(field,true)
    if not field.pobj then
        local d = pdfdictionary {
            Subtype  = pdf_widget,
            T        = pdfunicode(specification.title),
            F        = fieldplus(specification),
            Ff       = fieldflag(specification),
            OC       = fieldlayer(specification),
            AA       = fieldactions(specification),
            FT       = pdf_btn,
            DV       = default,
            V        = default,
            AS       = default,
            AP       = appearance,
            H        = pdf_n,
        }
        save_parent(field,specification,d)
        field.specification = specification
    end
    specification = field.specification or { } -- todo: radio spec
    local d = pdfdictionary {
        Subtype = pdf_widget,
        Parent  = pdfreference(field.pobj),
        F       = fieldplus(specification),
        DA      = fieldattributes(specification),
        OC      = fieldlayer(specification),
        AA      = fieldactions(specification),
        DV      = default,
        V       = default,
        AS      = default,
        AP      = appearance,
        H       = pdf_n,
    }
    save_kid(field,specification,d)
end

function methods.push(name,specification)
    local field = fields[name]
    local kind = field.kind
    if not field.pobj then
        enhance(specification,"PushButton")
        local d = pdfdictionary {
            Subtype  = pdf_widget,
            T        = pdfunicode(specification.title),
            F        = fieldplus(specification),
            Ff       = fieldflag(specification),
            OC       = fieldlayer(specification),
            AA       = fieldactions(specification),
            FT       = pdf_btn,
            AP       = fieldappearances(field),
            H        = pdf_p,
        }
        save_parent(field,specification,d)
        field.specification = specification
    end
    specification = field.specification or { } -- todo: radio spec
    local d = pdfdictionary {
        Subtype = pdf_widget,
        Parent  = pdfreference(field.pobj),
        F       = fieldplus(specification),
        DA      = fieldattributes(specification),
        OC      = fieldlayer(specification),
        AA      = fieldactions(specification),
        AP      = fieldappearances(field),
        H       = pdf_p,
    }
    save_kid(field,specification,d)
end

function methods.sub(name,specification)
    local field = radios[name]
    if not field then
        return
    end
    local parent = fields[field.parent]
    if not parent then
        return
    end
    if not parent.pobj then
        local specification = parent.specification or { }
        enhance(specification,"Radio,RadiosInUnison")
        local d = pdfdictionary {
            T    = parent.name,
            FT   = pdf_btn,
            Rect = pdf_no_rect,
            F    = fieldplus(specification),
            Ff   = fieldflag(specification),
            H    = pdf_n,
        }
        save_parent(parent,specification,d)
    end
    local appearance = fieldstates(field)
    local default = radiodefault(parent,field)
    local d = pdfdictionary {
        Subtype = pdf_widget,
        Parent  = pdfreference(parent.pobj),
        F       = fieldplus(specification),
        DA      = fieldattributes(specification),
        OC      = fieldlayer(specification),
        AA      = fieldactions(specification),
        DV      = default,
        V       = default,
        AS      = default,
        AP      = appearance,
        H       = pdf_n,
    }
    save_kid(parent,specification,d)
end
