if not modules then modules = { } end modules ['font-syn'] = {
    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"
}

-------------------------------------------------------------------------------
-- Define a new private environment into which to load "font-syn.lua".

-- In 5.3, setting this means 'free' variables get translated to 
--    <new value>.<name>
-- This means, I think, that both assignments and references use <new value>.
-- But luaotfload.fontloader has a metatable with __index where __index is a 
--    reference to _G.
local _ENV = table.copy(luaotfload.fontloader)

-- Define some functions required by "font-syn.lua".
local split = "^(.-)([^/]-)([^/]-)$"

---@function dowithfilesintree() -- {{{
local function dowithfilesintree(pattern, handle)
  local files = luaotfload.aux.font_index().files.full
  local match = string.match
  for i = 1, #files do
    local filename = files[i]
    if match(filename, pattern) then
      local root, path, name = match(filename, split)
      -- Path is always empty.
      handle("file", root, path, name)
    end
  end
end
-- }}}

---@function setmetatableindex() -- {{{
local function setmetatableindex(t, k)
  if k == "self" then
    return table.setmetatableindex(t, function(tt, kk)
      tt[kk] = kk
      return kk
    end)
  else
    return table.setmetatableindex(t, k)
  end
end
-- }}}

-- Define some dummy functions.
-- Todo: replace or exterminate.
local function datastate()     return {}  end

-------------------------------------------------------------------------------

local next, tonumber = next, tonumber
local isfile, modificationtime = lfs.isfile, lfs.modification
local P, Cs, lpegmatch = lpeg.P, lpeg.Cs, lpeg.match
local sub, gsub, find, lower, upper, formatters = string.sub, string.gsub, 
    string.find, string.lower, string.upper, string.formatters
local sort, fastcopy, serialize, sortedhash = 
    table.sort, table.fastcopy, table.serialize, table.sortedhash

local allocate             = utilities.storage.allocate

local removesuffix         = file.removesuffix
local splitbase            = file.splitbase
local splitname            = file.splitname
local basename             = file.basename
local pathpart             = file.pathpart
local filejoin             = file.join
local is_qualified_path    = file.is_qualified_path
local exists               = io.exists

local findfile             = resolvers.findfile
local resolveprefix        = resolvers.resolve

local report_names         = logs.reporter("fonts","names")

-- This module implements a name to filename resolver. Names are resolved using a
-- table that has keys filtered from the font related files.

fonts                      = fonts or { } -- also used elsewhere

fonts.names                = fonts.names or allocate { }
local names                = {}

local filters              = names.filters or { }
names.filters              = filters

local treatments           = fonts.treatments or { }
fonts.treatments           = treatments

names.data                 = names.data or allocate { }

names.version              = 1.131
names.basename             = "names"
names.saved                = false
names.loaded               = false
names.be_clever            = true
names.enabled              = true
names.cache                = containers.define("fonts","data",names.version,true)

local usesystemfonts       = true
local autoreload           = true

-------------------------------------------------------------------------------

-- -- what to do with these -- --
--
-- thin -> thin
--
-- regu -> regular  -> normal
-- norm -> normal   -> normal
-- stan -> standard -> normal
-- medi -> medium
-- ultr -> ultra
-- ligh -> light
-- heav -> heavy
-- blac -> black
-- thin
-- book
-- verylight
--
-- buch        -> book
-- buchschrift -> book
-- halb        -> demi
-- halbfett    -> demi
-- mitt        -> medium
-- mittel      -> medium
-- fett        -> bold
-- mage        -> light
-- mager       -> light
-- nord        -> normal
-- gras        -> normal

local weights = Cs ( -- not extra
    P("demibold")
  + P("semibold")
  + P("mediumbold")
  + P("ultrabold")
  + P("extrabold")
  + P("ultralight")
  + P("extralight")
  + P("bold")
  + P("demi")  -- / "semibold"
  + P("semi")  -- / "semibold"
  + P("light")
  + P("medium")
  + P("heavy")
  + P("ultra")
  + P("black")
--+ P("bol")      / "bold" -- blocks
  + P("bol")
  + P("regular")  / "normal"
)

-- local weights = {
--     [100] = "thin",
--     [200] = "extralight",
--     [300] = "light",
--     [400] = "normal",
--     [500] = "medium",
--     [600] = "semibold", -- demi demibold
--     [700] = "bold",
--     [800] = "extrabold",
--     [900] = "black",
-- }

local styles = Cs (
    P("reverseoblique") / "reverseitalic"
  + P("regular")        / "normal"
  + P("italic")
  + P("oblique")        / "italic"  -- cfr: "slanted"??
  + P("slanted")
  + P("slant")          / "slanted"
  + P("roman")          / "normal"
  + P("ital")           / "italic" -- might be tricky
  + P("ita")            / "italic" -- might be tricky
--+ P("obli")           / "oblique"
)

