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

-- see typo-cap.lua for a word traverser variant

local next, type, tonumber = next, type, tonumber
local format, insert = string.format, table.insert
local div, getrandom, random = math.div, utilities.randomizer.get, math.random

local trace_casing = false  trackers.register("typesetters.casing", function(v) trace_casing = v end)

local report_casing = logs.reporter("typesetting","casing")

local nodes, node = nodes, node

local nuts            = nodes.nuts

local getnext         = nuts.getnext
local getid           = nuts.getid
local getattr         = nuts.getattr
local getcharspec     = nuts.getcharspec
local getsubtype      = nuts.getsubtype
local getchar         = nuts.getchar
local isglyph         = nuts.isglyph
local getdisc         = nuts.getdisc

local setchar         = nuts.setchar
local setfont         = nuts.setfont
local setscales       = nuts.setscales
local setweight       = nuts.setweight

local copy_node       = nuts.copy
local endofmath       = nuts.endofmath
local insertafter     = nuts.insertafter
local findattribute   = nuts.findattribute
----- unsetattributes = nuts.unsetattributes

local nextchar        = nuts.traversers.char -- we prevent processing twice in case
local ischar          = nuts.ischar          -- unboxing as it can reverse casing

local nodecodes       = nodes.nodecodes
local kerncodes       = nodes.kerncodes

local glyph_code      <const> = nodecodes.glyph
local kern_code       <const> = nodecodes.kern
local disc_code       <const> = nodecodes.disc
local math_code       <const> = nodecodes.math

local fontkern_code   <const> = kerncodes.fontkern

local enableaction    = nodes.tasks.enableaction

local newkern         = nuts.pool.kern

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

local currentfont     = font.current

local variables       = interfaces.variables
local v_reset         <const> = variables.reset

local texsetattribute = tex.setattribute
local texgetattribute = tex.getattribute
local texgetscales    = tex.getglyphscales

local integer_value   <const> = tokens.values.integer

typesetters           = typesetters or { }
local typesetters     = typesetters

typesetters.cases     = typesetters.cases or { }
local cases           = typesetters.cases

cases.actions         = { }
local actions         = cases.actions

local a_cases         <const> = attributes.private("case")

local run             = 0 -- a trick to make neighbouring ranges work

local registervalue   = attributes.registervalue
local getvalue        = attributes.getvalue

local function set(tag,font,category,xscale,yscale,weight)
    run = run + 1
    local scales = { texgetscales() }
    local settings = {
        font     = font,
        tag      = tag,
        run      = run, -- still needed ?
        category = category or 0,
        scales   = scales,
        weight   = weight,
    }
    if xscale and xscale ~= 1000 then
        scales[2] = scales[2] * xscale / 1000
    end
    if yscale and yscale ~= 1000 then
        scales[3] = scales[3] * yscale / 1000
    end
    texsetattribute(a_cases,registervalue(a_cases,settings))
end

-- a previous implementation used char(0) as placeholder for the larger font, so we needed
-- to remove it before it can do further harm ... that was too tricky as we use char 0 for
-- other cases too
--
-- we could do the whole glyph run here (till no more attributes match) but then we end up
-- with more code .. maybe i will clean this up anyway as the lastfont hack is somewhat ugly
-- ... on the other hand, we need to deal with cases like:
--
-- \WORD {far too \Word{many \WORD{more \word{pushed} in between} useless} words}

local uccodes    = characters.uccodes
local lccodes    = characters.lccodes
local categories = characters.categories

-- true false true == mixed

local function replacer(start,codes)
    local char, fnt = isglyph(start)
    local dc = codes[char]
    if dc then
        local ifc = fontchar[fnt]
        if type(dc) == "table" then
            for i=1,#dc do
                if not ifc[dc[i]] then
                    return start, false
                end
            end
            for i=#dc,1,-1 do
                local chr = dc[i]
                if i == 1 then
                    setchar(start,chr)
                else
                    local g = copy_node(start)
                    setchar(g,chr)
                    insertafter(start,start,g)
                end
            end
        elseif ifc[dc] then
            setchar(start,dc)
        end
    end
    return start
end

local registered, n = { }, 0

local function register(name,f)
    if type(f) == "function" then
        n = n + 1
        actions[n] = f
        registered[name] = n
        return n
    else
        local n = registered[f]
        registered[name] = n
        return n
    end
end

cases.register = register

