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

-- possible optimization: delayed initialization of vectors
-- move to the nodes namespace

local format             = string.format
local bpfactor           = number.dimenfactors.bp

local report_fonts       = logs.reporter("fonts","checking")

local fonts              = fonts

fonts.checkers           = fonts.checkers or { }
local checkers           = fonts.checkers

local fonthashes         = fonts.hashes
local fontdata           = fonthashes.identifiers
local fontcharacters     = fonthashes.characters

local addprivate         = fonts.helpers.addprivate
local hasprivate         = fonts.helpers.hasprivate
local getprivatenode     = fonts.helpers.getprivatenode

local otffeatures        = fonts.constructors.newfeatures("otf")
local registerotffeature = otffeatures.register

local is_character       = characters.is_character
local chardata           = characters.data

local tasks              = nodes.tasks
local enableaction       = tasks.enableaction
local disableaction      = tasks.disableaction

local glyph_code         = nodes.nodecodes.glyph
local traverse_id        = node.traverse_id
local remove_node        = nodes.remove
local insert_node_after  = node.insert_after

-- maybe in fonts namespace
-- deletion can be option

local action = false

-- to tfmdata.properties ?

local function onetimemessage(font,char,message) -- char == false returns table
    local tfmdata = fontdata[font]
    local shared = tfmdata.shared
    local messages = shared.messages
    if not messages then
        messages = { }
        shared.messages = messages
    end
    local category = messages[message]
    if not category then
        category = { }
        messages[message] = category
    end
    if char == false then
        return table.sortedkeys(category)
    elseif not category[char] then
        report_fonts("char %U in font %a with id %a: %s",char,tfmdata.properties.fullname,font,message)
        category[char] = true
    end
end

fonts.loggers.onetimemessage = onetimemessage

local mapping = { -- this is just an experiment to illustrate some principles elsewhere
    lu = "placeholder uppercase red",
    ll = "placeholder lowercase red",
    lt = "placeholder uppercase red",
    lm = "placeholder lowercase red",
    lo = "placeholder lowercase red",
    mn = "placeholder mark green",
    mc = "placeholder mark green",
    me = "placeholder mark green",
    nd = "placeholder lowercase blue",
    nl = "placeholder lowercase blue",
    no = "placeholder lowercase blue",
    pc = "placeholder punctuation cyan",
    pd = "placeholder punctuation cyan",
    ps = "placeholder punctuation cyan",
    pe = "placeholder punctuation cyan",
    pi = "placeholder punctuation cyan",
    pf = "placeholder punctuation cyan",
    po = "placeholder punctuation cyan",
    sm = "placeholder lowercase magenta",
    sc = "placeholder lowercase yellow",
    sk = "placeholder lowercase yellow",
    so = "placeholder lowercase yellow",
}

table.setmetatableindex(mapping,function(t,k) v = "placeholder unknown gray" t[k] = v return v end)

local fakes = {
    {
        name   = "lowercase",
        code   = ".025 -.175 m .425 -.175 l .425 .525 l .025 .525 l .025 -.175 l .025 0 l .425 0 l .025 -.175 m h S",
        width  = .45,
        height = .55,
        depth  = .20,
    },
    {
        name   = "uppercase",
        code   = ".025 -.225 m .625 -.225 l .625 .675 l .025 .675 l .025 -.225 l .025 0 l .625 0 l .025 -.225 m h S",
        width  = .65,
        height = .70,
        depth  = .25,
    },
    {
        name   = "mark",
        code   = ".025  .475 m .125  .475 l .125 .675 l .025 .675 l .025  .475 l h B",
        width  = .15,
        height = .70,
        depth  = -.50,
    },
    {
        name   = "punctuation",
        code   = ".025 -.175 m .125 -.175 l .125 .525 l .025 .525 l .025 -.175 l h B",
        width  = .15,
        height = .55,
        depth  = .20,
    },
    {
        name   = "unknown",
        code   = ".025 0 m .425 0 l .425 .175 l .025 .175 l .025 0 l h B",
        width  = .45,
        height = .20,
        depth  = 0,
    },
}

local variants = {
    { tag = "gray",    r = .6, g = .6, b = .6 },
    { tag = "red",     r = .6, g =  0, b =  0 },
    { tag = "green",   r =  0, g = .6, b =  0 },
    { tag = "blue",    r =  0, g =  0, b = .6 },
    { tag = "cyan",    r =  0, g = .6, b = .6 },
    { tag = "magenta", r = .6, g =  0, b = .6 },
    { tag = "yellow",  r = .6, g = .6, b =  0 },
}

local package = "q %0.6f 0 0 %0.6f 0 0 cm %s %s %s rg %s %s %s RG 10 M 1 j 1 J 0.05 w %s Q"

local cache = { } -- saves some tables but not that impressive

