--- Fonthandling after fontloading.
--
--  fonts.lua
--  speedata publisher
--
--  For a list of authors see `git blame'
--  See file COPYING in the root directory for license info.
--
--- Loading a font is only one part of the story. Proper dealing with fonts
--- requires post processing at various stages.
file_start("fonts.lua")

local fontloader_mod = require("fonts.fontloader")

local colors_module = require("publisher.colors")

local M = {}

local lookup_fontname_filename={}
local font_instances={}

M.used_fonts = {}
local used_fonts = M.used_fonts

-- Tracks missing-glyph reports: missing_glyph_counts[font_name][code] = count.
-- Filled by report_missing_glyph(); flushed by summarize_missing_glyphs() at
-- the end of the run. We emit the warning only on first occurrence and then
-- print a "(N times)" summary for repeats, instead of flooding the log.
M.missing_glyph_counts = {}

local glue_node      = node.id("glue")
local glyph_node     = node.id("glyph")
local disc_node      = node.id("disc")
local rule_node      = node.id("rule")
local dir_node       = node.id("dir")
local kern_node      = node.id("kern")
local penalty_node   = node.id("penalty")
local whatsit_node   = node.id("whatsit")
local hlist_node     = node.id("hlist")
local vlist_node     = node.id("vlist")


local user_defined_whatsit
local pdf_refximage_whatsit
local pdf_action_whatsit
local pdf_dest_whatsit
for k,v in pairs(node.whatsits()) do
    if v == "user_defined" then
        -- for mark command
        user_defined_whatsit = k
    elseif v == "pdf_refximage" then
        pdf_refximage_whatsit = k
    elseif v == "pdf_action" then
        pdf_action_whatsit = k
    elseif v == "pdf_dest" then
        pdf_dest_whatsit = k
    end
end


--- Every font family ("text", "Chapter"), that is defined by DefineFontfamily
--- gets an internal number. This number is stored here.
M.lookup_fontfamily_name_number = {}
local lookup_fontfamily_name_number = M.lookup_fontfamily_name_number

--- Every font family (given by number) has variants like italic, bold etc.
--- These are stored as a table in this table.
--- The following keys are stored
---
---  * normal
---  * bolditalic
---  * italic
---  * bold
---  * baselineskip
---  * size
M.lookup_fontfamily_number_instance = {}
local lookup_fontfamily_number_instance = M.lookup_fontfamily_number_instance


function M.load_fontfile(name, filename, parameter_tab)
    assert(filename)
    assert(name)
    lookup_fontname_filename[name]={filename,parameter_tab or {}}
    return true
end

function table.find(tab,key)
    assert(tab)
    assert(key)
    local found
    for k_tab,v_tab in pairs(tab) do
        if type(key)=="table" then
            found = true
            for k_key,v_key in pairs(key) do
                if k_tab[k_key]~= v_key then found = false end
            end
            if found==true then
                return v_tab
            end
        end
    end
    return nil
end

local preloaded_fonts = {}


-- called from html.lua
function M.get_fontname( localname, url )
    localname = publisher.get_fontname(localname)
    -- w("get_fontname, localname %q",tostring(localname))
    if localname and lookup_fontname_filename[localname] then
        return localname
    elseif url then
        M.load_fontfile(url,url)
        return url
    end
    return nil
end

-- Return false, error message in case of failure, true, number otherwise. number
-- is the internal font number. After calling this method, the font can be used
-- with the key {filename,size}
function M.make_font_instance( name,size )
    -- Name is something like "TeXGyreHeros-Regular", the visible name of the font file
    assert(name)
    assert(tonumber(size))
    if not lookup_fontname_filename[name] then
        local msg = string.format("Font instance '%s' is not defined!", name)
        main.log("error","Make font instance: filename is not defined","filename",name)
        return false, msg
    end
    local filename,parameter = table.unpack(lookup_fontname_filename[name])
    assert(filename)
    local k = {filename = filename, fontsize = size, space = parameter.space, mode = parameter.mode or publisher.options.fontloader,fallbacks = parameter.fallbacks}

    if parameter.otfeatures then
        for fea,enabled in pairs(parameter.otfeatures) do
            if enabled then
                k[fea] = true
            end
        end
    end
    local fontnumber = table.find(font_instances,k)
    if fontnumber then
        return true,fontnumber
    else
        local f
        local num = font.nextid(true)
        f = fontloader_mod.preload_font(filename,size,parameter,parameter.mode or publisher.options.fontloader)
        f.reserved_num = num
        preloaded_fonts[num] = f
        main.log("debug","Preload font","name",filename,"size",tostring(math.round(size / publisher.factor,3)),"id",tostring(num))
        font_instances[k]=num
        return true, num
    end
    return false, "Internal error"
end

-- Define font from preloaded font
function M.define_font(instance)
    local mode = instance.requested_mode
    local num = instance.reserved_num
    main.log("info","Create font metrics","name",instance.requested_name,"size",math.round(instance.requested_size / publisher.factor,3),"id",num,"mode",mode)
    local f, ok
    if mode == "harfbuzz" then
        ok,f = fontloader_mod.define_font_hb(instance.requested_name,instance.requested_size,instance.requested_extra_parameter)
    else
        ok,f = fontloader_mod.define_font(instance.requested_name,instance.requested_size,instance.requested_extra_parameter)
    end
    if not ok then
        main.log("error","Failed to load font","requested name",instance.requested_name,"errormessage",f or "")
        return false
    end
    preloaded_fonts[num] = f
    used_fonts[num]=f
    font.define(num,f)
    return true
end

-- Return instance number from fontfamily number and instance name
function M.get_fontinstance(fontfamily,instancename)
    local instance
    if fontfamily and fontfamily > 0 then
        instance = lookup_fontfamily_number_instance[fontfamily][instancename]
    else
        instance = 1
    end
    if not instance then
        err("font %s not found for family %s",instancename,fontfamily)
        -- let's try "regular"
        if fontfamily and fontfamily > 0 then
            instance = lookup_fontfamily_number_instance[fontfamily].normal
        end
        if not instance then
            instance = 1
        end
    end
    local pe = preloaded_fonts[instance]
    if pe.loaded == false then
        local ok = M.define_font(pe)
        if not ok then
            return M.get_fontinstance(1,"normal")
        end
    end
    return instance
end

--- At this time we must adjust the contents of the paragraph how we would
--- like it. For example the (sub/sup)script glyphs still have the width of
--- the regular characters and need
-- node.direct locals for pre_linebreak hot loop
local d              = node.direct
local d_todirect     = d.todirect
local d_tonode       = d.tonode
local d_getnext      = d.getnext
local d_getprev      = d.getprev
local d_getid        = d.getid
local d_getsubtype   = d.getsubtype
local d_getchar      = d.getchar
local d_setchar      = d.setchar
local d_getfont      = d.getfont
local d_getlist      = d.getlist
local d_getleader    = d.getleader
local d_has_attribute = d.has_attribute
local d_set_attribute = d.set_attribute
local d_setfield     = d.setfield
local d_getfield     = d.getfield
local d_setnext      = d.setnext
local d_setprev      = d.setprev
local d_new          = d.new
local d_insert_after = d.insert_after
local d_insert_before = d.insert_before
local d_vpack        = d.vpack
local d_hpack        = d.hpack
local d_tail         = d.tail
local d_getproperty  = d.getproperty
local d_setproperty  = d.setproperty

-- Pre-resolved attribute numbers for pre_linebreak hot loop
local plb_att_fontfamily
local plb_att_fontstyle
local plb_att_fontweight
local plb_attval_italic  -- index of "italic" in font-style table
local plb_attval_bold    -- index of "bold" in font-weight table

local function pre_linebreak_direct( head )
    -- Cache for consecutive same-font glyphs
    local cache_ff, cache_fs, cache_fw
    local cache_fontnum, cache_is_fontforge, cache_f
    while head do
        local id = d_getid(head)
        if id == hlist_node then
            pre_linebreak_direct(d_getlist(head))
        elseif id == vlist_node then
            pre_linebreak_direct(d_getlist(head))
        elseif id == rule_node then
            -- ignore
        elseif id == dir_node then
            -- ignore
        elseif id == disc_node then
            local pre, post, replace = d.getdisc(head)
            pre_linebreak_direct(pre)
            pre_linebreak_direct(post)
            pre_linebreak_direct(replace)
        elseif id == whatsit_node then
            if d_getsubtype(head) == pdf_dest_whatsit then
                local dest_fontfamily = d_has_attribute(head, plb_att_fontfamily)
                if dest_fontfamily then
                    local tmpnext = d_getnext(head)
                    local tmpprev = d_getprev(head)
                    d_setnext(head, nil)
                    d_setprev(head, nil)
                    local instance = lookup_fontfamily_number_instance[dest_fontfamily]
                    local f = used_fonts[instance.normal]
                    local g = d_todirect(publisher.make_glue({width = f.size}))

                    local h = d_insert_after(head, head, g)
                    h = d_vpack(h)

                    if tmpprev then
                        d_setnext(tmpprev, h)
                        d_setprev(h, tmpprev)
                    end
                    if tmpnext then
                        d_setprev(tmpnext, h)
                        d_setnext(h, tmpnext)
                    end
                end
            end
        elseif id == glue_node then
            if d_getsubtype(head) == 100 then -- leader
                local l = d_getleader(head)
                local wd = d_has_attribute(l, publisher.att_leaderwd)

                -- Set the font for the leader
                pre_linebreak_direct(l)

                local tmpbox
                if wd == -1 then
                    tmpbox = d_hpack(l)
                else
                    -- \hbox{ 1fil, text, 1fil }
                    local l1 = d_todirect(set_glue(nil,{width = 0, stretch = 2^16, stretch_order = 2, shrink = 2^16, shrink_order = 2}))
                    local l2 = d_todirect(set_glue(nil,{width = 0, stretch = 2^16, stretch_order = 2, shrink = 2^16, shrink_order = 2}))
                    local newhead = d_insert_before(l, l, l1)
                    local endoftext = d_tail(l)
                    newhead = d_insert_after(newhead, endoftext, l2)
                    tmpbox = d_hpack(newhead, wd, "exactly")
                end
                d_set_attribute(tmpbox, publisher.att_leaderwd, wd)
                d_setfield(head, "leader", tmpbox)
            end
        elseif id == kern_node then -- kern
        elseif id == penalty_node then -- penalty
        elseif id == glyph_node then
            local ff = d_has_attribute(head, plb_att_fontfamily)
            if ff then
                -- not local, so that we can access fontfamily later
                fontfamily = ff

                -- Last resort
                if fontfamily == 0 then fontfamily = 1 warning("Undefined fontfamily, set fontfamily to 1") end

                local fontstyle = d_has_attribute(head, plb_att_fontstyle)
                local fontweight = d_has_attribute(head, plb_att_fontweight)

                if ff ~= cache_ff or fontstyle ~= cache_fs or fontweight ~= cache_fw then
                    local instancename
                    if fontstyle == plb_attval_italic and fontweight ~= plb_attval_bold then
                        instancename = "italic"
                    elseif fontstyle == plb_attval_italic and fontweight == plb_attval_bold then
                        instancename = "bolditalic"
                    elseif fontweight == plb_attval_bold then
                        instancename = "bold"
                    else
                        instancename = "normal"
                    end
                    cache_fontnum = M.get_fontinstance(fontfamily, instancename)
                    cache_f = used_fonts[cache_fontnum]
                    cache_is_fontforge = cache_f and cache_f.mode == "fontforge" and cache_f.otfeatures
                    cache_ff = ff
                    cache_fs = fontstyle
                    cache_fw = fontweight
                end

                -- check for font features (fontforge mode only)
                if cache_is_fontforge then
                    local f = cache_f
                    local headchar = d_getchar(head)
                    for _,featuretable in ipairs(f.otfeatures) do
                        local glyphno,lookups
                        local glyph_lookuptable
                        if f.characters[headchar] then
                            glyphno = f.characters[headchar].index
                            lookups = f.fontloader.glyphs[glyphno].lookups
                            for _,v in ipairs(featuretable) do
                                if lookups then
                                    glyph_lookuptable = lookups[v]
                                    if glyph_lookuptable then
                                        local glt1 = glyph_lookuptable[1]
                                        if glt1.type == "substitution" then
                                            d_setchar(head, f.fontloader.lookup_codepoint_by_name[glt1.specification.variant])
                                            headchar = d_getchar(head)
                                        elseif glt1.type == "multiple" then
                                            for i,comp in ipairs(string.explode(glt1.specification.components)) do
                                                if i==1 then
                                                    d_setchar(head, f.fontloader.lookup_codepoint_by_name[comp])
                                                    headchar = d_getchar(head)
                                                else
                                                    local n = d_new(glyph_node)
                                                    d_setnext(n, d_getnext(head))
                                                    d_setfield(n, "font", cache_fontnum)
                                                    d_setfield(n, "lang", 0)
                                                    d_setchar(n, f.fontloader.lookup_codepoint_by_name[comp])
                                                    d_setnext(head, n)
                                                    head = n
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
        else
            warning("Unknown node: %q", d_getid(head))
        end
        head = d_getnext(head)
    end
    return true
end

function M.pre_linebreak( head )
    if not plb_att_fontfamily then
        plb_att_fontfamily = publisher.attribute_name_number["fontfamily"]
        plb_att_fontstyle  = publisher.attribute_name_number["font-style"]
        plb_att_fontweight = publisher.attribute_name_number["font-weight"]
        local fs = publisher.attributes["font-style"]
        for i,v in ipairs(fs) do if v == "italic" then plb_attval_italic = i break end end
        local fw = publisher.attributes["font-weight"]
        for i,v in ipairs(fw) do if v == "bold" then plb_attval_bold = i break end end
    end
    return pre_linebreak_direct(d_todirect(head))
end

function M.insert_backgroundcolor( parent, head, start, bgcolorindex, bg_padding_top, bg_padding_bottom, reverse )
    reverse = reverse or false
    bg_padding_top    = bg_padding_top or 0
    bg_padding_bottom = bg_padding_bottom or 0
    local wd = node.dimensions(parent.glue_set,parent.glue_sign, parent.glue_order,start,head)
    local ht = parent.height
    local dp = parent.depth

    local colorname = colors_module.colortable[bgcolorindex]
    local pdfstring = colors_module.colors[colorname].pdfstring

    -- wd, ht and dp are now in pdf points
    wd = wd / publisher.factor
    ht = ht / publisher.factor
    dp = dp / publisher.factor
    bg_padding_top    = bg_padding_top    / publisher.factor
    bg_padding_bottom = bg_padding_bottom / publisher.factor
    local rule = node.new("whatsit","pdf_literal")
    if reverse then wd = wd * -1 end
    rule.data = string.format("q %s 0 %g %g %g re f Q", pdfstring, -dp - bg_padding_bottom ,  wd, ht + dp + bg_padding_top + bg_padding_bottom )
    rule.mode = 0
    parent.head = node.insert_before(parent.head,start,rule)
    return rule
end

--- Insert a horizontal rule in the nodelist that is used for underlining. typ is 1 (solid) or 2 (dashed)
function M.insert_underline( parent, head, start, typ, style, colornumber)
    colornumber = colornumber or 1
    if colornumber == 0 then colornumber = 1 end
    local wd = node.dimensions(parent.glue_set,parent.glue_sign, parent.glue_order,start,head)
    local ht = parent.height
    local dp = parent.depth
    local dashpattern = ""
    local pdfstring = colors_module.pdfstring_from_color(colornumber)

    -- wd, ht and dp are now in pdf points
    wd = wd / publisher.factor
    ht = ht / publisher.factor
    dp = dp / publisher.factor
    local rule = node.new("whatsit","pdf_literal")
    publisher.setprop(rule,"origin","insert_underline")
    -- thickness: ht / ...
    -- downshift: dp/2
    local rule_width = math.round(ht / 13,3)
    if style == "dashed" then
        dashpattern = string.format("[%g] 0 d", 3 * rule_width)
    end

    local shift_down = ( dp - rule_width ) / 1.5
    if typ == "line-through" then
        shift_down = - 1.6 * shift_down
    end
    rule.data = string.format("q %s %g w %s 0 %g m %g %g l S Q", pdfstring, rule_width, dashpattern, -1 * shift_down, -wd, -1 * shift_down )
    rule.mode = 0
    local attribs = publisher.get_attributes(start)
    publisher.set_attributes(rule,attribs)
    parent.head = node.insert_before(parent.head,head,rule)
    return rule
end

--- In the post_linebreak function we manipulate the paragraph that doesn't
--- affect it's typesetting. Underline and 'showhyphens' is done here. The
--- overall appearance of the paragraph is fixed at this time, we can only add
--- decoration now.
do
    local curdir = {}
    local plb_attnum_td_line
    local plb_attnum_td_style
    local plb_attnum_td_color
    local plb_attnum_bgcolor
    local plb_attnum_bgpad_top
    local plb_attnum_bgpad_bottom

    local function post_linebreak_direct( head, list_head_d )
        local insert_bgcolor = M.insert_backgroundcolor
        local insert_ul = M.insert_underline
        local opts = publisher.options
        local underlinetype = nil
        local underlinestyle = nil
        local start_underline = nil
        local underline_color = nil
        local bgcolorindex = nil
        local start_bgcolor = nil
        local bgcolor_reverse = false
        local bg_padding_top = 0
        local bg_padding_bottom = 0
        local reportmissingglyphs = opts.reportmissingglyphs
        local lasthead = nil
        local fast_path = not (opts.showhyphenation or opts.showkerning or reportmissingglyphs)
        while head do
            local id = d_getid(head)
            local props = d_getproperty(head)
            if props then
                local pd = props.pardir
                if pd and #curdir == 0 then
                    curdir = {pd}
                end
            end
            if id == hlist_node then
                post_linebreak_direct(d_getlist(head), head)
            elseif id == vlist_node then
                post_linebreak_direct(d_getlist(head), head)
            elseif id == dir_node then
                local dirval = d_getfield(head, "dir")
                local mode = string.sub(dirval,1,1)
                local texdir = string.sub(dirval,2,4)
                local ldir
                if texdir == "TLT" then ldir = "ltr" else ldir = "rtl" end
                if mode == "+" then
                    curdir[#curdir + 1] = ldir
                elseif mode == "-" then
                    local x = curdir[#curdir]
                    curdir[#curdir] = nil
                    if x ~= ldir then
                        warning("paragraph direction incorrect, found %s, expected %s",ldir,x)
                    end
                end
                if start_bgcolor then
                    insert_bgcolor(d_tonode(list_head_d), d_tonode(head), d_tonode(start_bgcolor), bgcolorindex,bg_padding_top,bg_padding_bottom,bgcolor_reverse)
                    start_bgcolor = nil
                end
            elseif id == disc_node then
                if opts.showhyphenation then
                    local n = node.new("whatsit","pdf_literal")
                    n.mode = 0
                    n.data = "q 0.3 w 0 2 m 0 7 l S Q"
                    node.insert_before(d_tonode(list_head_d), d_tonode(head), n)
                end
            elseif id == kern_node then
                if fast_path and not start_underline and not start_bgcolor then
                    goto continue_loop
                end
                local ul = d_has_attribute(head, plb_attnum_td_line)
                local bgcolor = d_has_attribute(head, plb_attnum_bgcolor)
                if ul == nil then
                    if start_underline then
                        insert_ul(d_tonode(list_head_d), d_tonode(head), d_tonode(start_underline),underlinetype,underlinestyle,underline_color)
                        start_underline = nil
                    end
                end
                if bgcolor == nil then
                    if start_bgcolor then
                        insert_bgcolor(d_tonode(list_head_d), d_tonode(head), d_tonode(start_bgcolor),bgcolorindex,bg_padding_top,bg_padding_bottom,bgcolor_reverse)
                        start_bgcolor = nil
                    end
                end
                if opts.showkerning then
                    local n = node.new("whatsit","pdf_literal")
                    n.mode = 0
                    n.data = "q .4 G 0.3 w 0 2 m 0 7 l S Q"
                    node.insert_before(d_tonode(list_head_d), d_tonode(head), n)
                end
            elseif id == glue_node then
                -- Fast path: skip when no decoration/bgcolor active or pending
                if fast_path and not start_underline and not start_bgcolor then
                    if not d_has_attribute(head, plb_attnum_td_line) and not d_has_attribute(head, plb_attnum_bgcolor) then
                        goto continue_loop
                    end
                end
                local ul = d_has_attribute(head, plb_attnum_td_line)
                local bgcolor = d_has_attribute(head, plb_attnum_bgcolor)

                -- at rightskip we must underline (if start exists)
                if ul == nil or d_getsubtype(head) == 9 then
                    if start_underline then
                        insert_ul(d_tonode(list_head_d), d_tonode(head), d_tonode(start_underline),underlinetype,underlinestyle,underline_color)
                        start_underline = nil
                    end
                end
                if bgcolor and bgcolor > 0 and not start_bgcolor then
                    bgcolor_reverse = ( curdir[#curdir] == "rtl" )
                    bgcolorindex = bgcolor
                    start_bgcolor = head
                    bg_padding_top = d_has_attribute(head, plb_attnum_bgpad_top)
                    bg_padding_bottom = d_has_attribute(head, plb_attnum_bgpad_bottom)
            elseif bgcolor == nil or d_getsubtype(head) == 9 then -- 9 == rightskip
                    if start_bgcolor then
                        insert_bgcolor(d_tonode(list_head_d), d_tonode(head), d_tonode(start_bgcolor),bgcolorindex,bg_padding_top,bg_padding_bottom,bgcolor_reverse)
                        start_bgcolor = nil
                    end
                end
            elseif id == glyph_node then
                local ul = d_has_attribute(head, plb_attnum_td_line)
                local bgcolor = d_has_attribute(head, plb_attnum_bgcolor)
                -- Fast path: skip when no decoration/bgcolor active or pending
                if fast_path and not ul and not bgcolor and not start_underline and not start_bgcolor then
                    goto continue_loop
                end
                if reportmissingglyphs then
                    local thisfont = used_fonts[d_getfont(head)]
                    if thisfont and not thisfont.characters[d_getchar(head)] then
                        local lvl = reportmissingglyphs == "warning" and "warn" or "error"
                        M.report_missing_glyph(lvl, thisfont.name, d_getchar(head))
                    end
                end
                if ul then
                    if not start_underline then
                        underlinetype = ul
                        underlinestyle = d_has_attribute(head, plb_attnum_td_style)
                        start_underline = head
                        underline_color = d_has_attribute(head, plb_attnum_td_color)
                    end
                else
                    if start_underline then
                        insert_ul(d_tonode(list_head_d), d_tonode(head), d_tonode(start_underline), underlinetype,underlinestyle,underline_color)
                        start_underline = nil
                    end
                end
                if bgcolor and bgcolor > 0 then
                    if not start_bgcolor then
                        bgcolorindex = bgcolor
                        bg_padding_top    = d_has_attribute(head, plb_attnum_bgpad_top)
                        bg_padding_bottom = d_has_attribute(head, plb_attnum_bgpad_bottom)
                        start_bgcolor = head
                        bgcolor_reverse = ( curdir[#curdir] == "rtl" )
                    end
                else
                    if start_bgcolor then
                        insert_bgcolor(d_tonode(list_head_d), d_tonode(head), d_tonode(start_bgcolor), bgcolorindex,bg_padding_top,bg_padding_bottom,bgcolor_reverse)
                        start_bgcolor = nil
                    end
                end
            end
            ::continue_loop::
            lasthead = head
            head = d_getnext(head)
        end
        if start_bgcolor then
            local _, dummy = publisher.add_rule(d_tonode(lasthead),"tail",{width = 0, height = 0, depth = 0},"bgcolor dummy")
            insert_bgcolor(d_tonode(list_head_d), dummy, d_tonode(start_bgcolor), bgcolorindex,bg_padding_top,bg_padding_bottom,bgcolor_reverse)
        end
        return head
    end

    function M.post_linebreak( head, list_head )
        if not plb_attnum_td_line then
            plb_attnum_td_line    = publisher.attribute_name_number["text-decoration-line"]
            plb_attnum_td_style   = publisher.attribute_name_number["text-decoration-style"]
            plb_attnum_td_color   = publisher.attribute_name_number["text-decoration-color"]
            plb_attnum_bgcolor    = publisher.attribute_name_number["background-color"]
            plb_attnum_bgpad_top  = publisher.attribute_name_number["bgpaddingtop"]
            plb_attnum_bgpad_bottom = publisher.attribute_name_number["bgpaddingbottom"]
        end
        return post_linebreak_direct(d_todirect(head), list_head and d_todirect(list_head))
    end
end

-- fam is a number
function M.clone_family( fam, params )
    -- fam_tbl = {
    --   ["baselineskip"] = "789372"
    --   ["name"] = "text"
    --   ["normalscript"] = "10"
    --   ["scriptsize"] = "526248"
    --   ["normal"] = "9"
    --   ["size"] = "657810"
    -- },
    local fam_tbl = lookup_fontfamily_number_instance[fam]
    local newfam = {}
    for k,v in pairs(fam_tbl) do
        newfam[k] = v
    end
    newfam.name = "cloned"

    if newfam.fontfaceregular then
        local ok,r = M.make_font_instance(newfam.fontfaceregular, params.size * newfam.size )
        if not ok then
            err(r)
            return fam
        end
        newfam.normal = r
    end

    if newfam.fontfacebold then
        local ok,b = M.make_font_instance(newfam.fontfacebold, params.size * newfam.size )
        if not ok then
            err(b)
            return fam
        end
        newfam.bold = b
    end

    if newfam.fontfaceitalic then
        local ok,i = M.make_font_instance(newfam.fontfaceitalic, params.size * newfam.size )
        if not ok then
            err(i)
            return fam
        end
        newfam.italic = i
        end

    if newfam.fontfacebolditalic then
        local ok,bi = M.make_font_instance(newfam.fontfacebolditalic, params.size * newfam.size )
        if not ok then
            err(bi)
            return fam
        end
        newfam.bolditalic = bi
    end

    newfam.size = math.floor(params.size * newfam.size)
    lookup_fontfamily_number_instance[#lookup_fontfamily_number_instance + 1] = newfam
    return #lookup_fontfamily_number_instance
end

--- Report a missing glyph, deduplicated by (font, code).
--- The first occurrence is logged immediately at `level` ("warn" or "error");
--- subsequent occurrences only increment the counter. summarize_missing_glyphs()
--- emits a "(N times)" summary line for codes seen more than once.
--- Extra arguments are passed through to the immediate log call only.
function M.report_missing_glyph(level, font_name, code, ...)
    local per_font = M.missing_glyph_counts[font_name]
    if not per_font then
        per_font = {}
        M.missing_glyph_counts[font_name] = per_font
    end
    local prev = per_font[code]
    if prev then
        per_font[code] = prev + 1
        return
    end
    per_font[code] = 1
    main.log(level, "Glyph is missing from the font",
        "font", font_name,
        "glyph_hex", string.format("%04x", code),
        ...)
end

function M.summarize_missing_glyphs()
    local rows = {}
    for font_name, per_font in next, M.missing_glyph_counts do
        for code, count in next, per_font do
            if count > 1 then
                rows[#rows + 1] = { font = font_name, code = code, count = count }
            end
        end
    end
    if #rows == 0 then return end
    table.sort(rows, function(a, b)
        if a.count ~= b.count then return a.count > b.count end
        if a.font ~= b.font then return a.font < b.font end
        return a.code < b.code
    end)
    for _, r in ipairs(rows) do
        main.log("warn", "Glyph is missing from the font (repeated)",
            "font", r.font,
            "glyph_hex", string.format("%04x", r.code),
            "count", tostring(r.count))
    end
end

file_end("fonts.lua")

_G.publisher = _G.publisher or {}
_G.publisher.fonts = M

return M