local widths = Cs(
    P("condensed")
  + P("thin")
  + P("expanded")
  + P("cond")     / "condensed"
  + P("normal")
  + P("book")     / "normal"  -- cfr: ???
)

local variants = Cs( -- fax casual
    P("smallcaps")
  + P("oldstyle")
  + P("caps")      / "smallcaps"
)

names.knownweights = {  -- cfr: book??
    "black",
    "bold",
    "demi",
    "demibold",
    "extrabold",
    "heavy",
    "light",
    "medium",
    "mediumbold",
    "normal",
    "regular",
    "semi",
    "semibold",
    "ultra",
    "ultrabold",
    "ultralight",
}

names.knownstyles = {
    "italic",
    "normal",
    "oblique",
    "regular",
    "reverseitalic",
    "reverseoblique",
    "roman",
    "slanted",
}

names.knownwidths = {
    "book",
    "condensed",
    "expanded",
    "normal",
    "thin",
}

names.knownvariants = {
    "normal",
    "oldstyle",
    "smallcaps",
}

local remappedweights = {
    [""]    = "normal",
    ["bol"] = "bold",
}

local remappedstyles = {
    [""]    = "normal",
}

local remappedwidths = {
    [""]    = "normal",
}

local remappedvariants = {
    [""]    = "normal",
}

names.remappedweights  = remappedweights   setmetatableindex(remappedweights ,"self")
names.remappedstyles   = remappedstyles    setmetatableindex(remappedstyles  ,"self")
names.remappedwidths   = remappedwidths    setmetatableindex(remappedwidths  ,"self")
names.remappedvariants = remappedvariants  setmetatableindex(remappedvariants,"self")

local any = P(1)

local analyzed_table

local analyzer = Cs (
    (
        weights  / function(s) analyzed_table[1] = s return "" end
      + styles   / function(s) analyzed_table[2] = s return "" end
      + widths   / function(s) analyzed_table[3] = s return "" end
      + variants / function(s) analyzed_table[4] = s return "" end
      + any
    )^0
)

local splitter = lpeg.splitat("-")

function names.splitspec(askedname)
    local name, weight, style, width, variant = lpegmatch(splitter,askedname)
    weight  = weight  and lpegmatch(weights, weight)  or weight
    style   = style   and lpegmatch(styles,  style)   or style
    width   = width   and lpegmatch(widths,  width)   or width
    variant = variant and lpegmatch(variants,variant) or variant
    if not weight or not style or not width or not variant then
        weight, style, width, variant = weight or "normal", style or "normal", width or "normal", variant or "normal"
    end
    return name or askedname, weight, style, width, variant
end

local function analyzespec(somename)
    if somename then
        analyzed_table = { }
        local name = lpegmatch(analyzer,somename)
        return name, analyzed_table[1], analyzed_table[2], analyzed_table[3], analyzed_table[4]
    end
end

-- It would make sense to implement the filters in the related modules, but to keep
-- the overview, we define them here.

filters.afm = fonts.handlers.afm.readers.getinfo
filters.otf = fonts.handlers.otf.readers.getinfo
filters.ttf = filters.otf
filters.ttc = filters.otf

-- The scanner loops over the filters using the information stored in the file
-- databases. Watch how we check not only for the names, but also for combination
-- with the weight of a font.

filters.list = {
    "otf", "ttf", "ttc", "afm", -- no longer dfont support (for now)
}

-- to be considered: loop over paths per list entry (so first all otf ttf etc)

names.fontconfigfile       = "fonts.conf"   -- a bit weird format, bonus feature
names.osfontdirvariable    = "OSFONTDIR"    -- the official way, in minimals etc
names.extrafontsvariable   = "EXTRAFONTS"   -- the official way, in minimals etc
names.runtimefontsvariable = "RUNTIMEFONTS" -- the official way, in minimals etc

filters.paths = { }
filters.names = { }