local function addmissingsymbols(tfmdata) -- we can have an alternative with rules
    local characters = tfmdata.characters
    local size       = tfmdata.parameters.size
    local privates   = tfmdata.properties.privates
    local scale      = size * bpfactor
    for i=1,#variants do
        local v = variants[i]
        local tag, r, g, b = v.tag, v.r, v.g, v.b
        for i =1, #fakes do
            local fake = fakes[i]
            local name = fake.name
            local privatename = format("placeholder %s %s",name,tag)
            if not hasprivate(tfmdata,privatename) then
                local hash = format("%s_%s_%s_%s_%s_%s",name,tag,r,g,b,size)
                local char = cache[hash]
                if not char then
                    char = {
                        width    = size*fake.width,
                        height   = size*fake.height,
                        depth    = size*fake.depth,
                        -- bah .. low level pdf ... should be a rule or plugged in
                        commands = { { "special", "pdf: " .. format(package,scale,scale,r,g,b,r,g,b,fake.code) } }
                    }
                    cache[hash] = char
                end
                addprivate(tfmdata, privatename, char)
            end
        end
    end
end

registerotffeature {
    name        = "missing",
    description = "missing symbols",
    manipulators = {
        base = addmissingsymbols,
        node = addmissingsymbols,
    }
}

fonts.loggers.add_placeholders        = function(id) addmissingsymbols(fontdata[id or true]) end
fonts.loggers.category_to_placeholder = mapping

function commands.getplaceholderchar(name)
    local id = font.current()
    addmissingsymbols(fontdata[id])
    context(fonts.helpers.getprivatenode(fontdata[id],name))
end

function checkers.missing(head)
    local lastfont, characters, found = nil, nil, nil
    for n in traverse_id(glyph_code,head) do -- faster than while loop so we delay removal
        local font = n.font
        local char = n.char
        if font ~= lastfont then
            characters = fontcharacters[font]
        end
        if not characters[char] and is_character[chardata[char].category] then
            if action == "remove" then
                onetimemessage(font,char,"missing (will be deleted)")
            elseif action == "replace" then
                onetimemessage(font,char,"missing (will be flagged)")
            else
                onetimemessage(font,char,"missing")
            end
            if not found then
                found = { n }
            else
                found[#found+1] = n
            end
        end
    end
    if not found then
        -- all well
    elseif action == "remove" then
        for i=1,#found do
            head = remove_node(head,found[i],true)
        end
    elseif action == "replace" then
        for i=1,#found do
            local n = found[i]
            local font = n.font
            local char = n.char
            local tfmdata = fontdata[font]
            local properties = tfmdata.properties
            local privates = properties.privates
            local category = chardata[char].category
            local fakechar = mapping[category]
            local p = privates and privates[fakechar]
            if not p then
                addmissingsymbols(tfmdata)
                p = properties.privates[fakechar]
            end
            if properties.lateprivates then -- .frozen
                -- bad, we don't have them at the tex end
                local fake = getprivatenode(tfmdata,fakechar)
                insert_node_after(head,n,fake)
                head = remove_node(head,n,true)
            else
                -- good, we have \definefontfeature[default][default][missing=yes]
                n.char = p
            end
        end
    else
        -- maye write a report to the log
    end
    return head, false
end

local relevant = { "missing (will be deleted)", "missing (will be flagged)", "missing" }

function checkers.getmissing(id)
    if id then
        local list = checkers.getmissing(font.current())
        if list then
            local _, list = next(checkers.getmissing(font.current()))
            return list
        else
            return { }
        end
    else
        local t = { }
        for id, d in next, fontdata do
            local shared = d.shared
            local messages = shared.messages
            if messages then
                local tf = t[d.properties.filename] or { }
                for i=1,#relevant do
                    local tm = messages[relevant[i]]
                    if tm then
                        tf = table.merged(tf,tm)
                    end
                end
                if next(tf) then
                    t[d.properties.filename] = tf
                end
            end
        end
        for k, v in next, t do
            t[k] = table.sortedkeys(v)
        end
        return t
    end
end

local tracked = false

trackers.register("fonts.missing", function(v)
    if v then
        enableaction("processors","fonts.checkers.missing")
        tracked = true
    else
        disableaction("processors","fonts.checkers.missing")
    end
    if v == "replace" then
        otffeatures.defaults.missing = true
    end
    action = v
end)

function commands.checkcharactersinfont()
    enableaction("processors","fonts.checkers.missing")
    tracked = true
end

function commands.removemissingcharacters()
    enableaction("processors","fonts.checkers.missing")
    action = "remove"
    tracked = true
end

function commands.replacemissingcharacters()
    enableaction("processors","fonts.checkers.missing")
    action = "replace"
    otffeatures.defaults.missing = true
    tracked = true
end

local report_characters = logs.reporter("fonts","characters")
local report_character  = logs.reporter("missing")

local logsnewline       = logs.newline
local logspushtarget    = logs.pushtarget
local logspoptarget     = logs.poptarget

luatex.registerstopactions(function()
    if tracked then
        local collected = checkers.getmissing()
        if next(collected) then
            logspushtarget("logfile")
            for filename, list in table.sortedhash(collected) do
                logsnewline()
                report_characters("start missing characters: %s",filename)
                logsnewline()
                for i=1,#list do
                    local u = list[i]
                    report_character("%U  %c  %s",u,u,chardata[u].description)
                end
                logsnewline()
                report_characters("stop missing characters")
                logsnewline()
            end
            logspoptarget()
        end
    end
end)
