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"
}

-- This is kind of old but it makes no real sense to upgrade it to for instance
-- using delayed type 3 fonts in order to be lean and mean and cut'n'paste
-- compliant. When this kicks one needs to fix the choice of fonts anyway! So,
-- instead we just keep the method we use but slightly adapted to the backend
-- of lmtx.

local type, next = type, next
local find, lower, gmatch = string.find, string.lower, string.gmatch
local floor = math.floor

local context              = context

local fastcopy             = table.fastcopy
local sortedkeys           = table.sortedkeys
local sortedhash           = table.sortedhash
local contains             = table.contains

local report               = logs.reporter("fonts")
local report_checking      = logs.reporter("fonts","checking")

local allocate             = utilities.storage.allocate

local getmacro             = tokens.getters.macro

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 currentfont          = font.current

local definers             = fonts.definers
local helpers              = fonts.helpers

local addprivate           = helpers.addprivate
local hasprivate           = helpers.hasprivate
local getprivateslot       = helpers.getprivateslot
local getprivatecharornode = helpers.getprivatecharornode

local otffeatures          = fonts.constructors.features.otf
local afmfeatures          = fonts.constructors.features.afm

local registerotffeature   = otffeatures.register
local registerafmfeature   = afmfeatures.register

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

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

local implement            = interfaces.implement

local glyph_code           <const> = nodes.nodecodes.glyph

local hpack_node           = nodes.hpack

local nuts                 = nodes.nuts

local isglyph              = nuts.isglyph
local setchar              = nuts.setchar
local setfont              = nuts.setfont
local getscale             = nuts.getscale
local setscale             = nuts.setscale

local nextglyph            = nuts.traversers.glyph

local remove_node          = nuts.remove
local insertnodeafter      = nuts.insertafter
local insertnodebefore     = nuts.insertbefore
local copy_node            = nuts.copy

local actions = false

-- to tfmdata.properties ?

local function onetimemessage(font,char,message) -- char == false returns table
    local tfmdata = fontdata[font]
    local shared  = tfmdata.shared
    if not shared then
        shared = { }
        tfmdata.shared = shared
    end
    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 sortedkeys(category), category
    end
    local cc = category[char]
    if not cc then
        report_checking("char %C in font %a with id %a: %s",char,tfmdata.properties.fullname,font,message)
        category[char] = 1
    else
        category[char] = cc + 1
    end
end

fonts.loggers.onetimemessage = onetimemessage

local fakes = {
    MissingLowercase   = { width  = .45, height = .55, depth  =  .20 },
    MissingUppercase   = { width  = .65, height = .70, depth  =  .25 },
    MissingMark        = { width  = .15, height = .70, depth  = -.50 },
    MissingPunctuation = { width  = .15, height = .55, depth  =  .20 },
    MissingUnknown     = { width  = .45, height = .20, depth  = 0    },
}

local mapping = allocate {
    lu = { "MissingUppercase",    "darkred"     },
    ll = { "MissingLowercase",    "darkred"     },
    lt = { "MissingUppercase",    "darkred"     },
    lm = { "MissingLowercase",    "darkred"     },
    lo = { "MissingLowercase",    "darkred"     },
    mn = { "MissingMark",         "darkgreen"   },
    mc = { "MissingMark",         "darkgreen"   },
    me = { "MissingMark",         "darkgreen"   },
    nd = { "MissingLowercase",    "darkblue"    },
    nl = { "MissingLowercase",    "darkblue"    },
    no = { "MissingLowercase",    "darkblue"    },
    pc = { "MissingPunctuation",  "darkcyan"    },
    pd = { "MissingPunctuation",  "darkcyan"    },
    ps = { "MissingPunctuation",  "darkcyan"    },
    pe = { "MissingPunctuation",  "darkcyan"    },
    pi = { "MissingPunctuation",  "darkcyan"    },
    pf = { "MissingPunctuation",  "darkcyan"    },
    po = { "MissingPunctuation",  "darkcyan"    },
    sm = { "MissingLowercase",    "darkmagenta" },
    sc = { "MissingLowercase",    "darkyellow"  },
    sk = { "MissingLowercase",    "darkyellow"  },
    so = { "MissingLowercase",    "darkyellow"  },
}

table.setmetatableindex(mapping, { "MissingUnknown", "darkgray" })

checkers.mapping = mapping

