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

-- needs a cleanup: merge of replace, lang/script etc

local texsprint, count, texsetcount = tex.sprint, tex.count, tex.setcount
local format, concat, gmatch, match, find, lower, gsub, byte = string.format, table.concat, string.gmatch, string.match, string.find, string.lower, string.gsub, string.byte

local tostring, next, type = tostring, next, type
local lpegmatch = lpeg.match
local round = math.round

local ctxcatcodes = tex.ctxcatcodes

local trace_defining = false  trackers.register("fonts.defining", function(v) trace_defining = v end)

local tfm      = fonts.tfm
local define   = fonts.define
local fontdata = fonts.identifiers
local specify  = define.specify

specify.context_setups  = specify.context_setups  or { }
specify.context_numbers = specify.context_numbers or { }
specify.context_merged  = specify.context_merged  or { }
specify.synonyms        = specify.synonyms        or { }

local setups   = specify.context_setups
local numbers  = specify.context_numbers
local merged   = specify.context_merged
local synonyms = specify.synonyms
local triggers = fonts.triggers

--[[ldx--
<p>So far we haven't really dealt with features (or whatever we want
to pass along with the font definition. We distinguish the following
situations:</p>
situations:</p>

<code>
name:xetex like specs
name@virtual font spec
name*context specification
</code>
--ldx]]--

function specify.predefined(specification)
    local detail = specification.detail
    if detail ~= "" then
    --  detail = gsub(detail,"["..define.splitsymbols.."].*$","") -- get rid of *whatever specs and such
        if define.methods[detail] then                            -- since these may be appended at the
            specification.features.vtf = { preset = detail }      -- tex end by default
        end
    end
    return specification
end

define.register_split("@", specify.predefined)

storage.register("fonts/setups" ,  define.specify.context_setups , "fonts.define.specify.context_setups" )
storage.register("fonts/numbers",  define.specify.context_numbers, "fonts.define.specify.context_numbers")
storage.register("fonts/merged",   define.specify.context_merged,  "fonts.define.specify.context_merged")
storage.register("fonts/synonyms", define.specify.synonyms,        "fonts.define.specify.synonyms")

local normalize_meanings = fonts.otf.meanings.normalize
local settings_to_hash   = aux.settings_to_hash
local default_features   = fonts.otf.features.default

local function preset_context(name,parent,features) -- currently otf only
    if features == "" and find(parent,"=") then
        features = parent
        parent = ""
    end
    if features == "" then
        features = { }
    elseif type(features) == "string" then
        features = normalize_meanings(settings_to_hash(features))
    else
        features = normalize_meanings(features)
    end
    -- todo: synonyms, and not otf bound
    if parent ~= "" then
        for p in gmatch(parent,"[^, ]+") do
            local s = setups[p]
            if s then
                for k,v in next, s do
                    if features[k] == nil then
                        features[k] = v
                    end
                end
            end
        end
    end
    -- these are auto set so in order to prevent redundant definitions
    -- we need to preset them (we hash the features and adding a default
    -- setting during initialization may result in a different hash)
    for k,v in next, triggers do
        if features[v] == nil then -- not false !
            local vv = default_features[v]
            if vv then features[v] = vv end
        end
    end
    -- sparse 'm so that we get a better hash and less test (experimental
    -- optimization)
    local t = { } -- can we avoid t ?
    for k,v in next, features do
        if v then t[k] = v end
    end
    -- needed for dynamic features
    local number = (setups[name] and setups[name].number) or 0
    if number == 0 then
        number = #numbers + 1
        numbers[number] = name
    end
    t.number = number
    setups[name] = t
    return number, t
end

local function context_number(name) -- will be replaced
    local t = setups[name]
    if not t then
        return 0
    elseif t.auto then
        local lng = tonumber(tex.language)
        local tag = name .. ":" .. lng
        local s = setups[tag]
        if s then
            return s.number or 0
        else
            local script, language = languages.association(lng)
            if t.script ~= script or t.language ~= language then
                local s = table.fastcopy(t)
                local n = #numbers + 1
                setups[tag] = s
                numbers[n] = tag
                s.number = n
                s.script = script
                s.language = language
                return n
            else
                setups[tag] = t
                return t.number or 0
            end
        end
    else
        return t.number or 0
    end
end

local function merge_context(currentnumber,extraname,option)
    local current = setups[numbers[currentnumber]]
    local extra = setups[extraname]
    if extra then
        local mergedfeatures, mergedname = { }, nil
        if option < 0 then
            if current then
                for k, v in next, current do
                    if not extra[k] then
                        mergedfeatures[k] = v
                    end
                end
            end
            mergedname = currentnumber .. "-" .. extraname
        else
            if current then
                for k, v in next, current do
                    mergedfeatures[k] = v
                end
            end
            for k, v in next, extra do
                mergedfeatures[k] = v
            end
            mergedname = currentnumber .. "+" .. extraname
        end
        local number = #numbers + 1
        mergedfeatures.number = number
        numbers[number] = mergedname
        merged[number] = option
        setups[mergedname] = mergedfeatures
        return number -- context_number(mergedname)
    else
        return currentnumber
    end