function names.getpaths() -- {{{
    local hash, result, r = { }, { }, 0
    local function collect(t,where)
        for i=1,#t do
            local v = t[i]
            v = gsub(v,"/+$","") -- not needed any more
            local key = lower(v)
            report_names("variable %a specifies path %a",where,v)
            if not hash[key] then
                r = r + 1
                result[r] = v
                hash[key] = true
            end
        end
    end
    local path = names.osfontdirvariable or ""
    if path ~= "" then
        collect(resolvers.expandedpathlist(path),path)
    end
    local path = names.extrafontsvariable or ""
    if path ~= "" then
        collect(resolvers.expandedpathlist(path),path)
    end
    if xml then
        local confname = resolvers.expansion("FONTCONFIG_FILE") or ""
        if confname == "" then
            confname = names.fontconfigfile or ""
        end
        if confname ~= "" then
            -- first look in the tex tree
            local name = findfile(confname,"fontconfig files") or ""
            if name == "" then
                -- after all, fontconfig is a unix thing
                name = filejoin("/etc",confname)
                if not isfile(name) then
                    name = "" -- force quit
                end
            end
            if name ~= "" and isfile(name) then
                local xmldata = xml.load(name)
                -- begin of untested mess
                xml.include(xmldata,"include","",true,function(incname)
                    if not is_qualified_path(incname) then
                        local path = pathpart(name) -- main name
                        if path ~= "" then
                            incname = filejoin(path,incname)
                        end
                    end
                    if isfile(incname) then
                        return io.loaddata(incname)
                    end
                end)
                -- end of untested mess
                local fontdirs = xml.collect_texts(xmldata,"dir",true)
                collect(fontdirs,"fontconfig file")
            end
        end
    end
    sort(result)
    function names.getpaths()
        return result
    end
    return result
end
-- }}}

local function cleanname(name)
    return (gsub(lower(name),"[^%a%d]",""))
end

local function cleanfilename(fullname,defaultsuffix)
    if fullname then
        local path, name, suffix = splitname(fullname)
        if name then
            name = gsub(lower(name),"[^%a%d]","")
            if suffix and suffix ~= "" then
                return name .. ".".. suffix
            elseif defaultsuffix and defaultsuffix ~= "" then
                return name .. ".".. defaultsuffix
            else
                return name
            end
        end
    end
    return "badfontname"
end

local sorter = function(a,b)
    return a > b -- longest first
end

names.cleanname     = cleanname
names.cleanfilename = cleanfilename

local function check_name(data,result,filename,modification,suffix,subfont)
    -- shortcuts
    local specifications = data.specifications
    -- fetch
    local fullname       = result.fullname
    local fontname       = result.fontname
    local family         = result.family
    local subfamily      = result.subfamily
    local familyname     = result.familyname
    local subfamilyname  = result.subfamilyname
    local weight         = result.weight
    local width          = result.width
    local italicangle    = tonumber(result.italicangle)
    local subfont        = subfont
    local rawname        = fullname or fontname or familyname
    local filebase       = removesuffix(basename(filename))
    local cleanfilename  = cleanname(filebase) -- for WS
    -- normalize
    fullname       = fullname       and cleanname(fullname)
    fontname       = fontname       and cleanname(fontname)
    family         = family         and cleanname(family)
    subfamily      = subfamily      and cleanname(subfamily)
    familyname     = familyname     and cleanname(familyname)
    subfamilyname  = subfamilyname  and cleanname(subfamilyname)
    weight         = weight         and cleanname(weight)
    width          = width          and cleanname(width)
    italicangle    = italicangle == 0 and nil
    -- analyze
    local a_name, a_weight, a_style, a_width, a_variant = analyzespec(fullname or fontname or familyname)
    -- check
    local width   = width or a_width
    local variant = a_variant
    local style   = subfamilyname or subfamily -- can re really trust subfamilyname?
    if style then
        style = gsub(style,"[^%a]","")
    elseif italicangle then
        style = "italic"
    end
    if not variant or variant == "" then
        variant = "normal"
    end
    if not weight or weight == "" then
        weight = a_weight
    end
    if not style or style == ""  then
        style = a_style
    end
    if not familyname then
        familyname = a_name
    end
    fontname   = fontname   or fullname or familyname or filebase -- maybe cleanfilename
    fullname   = fullname   or fontname
    familyname = familyname or fontname
    -- we do these sparse -- todo: check table type or change names in ff loader
    local units      = result.units       or 1000 -- can be zero too
    local designsize = result.designsize  or 0
    local minsize    = result.minsize     or 0
    local maxsize    = result.maxsize     or 0
    local angle      = result.italicangle or 0
    local pfmwidth   = result.pfmwidth    or 0
    local pfmweight  = result.pfmweight   or 0
    --
    local instancenames = result.instancenames
    --
    specifications[#specifications+1] = {
        filename       = filename, -- unresolved
        cleanfilename  = cleanfilename,
     -- subfontindex   = subfont,
        format         = lower(suffix),
        subfont        = subfont,
        rawname        = rawname,
        fullname       = fullname,
        fontname       = fontname,
        family         = family,
        subfamily      = subfamily,
        familyname     = familyname,
        subfamilyname  = subfamilyname,
        weight         = weight,
        style          = style,
        width          = width,
        variant        = variant,
        units          = units        ~= 1000 and units        or nil,
        pfmwidth       = pfmwidth     ~=    0 and pfmwidth     or nil,
        pfmweight      = pfmweight    ~=    0 and pfmweight    or nil,
        angle          = angle        ~=    0 and angle        or nil,
        minsize        = minsize      ~=    0 and minsize      or nil,
        maxsize        = maxsize      ~=    0 and maxsize      or nil,
        designsize     = designsize   ~=    0 and designsize   or nil,
        modification   = modification ~=    0 and modification or nil,
        instancenames  = instancenames or nil,
    }