-- We provide access by (private) name for tracing purposes. We also need to make
-- sure the dimensions are known at the lua and tex end. For previous variants see
-- the mkiv files or older lmtx files. I decided to just drop the old stuff here.

function checkers.placeholder(font,char,category)
    local category = category or chardata[char].category or "lu" -- todo: unknown
    local fakedata = mapping[category] or mapping.lu
    local tfmdata  = fontdata[font]
    local units    = tfmdata.parameters.units or 1000
    local slant    = (tfmdata.parameters.slant or 0)/65536
    local scale    = units/1000
    local rawdata  = tfmdata.shared and tfmdata.shared.rawdata
    local weight   = (rawdata and rawdata.metadata and rawdata.metadata.pfmweight or 400)/400
--     if slant then
--         slant = 0.2
--     end
    local specification = {
        code      = "MissingGlyph",
        scale     = scale,
        slant     = slant,
        weight    = weight,
        namespace = font,
        shapes    = { { shape = fakedata[1], color = fakedata[2] } },
    }
    fonts.helpers.setmetaglyphs("missing", font, char, specification)
end

function checkers.missing(head)
    local lastfont       = nil
    local characters     = nil
    local found          = nil
    local addplaceholder = checkers.placeholder -- so we can overload
    if actions.replace or actions.decompose then
        for n, char, font in nextglyph, head do
            if font ~= lastfont then
                lastfont   = font
                characters = fontcharacters[font]
            end
            if font > 0 and not characters[char] and is_character[chardata[char].category or "unknown"] then
                if actions.decompose then
                    local c = chardata[char]
                    if c then
                        local s = c.specials
                        if s and (s[1] == "char" or s[1] == "with") then -- with added
                            local l = #s
                            if l > 2 then
                                -- check first
                                local okay = true
                                for i=2,l do
                                    if not characters[s[i]] then
                                        okay = false
                                        break
                                    end
                                end
                                if okay then
                                    -- we work backward
                                    local o = n -- we're not supposed to change n (lua 5.4+)
                                    onetimemessage(font,char,"missing (decomposed)")
                                    setchar(n,s[l])
                                    for i=l-1,2,-1 do
                                        head, o = insertnodebefore(head,o,copy_node(n))
                                        setchar(o,s[i])
                                    end
                                    goto DONE
                                end
                            end
                        end
                    end
                end
                if actions.replace then
                    onetimemessage(font,char,"missing (replaced)")
                    local f, c = addplaceholder(font,char)
                    if f and c then
                        setchar(n, c, f)
                    end
                    goto DONE
                end
                if actions.remove then
                    onetimemessage(font,char,"missing (deleted)")
                    if not found then
                        found = { n }
                    else
                        found[#found+1] = n
                    end
                    goto DONE
                end
                onetimemessage(font,char,"missing (checked)")
              ::DONE::
            end
        end
    end
    if found then
        for i=1,#found do
            head = remove_node(head,found[i],true)
        end
    end
    return head
end

local relevant = {
    "missing (decomposed)",
    "missing (replaced)",
    "missing (deleted)",
    "missing (checked)",
    "missing",
}

local function getmissing(id)
    if id then
        local list = getmissing(currentfont())
        if list then
            local _, list = next(getmissing(currentfont()))
            return list
        else
            return { }
        end
    else
        local t = { }
        for id, d in next, fontdata do
            local shared   = d.shared
            local messages = shared and shared.messages
            if messages then
                local filename = d.properties.filename
                if not filename then
                    filename = tostring(d)
                end
                local tf = t[filename] or { }
                for i=1,#relevant do
                    local tm = messages[relevant[i]]
                    if tm then
                        for k, v in next, tm do
                            tf[k] = (tf[k] or 0) + v
                        end
                    end
                end
                if next(tf) then
                    t[filename] = tf
                end
            end
        end
        local l = { }
        for k, v in next, t do
            l[k] = sortedkeys(v)
        end
        return l, t
    end
end

checkers.getmissing = getmissing

statistics.feedback.register { name = "missing characters", index = 10, category = "problem" }

do

 -- -- This callback is gone:
 --
 -- local reported = true
 --
 -- local function glyph_not_found_callback(font,char)
 --     if font > 0 then
 --         if char > 0 then
 --             onetimemessage(font,char,"missing")
 --         else
 --             -- we have a special case
 --         end
 --     elseif not reported then
 --         report("nullfont is used, maybe no bodyfont is defined")
 --         reported = true
 --     end
 -- end
 --
 -- callbacks.register {
 --     name    = "glyph_not_found",
 --     action  = glyph_not_found_callback,
 --     comment = "report a missing character",
 --     frozen  = true,
 -- }

    local loaded = false

    trackers.register("fonts.missing", function(v)
        if v then
            enableaction("processors","fonts.checkers.missing")
            if v == true then
                actions = { check = true }
            else
                actions = utilities.parsers.settings_to_set(v)
                if not loaded and actions.replace then
                    metapost.simple("simplefun",'loadfile("mp-miss.mpxl");')
                    loaded = true
                end
            end
        else
            disableaction("processors","fonts.checkers.missing")
            actions = false
        end
    end)

    logs.registerfinalactions(function()
        local collected, details = getmissing()
        if next(collected) then
            statistics.feedback.setstate("missing characters",true)
            for filename, list in sortedhash(details) do
                logs.startfilelogging(report,"missing characters",filename)
                for u, v in sortedhash(list) do
                    report("%4i  %U  %c  %s",v,u,u,chardata[u].description)
                end
                logs.stopfilelogging()
            end
            if logs.loggingerrors() then
                for filename, list in sortedhash(details) do
                    logs.starterrorlogging(report,"missing characters",filename)
                    for u, v in sortedhash(list) do
                        report("%4i  %U  %c  %s",v,u,u,chardata[u].description)
                    end
                    logs.stoperrorlogging()
                end
            end
        end
    end)

end

-- for the moment here

local function expandglyph(characters,index,done)
    done = done or { }
    if not done[index] then
        local data = characters[index]
        if data then
            done[index] = true
            local d = fastcopy(data)
            local n = d.next
            if n then
                d.next = expandglyph(characters,n,done)
            end
            local p = d.parts
            if p then
                for i=1,#p do
                    local pi = p[i]
                    pi.glyph = expandglyph(characters,pi.glyph,done)
                end
            end
            return d
        end
    end
end

helpers.expandglyph = expandglyph

-- should not be needed as we add .notdef in the engine

local dummyzero = {
 -- width    = 0,
 -- height   = 0,
 -- depth    = 0,
    commands = { { "special", "" } },
}

local function adddummysymbols(tfmdata)
    local characters = tfmdata.characters
    if not characters[0] then
        characters[0] = dummyzero
    end
 -- if not characters[1] then
 --     characters[1] = dummyzero -- test only
 -- end
end

local dummies_specification = {
    name        = "dummies",
    description = "dummy symbols",
    default     = true,
    manipulators = {
        base = adddummysymbols,
        node = adddummysymbols,
    }
}

registerotffeature(dummies_specification)
registerafmfeature(dummies_specification)

--

local function addvisualspace(tfmdata)
    local spacechar = tfmdata.characters[32]
    if spacechar and not spacechar.commands then
        local w = spacechar.width
        local h = tfmdata.parameters.xheight
     -- local h = tfmdata.parameters.xheight / 4 -- could be "visualspace=large" or so
        local c = {
            width    = w,
            commands = { { "rule", h, w } },
         -- commands = { { "line", w, 5*h, h } },
        }
        local u = addprivate(tfmdata, "visualspace", c)
    end
end

local visualspace_specification = {
    name        = "visualspace",
    description = "visual space",
    default     = true,
    manipulators = {
        base = addvisualspace,
        node = addvisualspace,
    }
}

registerotffeature(visualspace_specification)
registerafmfeature(visualspace_specification)

-- local function adddots(tfmdata,unicode)
--     if not tfmdata.characters[unicode] then
--         local period   = tfmdata.characters[0x2E]
--         local original = period.height
--         local height   = tfmdata.parameters.xheight/2
--         tfmdata.characters[unicode] = {
--             width    = period.width,
--             height   = height + original,
--             unicode  = unicode,
--             commands = { { "offset", 0, height, 0x2E } },
--         }
--    end
-- end
--
-- local function addpunctuationdot(tfmdata)
--     adddots(tfmdata,0x2027) -- hyphenation period : in lucida dejavusans xcharter
--     adddots(tfmdata,0x2E31) -- word separator middle dot
-- end
--
-- local punctuationdot_specification = {
--     name        = "extradots",
--     description = "extra dots",
--     default     = true,
--     manipulators = {
--         base = addpunctuationdot,
--         node = addpunctuationdot,
--     }
-- }
--
-- registerotffeature(punctuationdot_specification)
-- registerafmfeature(punctuationdot_specification)

-- local function fixdot(tfmdata)
--     if tfmdata.characters[0xB7] then
--         local period   = tfmdata.characters[0x2E]
--         local self     = tfmdata.characters[0xB7]
--         local original = period.height
--         local height   = tfmdata.parameters.xheight/2
--         self.advance  = self.width
--         self.width    = period.width
--         self.height   = height + original
--         self.commands = { { "offset", 0, height, 0x2E } }
--    end
-- end

local function fixdot(tfmdata)
    local self   = tfmdata.characters[0xB7]
    local period = tfmdata.characters[0x2E]
    local hyphen = tfmdata.characters[0x2D]
    if self and period and hyphen then
        local swidth  = self.width
        local pwidth  = period.width
        if swidth > pwidth then -- maybe also ht here
            local sheight = self.height
            local hheight = hyphen.height
            self.advance  = swidth
            self.width    = pwidth
            self.commands = { { "offset", (pwidth-swidth)/2, (hheight-sheight)/2, 0xB7 } }
        end
   end
end

local dot_specification = {
    name        = "fixdot",
    description = "fix dot",
    default     = true,
    manipulators = {
        base = fixdot,
        node = fixdot,
    }
}

registerotffeature(dot_specification)
registerafmfeature(dot_specification)

do


    local reference = 88 -- string.byte("X")
    local mapping   = { ss = "sans", rm = "serif", tt = "mono" }
    local order     = {      "sans",      "serif",      "mono" }
    local fallbacks = {       sans = { },  serif = { },  mono = { } }

    local function locate(fallbacks,n,f,c)
        for i=1,#fallbacks do
            local id = fallbacks[i]
            if type(id) == "string" then
                local fid = definers.define { name = id }
                report("using fallback font %!font:name! (id: %i)",fid,fid)
                fallbacks[i] = fid
                id = fid
            end
            if type(id) == "number" then
                local cid = fontcharacters[id]
                if cid[c] then
                    local fc = fontcharacters[f]
                    local sc = (fc[reference].height / cid[reference].height) * (getscale(n) or 1000)
                    report("character %C in font %!font:name! (id: %i) is taken from fallback font %!font:name! (id: %i)",c,f,f,id,id)
                    return { id, sc }
                end
            end
        end
        return false
    end

    local cache = table.setmetatableindex("table")

    -- local c = tex.getmissingcharactervalues()
    --
    -- where: textglyph mathglyph mathkernel

    local function missing_character_callback(where,n,f,c)
        local cached = cache[f]
        local found  = cached[c]
        if found == nil then
            -- we can use fonts.helpers.name(f) but we need the monospace flag anyway so:
            local metadata = fontdata[f].shared
            if metadata then
                metadata = metadata.rawdata
                if metadata then
                    metadata = metadata.metadata
                    if metadata then
                        if metadata.monospaced then
                            found = locate(fallbacks.mono,n,f,c)
                            if found then
                                cached[c] = found
                                goto done
                            end
                        end
                        local fn = lower(metadata.fullname or fonts.helpers.name(f))
                        for i=1,3 do
                            local o = order[i]
                            if find(fn,o) then
                                found = locate(fallbacks[o],n,f,c)
                                if found then
                                    cached[c] = found
                                    goto done
                                end
                            end
                        end
                    end
                end
            end
            -- This getmacro can be called often in e.g. a manual that shows something but has a
            -- limited font (something that I noticed for luametatex manual). Just use a better
            -- font then.
            found = locate(fallbacks[mapping[getmacro("fontstyle")] or "mono"],n,f,c)
            if found then
                cached[c] = found
                goto done
            end
        end
      ::done::
        if found then
            setfont(n,found[1])
            setscale(n,found[2])
        end
        statistics.feedback.setstate("missing characters",true)
    end

    callbacks.register {
        name    = "missing_character",
        action  = missing_character_callback,
        comment = "report details about a missing character",
        frozen  = true,
    }

    function definers.registerfallbackfont(style,list)
        local l = fallbacks[style]
        if l then
            for s in gmatch(list,"[^, ]+") do
                if not contains(l,s) then
                    l[#l+1] = s
                end
            end
        end
    end

    implement {
        name      = "registerfallbackfont",
        public    = true,
        protected = true,
        arguments = { "optional", "optional" },
        actions   = definers.registerfallbackfont,
    }

end