end

local function register_context(fontnumber,extraname,option)
    local extra = setups[extraname]
    if extra then
        local mergedfeatures, mergedname = { }, nil
        if option < 0 then
            mergedname = fontnumber .. "-" .. extraname
        else
            mergedname = fontnumber .. "+" .. extraname
        end
        for k, v in next, extra do
            mergedfeatures[k] = v
        end
        local number = #numbers + 1
        mergedfeatures.number = number
        numbers[number] = mergedname
        merged[number] = option
        setups[mergedname] = mergedfeatures
        return number -- context_number(mergedname)
    else
        return 0
    end
end

specify.preset_context   = preset_context
specify.context_number   = context_number
specify.merge_context    = merge_context
specify.register_context = register_context

local current_font  = font.current
local tex_attribute = tex.attribute

local cache = { } -- concat might be less efficient than nested tables

function fonts.withset(name,what)
    local zero = tex_attribute[0]
    local hash = zero .. "+" .. name .. "*" .. what
    local done = cache[hash]
    if not done then
        done = merge_context(zero,name,what)
        cache[hash] = done
    end
    tex_attribute[0] = done
end
function fonts.withfnt(name,what)
    local font = current_font()
    local hash = font .. "*" .. name .. "*" .. what
    local done = cache[hash]
    if not done then
        done = register_context(font,name,what)
        cache[hash] = done
    end
    tex_attribute[0] = done
end

function specify.show_context(name)
    return setups[name] or setups[numbers[name]] or setups[numbers[tonumber(name)]] or { }
end

local function split_context(features)
    return setups[features] or (preset_context(features,"","") and setups[features])
end

specify.split_context = split_context

function specify.context_tostring(name,kind,separator,yes,no,strict,omit) -- not used
    return aux.hash_to_string(table.merged(fonts[kind].features.default or {},setups[name] or {}),separator,yes,no,strict,omit)
end

local splitter = lpeg.splitat(",")

function specify.starred(features) -- no longer fallbacks here
    local detail = features.detail
    if detail and detail ~= "" then
        features.features.normal = split_context(detail)
    else
        features.features.normal = { }
    end
    return features
end

define.register_split('*',specify.starred)

-- define (two steps)

local P, C, Cc = lpeg.P, lpeg.C, lpeg.Cc

local space        = P(" ")
local spaces       = space^0
local leftparent   = (P"(")
local rightparent  = (P")")
local value        = C((leftparent * (1-rightparent)^0 * rightparent + (1-space))^1)
local dimension    = C((space/"" + P(1))^1)
local rest         = C(P(1)^0)
local scale_none   =               Cc(0)
local scale_at     = P("at")     * Cc(1) * spaces * dimension -- value
local scale_sa     = P("sa")     * Cc(2) * spaces * dimension -- value
local scale_mo     = P("mo")     * Cc(3) * spaces * dimension -- value
local scale_scaled = P("scaled") * Cc(4) * spaces * dimension -- value

local sizepattern  = spaces * (scale_at + scale_sa + scale_mo + scale_scaled + scale_none)
local splitpattern = spaces * value * spaces * rest

local specification --

local get_specification = define.get_specification

-- we can make helper macros which saves parsing (but normaly not
-- that many calls, e.g. in mk a couple of 100 and in metafun 3500)

function define.command_1(str)
    statistics.starttiming(fonts)
    local fullname, size = lpegmatch(splitpattern,str)
    local lookup, name, sub, method, detail = get_specification(fullname)
    if not name then
        logs.report("define font","strange definition '%s'",str)
        texsprint(ctxcatcodes,"\\fcglet\\somefontname\\defaultfontfile")
    elseif name == "unknown" then
        texsprint(ctxcatcodes,"\\fcglet\\somefontname\\defaultfontfile")
    else
        texsprint(ctxcatcodes,"\\fcxdef\\somefontname{",name,"}")
    end
    -- we can also use a count for the size
    if size and size ~= "" then
        local mode, size = lpegmatch(sizepattern,size)
        if size and mode then
            count.scaledfontmode = mode
            texsprint(ctxcatcodes,"\\def\\somefontsize{",size,"}")
        else
            count.scaledfontmode = 0
            texsprint(ctxcatcodes,"\\let\\somefontsize\\empty")
        end
    elseif true then
        -- so we don't need to check in tex
        count.scaledfontmode = 2
        texsprint(ctxcatcodes,"\\let\\somefontsize\\empty")
    else
        count.scaledfontmode = 0
        texsprint(ctxcatcodes,"\\let\\somefontsize\\empty")
    end
    specification = define.makespecification(str,lookup,name,sub,method,detail,size)
end

local n = 0

-- we can also move rscale to here (more consistent)

function define.command_2(global,cs,str,size,classfeatures,fontfeatures,classfallbacks,fontfallbacks,mathsize,textsize,relativeid)
    if trace_defining then
        logs.report("define font","memory usage before: %s",statistics.memused())
    end
    -- name is now resolved and size is scaled cf sa/mo
    local lookup, name, sub, method, detail = get_specification(str or "")
    -- asome settings can be overloaded
    if lookup and lookup ~= "" then
        specification.lookup = lookup
    end
    if relativeid and relativeid ~= "" then -- experimental hook
        local id = tonumber(relativeid) or 0
        specification.relativeid = id > 0 and id
    end
    specification.name = name
    specification.size = size
    specification.sub = (sub and sub ~= "" and sub) or specification.sub
    specification.mathsize = mathsize
    specification.textsize = textsize
    if detail and detail ~= "" then
        specification.method, specification.detail = method or "*", detail
    elseif specification.detail and specification.detail ~= "" then
        -- already set
    elseif fontfeatures and fontfeatures ~= "" then
        specification.method, specification.detail = "*", fontfeatures
    elseif classfeatures and classfeatures ~= "" then
        specification.method, specification.detail = "*", classfeatures
    end
    if fontfallbacks and fontfallbacks ~= "" then
        specification.fallbacks = fontfallbacks
    elseif classfallbacks and classfallbacks ~= "" then
        specification.fallbacks = classfallbacks
    end
    local tfmdata = define.read(specification,size) -- id not yet known
    if not tfmdata then
        logs.report("define font","unable to define %s as \\%s",name,cs)
        texsetcount("global","lastfontid",-1)
    elseif type(tfmdata) == "number" then
        if trace_defining then
            logs.report("define font","reusing %s with id %s as \\%s (features: %s/%s, fallbacks: %s/%s)",name,tfmdata,cs,classfeatures,fontfeatures,classfallbacks,fontfallbacks)
        end
        tex.definefont(global,cs,tfmdata)
        -- resolved (when designsize is used):
        texsprint(ctxcatcodes,format("\\def\\somefontsize{%isp}",fontdata[tfmdata].size))
        texsetcount("global","lastfontid",tfmdata)
    else
    --  local t = os.clock(t)
        local id = font.define(tfmdata)
    --  print(name,os.clock()-t)
        tfmdata.id = id
        define.register(tfmdata,id)
        tex.definefont(global,cs,id)
        tfm.cleanup_table(tfmdata)
        if trace_defining then
            logs.report("define font","defining %s with id %s as \\%s (features: %s/%s, fallbacks: %s/%s)",name,id,cs,classfeatures,fontfeatures,classfallbacks,fontfallbacks)
        end
        -- resolved (when designsize is used):
        texsprint(ctxcatcodes,format("\\def\\somefontsize{%isp}",tfmdata.size))
    --~ if specification.fallbacks then
    --~     fonts.collections.prepare(specification.fallbacks)
    --~ end
        texsetcount("global","lastfontid",id)
    end
    if trace_defining then
        logs.report("define font","memory usage after: %s",statistics.memused())
    end
    statistics.stoptiming(fonts)
end

local enable_auto_r_scale = false

experiments.register("fonts.autorscale", function(v)
    enable_auto_r_scale = v
end)

local calculate_scale = fonts.tfm.calculate_scale

function fonts.tfm.calculate_scale(tfmtable, scaledpoints, relativeid)
    local scaledpoints, delta, units = calculate_scale(tfmtable, scaledpoints, relativeid)
    if enable_auto_r_scale and relativeid then -- for the moment this is rather context specific
        local relativedata = fontdata[relativeid]
        local id_x_height = relativedata and relativedata.parameters and relativedata.parameters.x_height
        local tf_x_height = id_x_height and tfmtable.parameters and tfmtable.parameters.x_height * delta
        if tf_x_height then
            scaledpoints = (id_x_height/tf_x_height) * scaledpoints
            delta = scaledpoints/units
        end
    end
    return scaledpoints, delta, units
end

--~ table.insert(readers.sequence,1,'vtf')

--~ function readers.vtf(specification)
--~     if specification.features.vtf and specification.features.vtf.preset then
--~         return tfm.make(specification)
--~     else
--~         return nil
--~     end
--~ end

-- we need a place for this .. outside the generic scope

local dimenfactors = number.dimenfactors

function fonts.dimenfactor(unit,tfmdata)
    if unit == "ex" then
        return (tfmdata and tfmdata.parameters.x_height) or 655360
    elseif unit == "em" then
        return (tfmdata and tfmdata.parameters.em_height) or 655360
    else
        return dimenfactors[unit] or unit
    end