local function WORD(start,data,lastfont,n,count,where,first)
    lastfont[n] = false
    return replacer(first or start,uccodes)
end

local function word(start,data,lastfont,n,count,where,first)
    lastfont[n] = false
    return replacer(first or start,lccodes)
end

local function Words(start,data,lastfont,n,count,where,first) -- looks quite complex
    if where == "post" then
        return
    end
    if count == 1 and where ~= "post" then
        replacer(first or start,uccodes)
        return start, true
    else
        return start, true
    end
end

local function Word(start,data,lastfont,n,count,where,first)
    data.blocked = true
    return Words(start,data,lastfont,n,count,where,first)
end

local function camel(start,data,lastfont,n,count,where,first)
    word(start,data,lastfont,n,count,where,first)
    Words(start,data,lastfont,n,count,where,first)
    return start, true
end

local function mixed(start,data,lastfont,n,count,where,first,keep)
    if where == "post" then
        return
    end
    local used = first or start
    local char = getchar(used)
    local dc   = uccodes[char]
    if not dc then
        -- quit
    elseif dc == char then
        local lfa = lastfont[n]
        if lfa then
            local s = data.scales
            setfont(used,lfa)
            if s then
                setscales(used,s[1],s[2],s[3])
            end
        end
    elseif not keep then
        replacer(used,uccodes)
    end
    return start, true
end

local function Camel(start,data,lastfont,n,count,where,first)
    return mixed(start,data,lastfont,n,count,where,first,true)
end

local function Capital(start,data,lastfont,n,count,where,first,once) -- 3
    local used = first or start
    if count == 1 and where ~= "post" then
        local lfa = lastfont[n]
        if lfa then
            local char = getchar(used)
            local dc   = uccodes[char]
            if dc then
                local s = data.scales
                setfont(used,lfa)
                if s then
                    local weight = data.weight or 0
                    if weight ~= 0 then
                        setweight(used,weight)
                    end
                    weight = 1 + weight / 1000
                    setscales(used,s[1],weight*s[2],s[3])
                end
            end
        end
    end
    local s, c = replacer(first or start,uccodes)
    if once then
        lastfont[n] = false -- here
    end
    return start, c
end

local function capital(start,data,lastfont,n,where,count,first,count)
    return Capital(start,data,lastfont,n,where,count,first,true)
end

local function fakecaps(start,data,lastfont,n,where,count,first,count)
    local used = first or start
    local lfa = lastfont[n]
    if lfa then
        local char = getchar(used)
        local kind = categories[char]
        if kind ~= "lu" then
            local dc = uccodes[char]
            if dc then
                local s = data.scales
                setfont(used,lfa)
                if s then
                    local weight = data.weight or 0
                    if weight ~= 0 then
                        nuts.setweight(used,weight)
                    end
                    weight = 1 + weight / 1000
                    setscales(used,s[1],weight*s[2],s[3])
                end
                return replacer(first or start,uccodes)
            end
        end
    end
    return start, false
end

local function none(start,data,lastfont,n,count,where,first)
    return start, true
end

local function randomized(start,data,lastfont,n,count,where,first)
    local used  = first or start
    local char,
          font  = getcharfont(used)
    local tfm   = fontchar[font]
    lastfont[n] = false
    local kind  = categories[char]
    if kind == "lu" then
        while true do
            local n = getrandom("capital lu",0x41,0x5A)
            if tfm[n] then -- this also intercepts tables
                setchar(used,n)
                return start
            end
        end
    elseif kind == "ll" then
        while true do
            local n = getrandom("capital ll",0x61,0x7A)
            if tfm[n] then -- this also intercepts tables
                setchar(used,n)
                return start
            end
        end
    end
    return start
end

register(variables.WORD,      WORD)       --   1
register(variables.word,      word)       --   2
register(variables.Word,      Word)       --   3
register(variables.Words,     Words)      --   4
register(variables.capital,   capital)    --   5
register(variables.Capital,   Capital)    --   6
register(variables.none,      none)       --   7 (dummy)
register(variables.random,    randomized) --   8
register(variables.mixed,     mixed)      --   9
register(variables.camel,     camel)      --  10
register(variables.Camel,     Camel)      --  11
register(variables.fakecaps,  fakecaps)   --  12

register(variables.cap, variables.capital) -- clone
register(variables.Cap, variables.Capital) -- clone