end

local function cleanupkeywords()
    local data           = names.data
    local specifications = names.data.specifications
    if specifications then
        local weights  = { }
        local styles   = { }
        local widths   = { }
        local variants = { }
        for i=1,#specifications do
            local s = specifications[i]
            -- fix (sofar styles are taken from the name, and widths from the specification)
            local _, b_weight, b_style, b_width, b_variant = analyzespec(s.weight)
            local _, c_weight, c_style, c_width, c_variant = analyzespec(s.style)
            local _, d_weight, d_style, d_width, d_variant = analyzespec(s.width)
            local _, e_weight, e_style, e_width, e_variant = analyzespec(s.variant)
            local _, f_weight, f_style, f_width, f_variant = analyzespec(s.fullname or "")
            local weight  = b_weight  or c_weight  or d_weight  or e_weight  or f_weight  or "normal"
            local style   = b_style   or c_style   or d_style   or e_style   or f_style   or "normal"
            local width   = b_width   or c_width   or d_width   or e_width   or f_width   or "normal"
            local variant = b_variant or c_variant or d_variant or e_variant or f_variant or "normal"
            weight  = remappedweights [weight  or ""]
            style   = remappedstyles  [style   or ""]
            width   = remappedwidths  [width   or ""]
            variant = remappedvariants[variant or ""]
            weights [weight ] = (weights [weight ] or 0) + 1
            styles  [style  ] = (styles  [style  ] or 0) + 1
            widths  [width  ] = (widths  [width  ] or 0) + 1
            variants[variant] = (variants[variant] or 0) + 1
            if weight ~= s.weight then
                s.fontweight = s.weight
            end
            s.weight, s.style, s.width, s.variant = weight, style, width, variant
        end
        local statistics = data.statistics
        statistics.used_weights  = weights
        statistics.used_styles   = styles
        statistics.used_widths   = widths
        statistics.used_variants = variants
    end
end

local function collectstatistics(runtime)
    local data           = names.data
    local specifications = data.specifications
    local statistics     = data.statistics
    if specifications then
        local f_w = formatters["%i"]
        local f_a = formatters["%0.2f"]
        -- normal stuff
        local weights    = { }
        local styles     = { }
        local widths     = { }
        local variants   = { }
        -- weird stuff
        local angles     = { }
        -- extra stuff
        local pfmweights = { } setmetatableindex(pfmweights,"table")
        local pfmwidths  = { } setmetatableindex(pfmwidths, "table")
        -- main loop
        for i=1,#specifications do
            local s = specifications[i]
            -- normal stuff
            local weight  = s.weight
            local style   = s.style
            local width   = s.width
            local variant = s.variant
            if weight  then weights [weight ] = (weights [weight ] or 0) + 1 end
            if style   then styles  [style  ] = (styles  [style  ] or 0) + 1 end
            if width   then widths  [width  ] = (widths  [width  ] or 0) + 1 end
            if variant then variants[variant] = (variants[variant] or 0) + 1 end
            -- weird stuff
            local angle   = f_a(tonumber(s.angle) or 0)
            angles[angle] = (angles[angles] or 0) + 1
            -- extra stuff
            local pfmweight     = f_w(s.pfmweight or 0)
            local pfmwidth      = f_w(s.pfmwidth  or 0)
            local tweights      = pfmweights[pfmweight]
            local twidths       = pfmwidths [pfmwidth]
            tweights[pfmweight] = (tweights[pfmweight] or 0) + 1
            twidths[pfmwidth]   = (twidths [pfmwidth]  or 0) + 1
        end
        --
        statistics.weights    = weights
        statistics.styles     = styles
        statistics.widths     = widths
        statistics.variants   = variants
        statistics.angles     = angles
        statistics.pfmweights = pfmweights
        statistics.pfmwidths  = pfmwidths
        statistics.fonts      = #specifications
        --
        setmetatableindex(pfmweights,nil)
        setmetatableindex(pfmwidths, nil)
        --
        report_names("")
        report_names("statistics: ")
        report_names("")
        report_names("weights")
        report_names("")
        report_names(formatters["  %T"](weights))
        report_names("")
        report_names("styles")
        report_names("")
        report_names(formatters["  %T"](styles))
        report_names("")
        report_names("widths")
        report_names("")
        report_names(formatters["  %T"](widths))
        report_names("")
        report_names("variants")
        report_names("")
        report_names(formatters["  %T"](variants))
        report_names("")
        report_names("angles")
        report_names("")
        report_names(formatters["  %T"](angles))
        report_names("")
        report_names("pfmweights")
        report_names("")
        for k, v in sortedhash(pfmweights) do
            report_names(formatters["  %-10s: %T"](k,v))
        end
        report_names("")
        report_names("pfmwidths")
        report_names("")
        for k, v in sortedhash(pfmwidths) do
            report_names(formatters["  %-10s: %T"](k,v))
        end
        report_names("")
        report_names("registered fonts : %i", statistics.fonts)
        report_names("read files       : %i", statistics.readfiles)
        report_names("skipped files    : %i", statistics.skippedfiles)
        report_names("duplicate files  : %i", statistics.duplicatefiles)
            if runtime then
        report_names("total scan time  : %0.3f seconds",runtime)
            end
    end