end

function fonts.cleanname(name)
    texsprint(ctxcatcodes,fonts.names.cleanname(name))
end

local p, f = 1, "%0.1fpt" -- normally this value is changed only once

local stripper = lpeg.patterns.strip_zeros

function fonts.nbfs(amount,precision)
    if precision ~= p then
        p = precision
        f = "%0." .. p .. "fpt"
    end
    texsprint(ctxcatcodes,lpegmatch(stripper,format(f,amount/65536)))
end

-- for the moment here, this will become a chain of extras that is
-- hooked into the ctx registration (or scaler or ...)

function fonts.set_digit_width(font)
    local tfmtable = fontdata[font]
    local parameters = tfmtable.parameters
    local width = parameters.digitwidth
    if not width then
        width = round(parameters.quad/2) -- maybe tex.scale
        local characters = tfmtable.characters
        for i=48,57 do
            local wd = round(characters[i].width)
            if wd > width then
                width = wd
            end
        end
        parameters.digitwidth = width
    end
    return width
end

fonts.get_digit_width = fonts.set_digit_width

-- soon to be obsolete:

local loaded = { -- prevent loading (happens in cont-sys files)
    ["original-base.map"     ] = true,
    ["original-ams-base.map" ] = true,
    ["original-ams-euler.map"] = true,
    ["original-public-lm.map"] = true,
}

function fonts.map.loadfile(name)
    name = file.addsuffix(name,"map")
    if not loaded[name] then
        pdf.mapfile(name)
        loaded[name] = true
    end
end

local loaded = { -- prevent double loading
}

function fonts.map.loadline(how,line)
    if line then
        how = how .. " " .. line
    elseif how == "" then
        how = "= " .. line
    end
    if not loaded[how] then
        pdf.mapline(how)
        loaded[how] = true
    end
end

function fonts.map.reset()
    pdf.mapfile("")
end

fonts.map.reset() -- resets the default file

-- we need an 'do after the banner hook'

-- pdf.mapfile("mkiv-base.map") -- loads the default file

local nounicode = byte("?")

local function name_to_slot(name) -- maybe some day rawdata
    local tfmdata = fonts.ids[font.current()]
    local shared = tfmdata and tfmdata.shared
    local fntdata = shared and shared.otfdata or shared.afmdata
    if fntdata then
        local unicode = fntdata.luatex.unicodes[name]
        if not unicode then
            return nounicode
        elseif type(unicode) == "number" then
            return unicode
        else -- multiple unicodes
            return unicode[1]
        end
    end
    return nounicode
end

fonts.name_to_slot = name_to_slot

function fonts.char(n) -- todo: afm en tfm
    if type(n) == "string" then
        n = name_to_slot(n)
    end
    if type(n) == "number" then
        texsprint(ctxcatcodes,format("\\char%s ",n))
    end
end

-- moved from ini:

fonts.color = { } -- dummy in ini

local attribute = attributes.private('color')
local mapping   = (attributes and attributes.list[attribute])  or { }

local set_attribute   = node.set_attribute
local unset_attribute = node.unset_attribute

function fonts.color.set(n,c)
    local mc = mapping[c]
    if not mc then
        unset_attribute(n,attribute)
    else
        set_attribute(n,attribute,mc)
    end
end

function fonts.color.reset(n)
    unset_attribute(n,attribute)
end

-- this will become obsolete:

fonts.otf.name_to_slot = name_to_slot
fonts.afm.name_to_slot = name_to_slot

fonts.otf.char = fonts.char
fonts.afm.char = fonts.char

-- this will change ...

function fonts.show_char_data(n)
    local tfmdata = fonts.ids[font.current()]
    if tfmdata then
        if type(n) == "string" then
            n = utf.byte(n)
        end
        local chr = tfmdata.characters[n]
        if chr then
            write_nl(format("%s @ %s => U%04X => %s => ",tfmdata.fullname,tfmdata.size,n,utf.char(n)) .. serialize(chr,false))
        end
    end
end

function fonts.show_font_parameters()
    local tfmdata = fonts.ids[font.current()]
    if tfmdata then
        local parameters, mathconstants = tfmdata.parameters, tfmdata.MathConstants
        local hasparameters, hasmathconstants = parameters and next(parameters), mathconstants and next(mathconstants)
        if hasparameters then
            write_nl(format("%s @ %s => parameters => ",tfmdata.fullname,tfmdata.size) .. serialize(parameters,false))
        end
        if hasmathconstants then
            write_nl(format("%s @ %s => math constants => ",tfmdata.fullname,tfmdata.size) .. serialize(mathconstants,false))
        end
        if not hasparameters and not hasmathconstants then
            write_nl(format("%s @ %s => no parameters and/or mathconstants",tfmdata.fullname,tfmdata.size))
        end
    end
end