function cases.handler(head)
    local _, start = findattribute(head,a_cases)
    if start then
        local lastfont = { }
        local lastattr = nil
        local count    = 0
        while start do -- while because start can jump ahead
         -- local id = getid(start)
         -- if id == glyph_code then
            local char, id = ischar(start)
            if char then
                local attr = getattr(start,a_cases)
                if attr and attr > 0 then
                    local data = getvalue(a_cases,attr)
                    if data and not data.blocked then
                        if attr ~= lastattr then
                            lastattr = attr
                            count    = 1
                        else
                            count    = count + 1
                        end
                        local tag    = data.tag
                        local font   = data.font
                        local run    = data.run
                        local action = actions[tag] -- map back to low number
                        lastfont[tag] = font
                        if action then
                            local quit
                            start, quit = action(start,data,lastfont,tag,count)
                            if trace_casing then
                                report_casing("case trigger %a, instance %a, fontid %a, character %c, result %a",
                                    tag,run,font,char,quit and "-" or "+")
                            end
                        elseif trace_casing then
                            report_casing("unknown case trigger %a",tag)
                        end
                    end
                end
            elseif id == disc_code then
                local attr = getattr(start,a_cases)
                if attr and attr > 0 then
                    local data = getvalue(a_cases,attr)
                    if data and not data.blocked then
                        if attr ~= lastattr then
                            lastattr = attr
                            count    = 0
                        end
                        local tag    = data.tag
                        local font   = data.font
                        local action = actions[tag] -- map back to low number
                        lastfont[tag] = font
                        if action then
                            local pre, post, replace = getdisc(start)
                            if replace then
                                local cnt = count
                                for g in nextchar, replace do
                                    cnt = cnt + 1
                                    getattr(g,a_cases) -- why no check
                                    local h, quit = action(start,data,lastfont,tag,cnt,"replace",g)
                                    if quit then
                                        break
                                    end
                                end
                            end
                            if pre then
                                local cnt = count
                                for g in nextchar, pre do
                                    cnt = cnt + 1
                                    getattr(g,a_cases) -- why no check
                                    local h, quit = action(start,data,lastfont,tag,cnt,"pre",g)
                                    if quit then
                                        break
                                    end
                                end
                            end
                            if post then
                                local cnt = count
                                for g in nextchar, post do
                                    cnt = cnt + 1
                                    getattr(g,a_cases) -- why no check
                                    local h, quit = action(start,data,lastfont,tag,cnt,"post",g)
                                    if quit then
                                        break
                                    end
                                end
                            end
                        end
                        count = count + 1
                    end
                end
            elseif id == math_code then
                start = endofmath(start)
                count = 0
            elseif id == kern_code and getsubtype(start) == fontkern_code then
                -- still inside a word ...normally kerns are added later but we can pass
                -- twice as in an unhbox .. brrr .. is now catched but not yet in
                -- disc ...
            else
                count = 0
            end
            if start then
                start = getnext(start)
            end
        end
    end
    return head
end

local enabled = false

local function setcases(n,id,category,xscale,yscale,weight)
    if n ~= v_reset then
        n = registered[n] or tonumber(n) or tonumber(category)
        if n then
            if not enabled then
                enableaction("processors","typesetters.cases.handler")
                if trace_casing then
                    report_casing("enabling case handler")
                end
                enabled = true
            end
            set(n,id or currentfont(),category,xscale,yscale,weight)
            return
        end
    end
    texsetattribute(a_cases)
end

cases.set = setcases

-- interface

interfaces.implement {
    name      = "setcharactercasing",
    actions   = function(name,category,xscale,yscale,weight)
        setcases(name,false,category,xscale,yscale,weight)
    end,
    arguments = { "argument", "integer", "integer", "integer", "integer" },
}

interfaces.implement {
    name      = "getcharactercasingcategory",
    public    = true,
    usage     = "value",
    actions   = function()
        local v = getvalue(a_cases,texgetattribute(a_cases))
        return integer_value, v and v.category or 0
    end,
}

-- An example of a special plug, see type-imp-punk for usage.

cases.register("randomvariant", function(start)
    local char, fnt = isglyph(start)
    local data = fontchar[fnt][char]
    if data then
        local variants = data.variants
        if variants then
            local n = #variants
            local i = getrandom("variant",1,n+1)
            if i > n then
                -- we keep the original
            else
                setchar(start,variants[i])
            end
            return start, true
        end
    end
    return start, false
end)