end

local function collecthashes()
    local data           = names.data
    local mappings       = data.mappings
    local fallbacks      = data.fallbacks
    local specifications = data.specifications
    local nofmappings    = 0
    local noffallbacks   = 0
    if specifications then
        -- maybe multiple passes (for the compatible and cffnames so that they have less preference)
        local conflicts = setmetatableindex("table")
        for index=1,#specifications do
            local specification  = specifications[index]
            local format         = specification.format
            local fullname       = specification.fullname
            local fontname       = specification.fontname
            local familyname     = specification.familyname or specification.family
            local subfamilyname  = specification.subfamilyname
            local subfamily      = specification.subfamily
            local weight         = specification.weight
            local mapping        = mappings[format]
            local fallback       = fallbacks[format]
            local instancenames  = specification.instancenames
            if fullname and not mapping[fullname] then
                mapping[fullname] = index
                nofmappings       = nofmappings + 1
            end
            if fontname and not mapping[fontname] then
                mapping[fontname] = index
                nofmappings       = nofmappings + 1
            end
            if instancenames then
                for i=1,#instancenames do
                    local instance = fullname .. instancenames[i]
                    mapping[instance] = index
                    nofmappings       = nofmappings + 1
                end
            end
            if familyname then
                if weight and weight ~= sub(familyname,#familyname-#weight+1,#familyname) then
                    local madename = familyname .. weight
                    if not mapping[madename] and not fallback[madename] then
                        fallback[madename] = index
                        noffallbacks       = noffallbacks + 1
                    end
                end
                if subfamily and subfamily ~= sub(familyname,#familyname-#subfamily+1,#familyname) then
                    local extraname = familyname .. subfamily
                    if not mapping[extraname] and not fallback[extraname] then
                        fallback[extraname] = index
                        noffallbacks        = noffallbacks + 1
                    end
                end
                if subfamilyname and subfamilyname ~= sub(familyname,#familyname-#subfamilyname+1,#familyname) then
                    local extraname = familyname .. subfamilyname
                    if not mapping[extraname] and not fallback[extraname] then
                        fallback[extraname] = index
                        noffallbacks        = noffallbacks + 1
                    end
                end
                -- dangerous ... first match takes slot
                if not mapping[familyname] and not fallback[familyname] then
                    fallback[familyname] = index
                    noffallbacks         = noffallbacks + 1
                end
                local conflict = conflicts[format]
                conflict[familyname] = (conflict[familyname] or 0) + 1
            end
        end
        for format, conflict in next, conflicts do
            local fallback = fallbacks[format]
            for familyname, n in next, conflict do
                if n > 1 then
                    fallback[familyname] = nil
                    noffallbacks = noffallbacks - n
                end
            end
        end
    end
    return nofmappings, noffallbacks
end

local function collectfamilies()
    local data           = names.data
    local specifications = data.specifications
    local families       = data.families
    for index=1,#specifications do
        local familyname = specifications[index].familyname
        local family     = families[familyname]
        if not family then
            families[familyname] = { index }
        else
            family[#family+1] = index
        end
    end
end

local function checkduplicate(where) -- fails on "Romantik" but that's a border case anyway
    local data           = names.data
    local mapping        = data[where]
    local specifications = data.specifications
    local loaded         = { }
    if specifications and mapping then
     -- was: for _, m in sortedhash(mapping) do
        local order = filters.list
        for i=1,#order do
            local m = mapping[order[i]]
            for k, v in sortedhash(m) do
                local s = specifications[v]
                local hash = formatters["%s-%s-%s-%s-%s"](s.familyname,s.weight or "*",s.style or "*",s.width or "*",s.variant or "*")
                local h = loaded[hash]
                if h then
                    local ok = true
                    local fn = s.filename
                    for i=1,#h do
                        if h[i] == fn then
                            ok = false
                            break
                        end
                    end
                    if ok then
                        h[#h+1] = fn
                    end
                else
                    loaded[hash] = { s.filename }
                end
            end
        end
    end
    local n = 0
    for k, v in sortedhash(loaded) do
        local nv = #v
        if nv > 1 then
            n = n + nv
        end
    end
    report_names("%a double lookups in %a",n,where)
end

local function checkduplicates()
    checkduplicate("mappings")
    checkduplicate("fallbacks")
end

local function sorthashes()
    local data             = names.data
    local list             = filters.list
    local mappings         = data.mappings
    local fallbacks        = data.fallbacks
    local sorted_mappings  = { }
    local sorted_fallbacks = { }
    data.sorted_mappings   = sorted_mappings
    data.sorted_fallbacks  = sorted_fallbacks
    for i=1,#list do
        local l = list[i]
        sorted_mappings [l] = table.keys(mappings[l])
        sorted_fallbacks[l] = table.keys(fallbacks[l])
        sort(sorted_mappings [l],sorter)
        sort(sorted_fallbacks[l],sorter)
    end
    local sorted_families = table.keys(data.families)
    data.sorted_families  = sorted_families
    sort(sorted_families,sorter)
end

local function unpackreferences()
    local data           = names.data
    local specifications = data.specifications
    if specifications then
        for k, v in sortedhash(data.families) do
            for i=1,#v do
                v[i] = specifications[v[i]]
            end
        end
        local mappings = data.mappings
        if mappings then
            for _, m in sortedhash(mappings) do
                for k, v in sortedhash(m) do
                    m[k] = specifications[v]
                end
            end
        end
        local fallbacks = data.fallbacks
        if fallbacks then
            for _, f in sortedhash(fallbacks) do
                for k, v in sortedhash(f) do
                    f[k] = specifications[v]
                end
            end
        end
    end
end

local function analyzefiles(olddata)

    local data               = names.data
    local done               = { }
    local totalnofread       = 0
    local totalnofskipped    = 0
    local totalnofduplicates = 0
    local nofread            = 0
    local nofskipped         = 0
    local nofduplicates      = 0
    local skip_paths         = filters.paths
    local skip_names         = filters.names
    local specifications     = data.specifications
    local oldindices         = olddata and olddata.indices        or { }
    local oldspecifications  = olddata and olddata.specifications or { }
    local oldrejected        = olddata and olddata.rejected       or { }
    local treatmentdata      = treatments.data or { } -- when used outside context

    local function identify(completename,name,suffix,storedname)
        local pathpart, basepart = splitbase(completename)
        nofread = nofread + 1
        local treatment = treatmentdata[completename] or treatmentdata[basepart]
        if treatment and treatment.ignored then
            nofskipped = nofskipped + 1
        elseif done[name] then
            if lower(completename) ~= lower(done[name]) then
                -- already done (avoid otf afm clash)
                nofduplicates = nofduplicates + 1
                nofskipped = nofskipped + 1
            end
        elseif not exists(completename) then
            -- weird error
            nofskipped = nofskipped + 1
        elseif not is_qualified_path(completename) and findfile(completename,suffix) == "" then
            -- not locatable by backend anyway
            nofskipped = nofskipped + 1
        else
            if #skip_paths > 0 then
                for i=1,#skip_paths do
                    if find(pathpart,skip_paths[i]) then
                        nofskipped = nofskipped + 1
                        return
                    end
                end
            end
            if #skip_names > 0 then
                for i=1,#skip_paths do
                    if find(basepart,skip_names[i]) then
                        done[name] = true
                        nofskipped = nofskipped + 1
                        return
                    end
                end
            end
            -- needs checking with ttc / ttx : date not updated ?
            local result = nil
            local modification = modificationtime(completename)
            if olddata and modification and modification > 0 then
                local oldindex = oldindices[storedname] -- index into specifications
                if oldindex then
                    local oldspecification = oldspecifications[oldindex]
                    if oldspecification and oldspecification.filename == storedname then -- double check for out of sync
                        local oldmodification = oldspecification.modification
                        if oldmodification == modification then
                            result = oldspecification
                            specifications[#specifications + 1] = result
                        end
                    end
                elseif oldrejected[storedname] == modification then
                    result = false
                end
            end
            if result == nil then
                local lsuffix = lower(suffix)
                local result, message = filters[lsuffix](completename)
                if result then
                    if #result > 0 then
                        for r=1,#result do
                            check_name(data,result[r],storedname,modification,suffix,r) -- subfonts start at zero
                        end
                    else
                        check_name(data,result,storedname,modification,suffix)
                    end
                end
            end
            done[name] = completename
        end
    end

    local function traverse(what, method)
        local list = filters.list
        for n=1,#list do
            local suffix = list[n]
            local t = os.gettimeofday() -- use elapser
            nofread, nofskipped, nofduplicates = 0, 0, 0
            suffix = lower(suffix)
            report_names("identifying %s font files with suffix %a",what,suffix)
            method(suffix)
            suffix = upper(suffix)
            report_names("identifying %s font files with suffix %a",what,suffix)
            method(suffix)
            totalnofread, totalnofskipped, totalnofduplicates = totalnofread + nofread, totalnofskipped + nofskipped, totalnofduplicates + nofduplicates
            local elapsed = os.gettimeofday() - t
            report_names("%s %s files identified, %s skipped, %s duplicates, %s hash entries added, runtime %0.3f seconds",nofread,what,nofskipped,nofduplicates,nofread-nofskipped,elapsed)
        end
    end

    -- problem .. this will not take care of duplicates

    local function withtree(suffix)
        dowithfilesintree(
            ".*%." .. suffix .. "$",
            function(method,root,path,name)
                if method == "file" or method == "tree" then
                    local completename = root .."/" .. path .. "/" .. name
                    completename = resolveprefix(completename) -- no shortcut
                    identify(completename,name,suffix,name)
                    return true
                end
            end
        )
    end

    -- This include system fonts, I think, because dowithfilesintree() gets the
    --    list from luaotfload's db and then augments that db.
    -- So I think the comment on the next line doesn't apply here.
    traverse("tree",withtree) -- TEXTREE only

    data.statistics.readfiles      = totalnofread
    data.statistics.skippedfiles   = totalnofskipped
    data.statistics.duplicatefiles = totalnofduplicates

end

local function addfilenames()
    local data           = names.data
    local specifications = data.specifications
    local indices        = { }
    local files          = { }
    for i=1,#specifications do
        local fullname = specifications[i].filename
        files[cleanfilename(fullname)] = fullname
        indices[fullname] = i
    end
    data.files   = files
    data.indices = indices
end

local function rejectclashes() -- just to be sure, so no explicit afm will be found then
    local specifications  = names.data.specifications
    local used            = { }
    local okay            = { }
    local rejected        = { } -- only keep modification
    local o               = 0
    for i=1,#specifications do
        local s = specifications[i]
        local f = s.fontname
        if f then
            local fnd = used[f]
            local fnm = s.filename
            if fnd then
                rejected[f] = s.modification
            else
                used[f] = fnm
                o = o + 1
                okay[o] = s
            end
        else
            o = o + 1
            okay[o] = s
        end
    end
    local d = #specifications - #okay
    if d > 0 then
        report_names("%s files rejected due to clashes",d)
    end
    names.data.specifications = okay
    names.data.rejected       = rejected
end

local function resetdata()
    local mappings  = { }
    local fallbacks = { }
    for _, k in next, filters.list do
        mappings [k] = { }
        fallbacks[k] = { }
    end
    names.data = {
        version        = names.version,
        mappings       = mappings,
        fallbacks      = fallbacks,
        specifications = { },
        families       = { },
        statistics     = { },
        names          = { },
        indices        = { },
        rejected       = { },
        datastate      = datastate(),
    }
end

function names.identify(force)
    -- Print a message while generating our font name database so that users
    -- don't get confused by the long pause.
    texio.write_nl("Generating font name database...")
    local starttime = os.gettimeofday() -- use elapser
    resetdata()
    analyzefiles(not force and names.readdata(names.basename))
    rejectclashes()
    collectfamilies()
    cleanupkeywords()
    collecthashes()
    checkduplicates()
    addfilenames()
    -- sorthashes() -- will be resorted when saved
    collectstatistics(os.gettimeofday()-starttime)
    texio.write(" done.\n")
end

function names.is_permitted(name)
    return containers.is_usable(names.cache, name)
end
function names.writedata(name,data)
    containers.write(names.cache,name,data)
end
function names.readdata(name)
    return containers.read(names.cache,name)
end

function names.load(reload,force)
    if not names.loaded then
        if reload then
            if names.is_permitted(names.basename) then
                names.identify(force)
                names.writedata(names.basename,names.data)
            else
                report_names("unable to access database cache")
            end
            names.saved = true
        end
        local data = names.readdata(names.basename)
        names.data = data
        if not names.saved then
            if not data or not next(data) or not data.specifications or not next(data.specifications) then
               names.load(true)
            end
            names.saved = true
        end
        if not data then
            report_names("accessing the data table failed")
        else
            unpackreferences()
            sorthashes()
        end
        names.loaded = true
    end
end

local function list_them(mapping,sorted,pattern,t,all)
    if mapping[pattern] then
        t[pattern] = mapping[pattern]
    else
        for k=1,#sorted do
            local v = sorted[k]
            if not t[v] and find(v,pattern) then
                t[v] = mapping[v]
                if not all then
                    return
                end
            end
        end
    end
end

function names.list(pattern,reload,all) -- here?
    names.load() -- todo reload
    if names.loaded then
        local t = { }
        local data = names.data
        if data then
            local list             = filters.list
            local mappings         = data.mappings
            local sorted_mappings  = data.sorted_mappings
            local fallbacks        = data.fallbacks
            local sorted_fallbacks = data.sorted_fallbacks
            for i=1,#list do
                local format = list[i]
                list_them(mappings[format],sorted_mappings[format],pattern,t,all)
                if next(t) and not all then
                    return t
                end
                list_them(fallbacks[format],sorted_fallbacks[format],pattern,t,all)
                if next(t) and not all then
                    return t
                end
            end
        end
        return t
    end
end

local reloaded = false

local function is_reloaded()
    if not reloaded then
        local data = names.data
        if autoreload then
            local c_status = serialize(datastate())
            local f_status = serialize(data.datastate)
            if c_status == f_status then
                return
            else
                report_names("font database has mismatching configuration and file hashes")
            end
        else
            report_names("font database is regenerated")
        end
        names.loaded = false
        reloaded = true
        names.load(true)
    end
end

-- The resolver also checks if the cached names are loaded. Being clever here is for
-- testing purposes only (it deals with names prefixed by an encoding name).

local function fuzzy(mapping,sorted,name,sub) -- no need for reverse sorted here
    local condensed = gsub(name,"[^%a%d]","")
    local pattern   = condensed .. "$"
    local matches   = false
    for k=1,#sorted do
        local v = sorted[k]
        if v == condensed then
            return mapping[v], v
        elseif find(v,pattern) then
            return mapping[v], v
        elseif find(v,condensed) then
            if matches then
                matches[#matches+1] = v
            else
                matches = { v }
            end
        end
    end
    if matches then
        if #matches > 1 then
            sort(matches,function(a,b) return #a < #b end)
        end
        matches = matches[1]
        return mapping[matches], matches
    end
end

-- we could cache a lookup .. maybe some day ... (only when auto loaded!)

local function checkinstance(found,askedname)
    local instancenames = found.instancenames
    if instancenames then
        local fullname = found.fullname
        for i=1,#instancenames do
            local instancename = instancenames[i]
            if fullname .. instancename == askedname then
                local f = fastcopy(found)
                f.instances = nil
                f.instance  = instancename
                return f
            end
        end
    end
    return found
end

local function foundname(name,sub) -- sub is not used currently
    local data             = names.data
    local mappings         = data.mappings
    local sorted_mappings  = data.sorted_mappings
    local fallbacks        = data.fallbacks
    local sorted_fallbacks = data.sorted_fallbacks
    local list             = filters.list
    -- dilemma: we lookup in the order otf ttf ttc ... afm but now an otf fallback
    -- can come after an afm match ... well, one should provide nice names anyway
    -- and having two lists is not an option
    for i=1,#list do
        local l = list[i]
        local found = mappings[l][name]
        if found then
            return checkinstance(found,name)
        end
    end
    for i=1,#list do
        local l = list[i]
        local found, fname = fuzzy(mappings[l],sorted_mappings[l],name,sub)
        if found then
            return checkinstance(found,name)
        end
    end
    for i=1,#list do
        local l = list[i]
        local found = fallbacks[l][name]
        if found then
            return checkinstance(found,name)
        end
    end
    for i=1,#list do
        local l = list[i]
        local found, fname = fuzzy(sorted_mappings[l],sorted_fallbacks[l],name,sub)
        if found then
            return checkinstance(found,name)
        end
    end
end

function names.resolvedspecification(askedname,sub)
    if askedname and askedname ~= "" and names.enabled then
        askedname = cleanname(askedname)
        names.load()
        local found = foundname(askedname,sub)
        if not found and is_reloaded() then
            found = foundname(askedname,sub)
        end
        return found
    end
end

function names.resolve(askedname,sub)
    local found = names.resolvedspecification(askedname,sub)
    if found then
        return found.filename, found.subfont and found.rawname, found.subfont, found.instance
    end
end

local specifications = allocate()

return {
    cache = names.cache,
    cleanfilename = names.cleanfilename,
    get_data = function() return names.data end,
    -- data = names.data,
    list = names.list,
    load = names.load,
    resolve = names.resolve,
    writable = names.cache.writable,
    -- ? --
    -- filters = names.filters,
}

-- vim: et:foldmethod=marker:tw=4:sw=2:
