-- The fontloader uses the LuaTeX internal fontforge library (called
-- fontloader) to inspect an OpenType, a TrueType or a Type1 font. It
-- converts this font to a font structure  TeX uses internally.
--
--  fontloader.lua
--  speedata publisher
--
--  For a list of authors see `git blame'
--  See file COPYING in the root directory for license info.

file_start("fontloader.lua")

local publisher = require("publisher")

local M = {}

-- Return `truetype`, `opentype` or `type1` depending on the string
-- `filename`. If not recognized form  the file name, return _nil_.
-- This function simply looks at the last three letters.
function M.guess_fonttype(filename)
    local f = filename:lower()
    if f:match(".*%.ttf$") then
        return "truetype"
    elseif f:match(".*%.otf$") then
        return "opentype"
    elseif f:match(".*%.pfb$") then
        return "type1"
    else
        return nil
    end
end

-- Return `true` if the this feature table `tab` has an entry for the
-- given `script` and `lang`. The table is something like:
--
--     [1] = {
--       ["langs"] = {
--         [1] = "AZE "
--         [2] = "CRT "
--         [3] = "TRK "
--       },
--       ["script"] = "latn"
--     },
function M.features_scripts_matches(tab, script, lang)
    lang = string.lower(lang)
    script = string.lower(script)
    for i = 1, #tab do
        local entry = tab[i]
        if string.lower(entry.script) == script then
            for j = 1, #entry.langs do
                if string.lower(entry.langs[j]) == lang then
                    return true
                end
            end
        end
    end
    return false
end

-- Convert codepoint to a UTF-16 string.
function M.to_utf16(codepoint)
    assert(codepoint)
    if codepoint < 65536 then
        return string.format("%04X", codepoint)
    else
        return string.format("%04X%04X", math.floor(codepoint / 1024) + 0xD800, codepoint % 1024 + 0xDC00)
    end
end

-- Return the string that is responsible for the OpenType feature `featurename`.
-- Currently only gsub lookups are supported for script `latn` and language `dflt`.
function M.find_feature_string(f, featurename)
    local ret = {}
    if f.gsub == nil then
        return ret
    end
    for i = 1, #f.gsub do
        local gsub_table = f.gsub[i]
        if gsub_table.features then
            for j = 1, #gsub_table.features do
                local gtf = gsub_table.features[j]
                if gtf.tag == featurename and M.features_scripts_matches(gtf.scripts, "latn", "dflt") then
                    ret[#ret + 1] = gsub_table.subtables[1].name
                end
            end
        end
    end
    return ret
end

---@class SpGlyph: FontloaderGlyph
---@field glyphno integer

-- The result of `fontloader.to_table()` with the publisher specific lookup
-- tables that are stored alongside it.
---@class SpFontloaderTable: FontloaderFont
---@field filename_with_path string
---@field lookup_codepoint_by_name table<string, integer>
---@field lookup_codepoint_by_number table<integer, integer>
---@field glyphtable SpGlyph[]

-- LuaTeX's fontloader (function `to_table()`) returns a rather complex table
-- with all kinds of information. Loading this table is expensive (_todo:
-- measure it_), so we  don't load it over and over again if the user
-- requests the same font in a different size. We also cache the `to_unicode` mapping.
-- Only the size dependent values are computed.
---@type table<string, SpFontloaderTable>
local lookup_fonttable_from_filename = {}

-- The fontforge based font loader is deprecated. Warn only once per font file.
local fontforge_deprecation_warned = {}

---@param name string Font file name (without path).
---@param size number Font size in scaled points.
---@param extra_parameter table Additional font settings such as `space` or `otfeatures`.
---@param mode string The requested font loader ("harfbuzz" or "fontforge").
---@return table f A not yet loaded font instance descriptor.
function M.preload_font(name, size, extra_parameter, mode)
    local f = {
        requested_mode = mode,
        loaded = false,
        requested_name = name,
        requested_size = size,
        requested_extra_parameter = extra_parameter,
    }
    return f
end

-- The harfbuzz version of the fontloader.
---@param name string Font file name (without path).
---@param size number Font size in scaled points.
---@param extra_parameter table Additional font settings such as `space` or `otfeatures`.
---@return boolean ok
---@return table|string fnt_or_msg A TeX usable font table on success, an error message otherwise.
function M.define_font_hb(name, size, extra_parameter)
    local glyphname_uni = {}
    if not publisher.hasharfbuzz then
        main.log("error", 'Can\'t use mode="harfbuzz" on LoadFontfile without harfbuzz library')
        return M.define_font(name, size, extra_parameter)
    end
    if M.guess_fonttype(name) == "type1" then
        main.log(
            "warn",
            "Type 1 fonts are not supported by harfbuzz, falling back to the fontforge font loader",
            "filename",
            name
        )
        return M.define_font(name, size, extra_parameter)
    end
    local filename_with_path = kpse.find_file(name)
    if not filename_with_path then
        return false, string.format("Fontfile '%s' not found.", name)
    end
    -- Nothing must be stored in lookup_fonttable_from_filename here: that
    -- cache holds fontloader tables for M.define_font, a harfbuzz face stored
    -- there breaks a later fontforge load of the same file.
    local face = publisher.harfbuzz.Face.new(filename_with_path)
    local fnt = publisher.harfbuzz.Font.new(face)

    if size < 0 then
        size = -655.36 * size
    end

    -- Some fonts have `units_per_em` set to 0. I am not sure if setting this to
    -- 1000 in that case has any drawbacks.
    local upem = face:get_upem()
    local mag = size / upem
    local backmap = {}
    local f = {}
    f.mag = mag
    f.mode = "harfbuzz"
    f.units_per_em = upem
    f.face = face
    f.font = fnt
    f.characters = {}
    f.otfeatures = {}
    f.name = face:get_name(6)
    f.fullname = face:get_name(6)
    f.designsize = size
    f.size = size
    f.direction = 0
    f.filename = filename_with_path
    f.type = "real"
    f.encodingbytes = 2
    f.tounicode = 1
    f.stretch = publisher.options.fontstretch or extra_parameter.stretch or 40
    f.shrink = publisher.options.fontshrink or extra_parameter.shrink or 30
    f.step = publisher.options.fontstep or extra_parameter.step or 10
    f.auto_expand = true
    f.embedding = "subset"
    f.format = M.guess_fonttype(name)
    f.parameters = {
        slant = 0,
        space = (extra_parameter.space or 25) / 100 * size,
        space_stretch = 0.3 * size,
        space_shrink = 0.1 * size,
        x_height = 0.4 * size,
        quad = 1.0 * size,
        emspace = size,
        enspace = size / 2,
        thirdspace = size / 3,
        quarterspace = size / 4,
        sixthspace = size / 6,
        thinspace = size / 8,
        hairspace = size / 24,
        extra_space = 0,
    }
    local features = {}
    if extra_parameter.otfeatures then
        for fea, enabled in pairs(extra_parameter.otfeatures) do
            local firstletter
            if enabled then
                firstletter = "+"
            else
                firstletter = "-"
            end
            table.insert(features, publisher.harfbuzz.Feature.new(firstletter .. fea))
        end
    end
    f.otfeatures = features
    local unicodes = face:collect_unicodes()
    local glyph_uni = {} -- gid -> primary unicode (lowest codepoint)
    local glyph_all_uni = {} -- gid -> { [uni1]=true, [uni2]=true, ... }
    for _, uni in next, unicodes do
        local gid = fnt:get_nominal_glyph(uni)
        if not glyph_all_uni[gid] then
            glyph_all_uni[gid] = {}
        end
        glyph_all_uni[gid][uni] = true
        local prev = glyph_uni[gid]
        if
            not prev
            or (prev < 32 and uni >= 32)
            or (uni >= 32 and uni < prev)
            or (uni < 32 and prev < 32 and uni < prev)
        then
            glyph_uni[gid] = uni
        end
    end

    for gid = 0, face:get_glyph_count() - 1 do
        local touni = glyph_uni[gid]
        local uni = touni or (publisher.puastart + gid)
        local ge = fnt:get_glyph_extents(gid)
        local hadvance = fnt:get_glyph_h_advance(gid)
        if uni == 160 then -- U+00A0 NO-BREAK SPACE
            uni = 32
        elseif uni == 48 then
            f.zerowidth = hadvance * mag
        end
        local glyphname = fnt:get_glyph_name(gid)

        if glyphname then
            glyphname_uni[glyphname] = uni
        end

        f.characters[uni] = {
            index = gid,
            width = hadvance * mag,
            hadvance = hadvance,
            name = glyphname,
            expansion_factor = 1000,
        }
        -- Margin protrusion is enabled in `spinit.lua`.
        if (uni == 44 or uni == 45 or uni == 46) and extra_parameter and tonumber(extra_parameter.marginprotrusion) then
            f.characters[uni]["right_protruding"] = extra_parameter.marginprotrusion
        end

        local thischar = f.characters[uni]
        backmap[gid] = uni
        if touni then
            thischar.tounicode = touni
        end
        if ge then
            thischar.height = ge.y_bearing * mag
            thischar.depth = (ge.height + ge.y_bearing) * -1 * mag
        end

        -- Create separate entries for all other unicodes sharing this glyph
        if glyph_all_uni[gid] then
            for other_uni, _ in pairs(glyph_all_uni[gid]) do
                if other_uni ~= uni and not f.characters[other_uni] then
                    f.characters[other_uni] = {
                        index = gid,
                        width = hadvance * mag,
                        hadvance = hadvance,
                        name = glyphname,
                        expansion_factor = 1000,
                        tounicode = other_uni,
                    }
                    if
                        (other_uni == 44 or other_uni == 45 or other_uni == 46)
                        and extra_parameter
                        and tonumber(extra_parameter.marginprotrusion)
                    then
                        f.characters[other_uni]["right_protruding"] = extra_parameter.marginprotrusion
                    end
                    if ge then
                        f.characters[other_uni].height = ge.y_bearing * mag
                        f.characters[other_uni].depth = (ge.height + ge.y_bearing) * -1 * mag
                    end
                end
            end
        end
    end

    for gid = 0, face:get_glyph_count() - 1 do
        local touni = glyph_uni[gid]
        local uni = touni or (publisher.puastart + gid)
        local glyphname = fnt:get_glyph_name(gid)
        if glyphname and string.find(glyphname, ".", 1, true) then
            local basename = string.match(glyphname, "^(.*)%.")
            local thischar = f.characters[uni]
            if thischar then
                thischar.tounicode = glyphname_uni[basename]
            end
        end
    end
    f.backmap = backmap

    local fallback_fontdefinitions = {}
    if extra_parameter.fallbacks then
        for i = #extra_parameter.fallbacks, 1, -1 do
            local fallbackname = extra_parameter.fallbacks[i]
            main.log(
                "info",
                "Create font metrics for fallback font",
                "name",
                fallbackname,
                "size",
                math.round(size / publisher.factor, 3)
            )
            local tmp, newfont_or_msg = M.define_font_hb(fallbackname, size, {})
            if not tmp then
                return false, newfont_or_msg
            end
            ---@cast newfont_or_msg -string
            local num = font.define(newfont_or_msg)
            newfont_or_msg.fontnum = num
            fallback_fontdefinitions[#fallback_fontdefinitions + 1] = newfont_or_msg
        end
    end

    f.fallback_fontdefinitions = fallback_fontdefinitions

    return true, f
end

-- Return `true` and a TeX usable font table, or `false` plus an error
-- message. The parameter `extra_parameter` is a table such as:
--     {
--       ["space"] = "25"
--       ["marginprotrusion"] = "100"
--       ["otfeatures"] = {
--         ["smcp"] = "true"
--       },
--     },
---@param name string Font file name (without path).
---@param size number Font size in scaled points.
---@param extra_parameter? table Additional font settings such as `space` or `otfeatures`.
---@return boolean ok
---@return table|string fnt_or_msg A TeX usable font table on success, an error message otherwise.
function M.define_font(name, size, extra_parameter)
    if not fontforge_deprecation_warned[name] then
        fontforge_deprecation_warned[name] = true
        main.log(
            "warn",
            "The fontforge font loader is deprecated and will be removed in a future version",
            "filename",
            name
        )
    end
    extra_parameter = extra_parameter or {}
    local fonttable ---@type SpFontloaderTable
    local missing_features = {}

    if lookup_fonttable_from_filename[name] then
        fonttable = lookup_fonttable_from_filename[name]
        assert(fonttable)
    else
        -- These are stored in the cached fonttable table
        ---@type table<string, integer>
        local lookup_codepoint_by_name = {}
        ---@type table<integer, integer>
        local lookup_codepoint_by_number = {}

        local filename_with_path = kpse.find_file(name)
        if not filename_with_path then
            return false, string.format("Fontfile '%s' not found.", name)
        end
        local loadedfont, err = fontloader.open(filename_with_path)
        if not loadedfont then
            if type(err) == "string" then
                return false, err
            else
                printtable("Font error", err)
                return false, string.format("Problem while loading font '%s'", tostring(filename_with_path))
            end
        end
        local totable = fontloader.to_table(loadedfont)
        if not totable then
            return false, string.format("Problem while loading font '%s'", tostring(filename_with_path))
        end
        ---@cast totable SpFontloaderTable
        fonttable = totable

        -- Store the table for quicker lookup later.
        lookup_fonttable_from_filename[name] = fonttable

        fonttable.filename_with_path = filename_with_path

        -- We require a mapping glyph number -> unicode codepoint.
        -- I used to have two different means of unicode -> glyph mapping.
        -- The type1 fonts got the unicode point with `g.unicode`, the
        -- The ttf/otf font got the point with `fonttable.map.backmap[i]`
        -- Somehow this worked a few years until Arial Narrow could not display
        -- a semicolon (see issue #152).
        -- Now I use g.unicode if present, otherwise map.backmap.
        -- See issue #154

        ---@type SpGlyph[]
        local glyphtable = {}
        if fonttable.subfonts and #fonttable.subfonts > 0 then
            -- this looks like a CID-keyed font such as Noto
            for _, subfont in pairs(fonttable.subfonts) do
                for glyphno, g in pairs(subfont.glyphs) do
                    ---@cast g SpGlyph
                    -- hyphen
                    if g.unicode == 8209 then
                        g.unicode = 45
                    end
                    g.glyphno = glyphno
                    glyphtable[#glyphtable + 1] = g
                end
            end
        else
            -- all regular
            for glyphno, g in pairs(fonttable.glyphs) do
                ---@cast g SpGlyph
                g.glyphno = glyphno
                glyphtable[#glyphtable + 1] = g
            end
        end
        -- For kerning a mapping glyphname -> codepoint is needed.

        for i = 1, #glyphtable do
            local g = glyphtable[i]
            local cp = g.unicode
            if cp == -1 then
                cp = fonttable.map.backmap[i]
            end
            lookup_codepoint_by_name[g.name] = cp
            lookup_codepoint_by_number[g.glyphno] = cp
        end
        fonttable.lookup_codepoint_by_name = lookup_codepoint_by_name
        fonttable.lookup_codepoint_by_number = lookup_codepoint_by_number
        fonttable.glyphtable = glyphtable
    end

    -- A this point we have taken the `fonttable` from memory or from `fontloader#to_table()`. The next
    -- part is mostly size/features dependent.

    if size < 0 then
        size = -655.36 * size
    end
    -- Some fonts have `units_per_em` set to 0. I am not sure if setting this to
    -- 1000 in that case has any drawbacks.
    if not fonttable.units_per_em then
        return false, string.format("something went wrong defining %s. Please file a bug report.", name)
    end
    if fonttable.units_per_em == 0 then
        fonttable.units_per_em = 1000
    end
    local mag = size / fonttable.units_per_em

    -- The table `f` is the font structure that TeX can use, see chapter 7 of the LuaTeX manual for a detailed description. This is returned from
    -- the function. It is safe to store additional data here.
    local f = {}

    -- The index of the characters table must match the glyphs in the
    -- "document". It is wise to have everything in unicode, so we do keep that
    -- in mind when filling the characters subtable.
    f.characters = {}
    f.fontloader = fonttable
    f.otfeatures = {}
    f.mode = "fontforge"
    f.units_per_em = fonttable.units_per_em
    f.name = fonttable.fontname
    f.fullname = fonttable.fontname
    f.designsize = size
    f.size = size
    f.direction = 0
    f.filename = fonttable.filename_with_path
    f.type = "real"
    f.encodingbytes = 2
    f.tounicode = 0
    f.stretch = publisher.options.fontstretch or extra_parameter.stretch or 40
    f.shrink = publisher.options.fontshrink or extra_parameter.shrink or 30
    f.step = publisher.options.fontstep or extra_parameter.step or 10
    f.auto_expand = true

    f.parameters = {
        slant = 0,
        space = (extra_parameter.space or 25) / 100 * size,
        space_stretch = 0.3 * size,
        space_shrink = 0.1 * size,
        x_height = 0.4 * size,
        quad = 1.0 * size,
        emspace = size,
        enspace = size / 2,
        thirdspace = size / 3,
        quarterspace = size / 4,
        sixthspace = size / 6,
        thinspace = size / 8,
        hairspace = size / 24,
        extra_space = 0,
    }

    f.format = M.guess_fonttype(name)
    if f.format == nil then
        return false, "Could not determine the type of the font '" .. fonttable.filename_with_path .. "'."
    end

    f.embedding = "subset"
    f.cidinfo = fonttable.cidinfo

    for i = 1, #fonttable.glyphtable do
        local glyph = fonttable.glyphtable[i]
        local glyphno = glyph.glyphno
        local codepoint = fonttable.lookup_codepoint_by_number[glyphno]
        if codepoint == 48 then
            f.zerowidth = glyph.width * mag
        end
        -- TeX uses U+002D HYPHEN-MINUS for hyphen, correct would be U+2012 HYPHEN.
        -- Because font vendors all have different ideas of hyphen, we just map all
        -- occurrences of *HYPHEN* to 0x2D (decimal 45)
        if glyph.name:lower():match("^hyphen$") then
            codepoint = 45
        end
        if codepoint then
            f.characters[codepoint] = {
                index = glyphno,
                width = glyph.width * mag,
                name = glyph.name,
                expansion_factor = 1000,
                lookups = glyph.lookups,
            }

            -- Height and depth of the glyph
            if glyph.boundingbox[4] then
                f.characters[codepoint].height = glyph.boundingbox[4] * mag
            end
            if glyph.boundingbox[2] then
                f.characters[codepoint].depth = -glyph.boundingbox[2] * mag
            end

            -- We change the `tounicode` entry for entries with a period. Sometimes fonts
            -- have entries like `a.sc` or `a.c2sc` for smallcaps letter a. We are
            -- only interested in the part before the period.
            -- _This solution might not be perfect_.
            if glyph.name:match("%.") then
                local destname = glyph.name:gsub("^([^%.]*)%..*$", "%1")
                local cp = fonttable.lookup_codepoint_by_name[destname]
                if cp then
                    f.characters[codepoint].tounicode = M.to_utf16(cp)
                end
            end

            -- Margin protrusion is enabled in `spinit.lua`.
            if
                (glyph.name == "hyphen" or glyph.name == "period" or glyph.name == "comma")
                and extra_parameter
                and type(extra_parameter.marginprotrusion) == "number"
            then
                f.characters[codepoint]["right_protruding"] = extra_parameter.marginprotrusion
            end

            -- We do kerning by default. In the future we could turn it off.
            local kerns = {}
            if glyph.kerns then
                for _, kern in pairs(glyph.kerns) do
                    local dest = fonttable.lookup_codepoint_by_name[kern.char]
                    if dest and dest > 0 then
                        kerns[dest] = kern.off * mag
                    else
                    end
                end
            end
            f.characters[codepoint].kerns = kerns
        end
    end

    local fallback_fontdefinitions = {}
    if extra_parameter.fallbacks then
        for i = #extra_parameter.fallbacks, 1, -1 do
            local fnt = extra_parameter.fallbacks[i]
            main.log(
                "info",
                "Create font metrics for fallback font",
                "name",
                fnt,
                "size",
                math.round(size / publisher.factor, 3)
            )
            local tmp, newfont_or_msg = M.define_font(fnt, size)
            if not tmp then
                return false, newfont_or_msg
            end
            ---@cast newfont_or_msg -string
            local num = font.define(newfont_or_msg)
            newfont_or_msg.fontnum = num
            fallback_fontdefinitions[#fallback_fontdefinitions + 1] = newfont_or_msg
        end
    end

    -- create a virtual font to fake a feature
    local needs_virtual_font = false
    local new_f
    if extra_parameter and extra_parameter.otfeatures then
        for of, enabled in pairs(extra_parameter.otfeatures) do
            if enabled then
                if of == "tnum" or of == "lnum" then
                    missing_features[#missing_features + 1] = of
                    needs_virtual_font = true
                else
                    local featuret = M.find_feature_string(fonttable, of)
                    if featuret and #featuret > 0 then
                        f.otfeatures[#f.otfeatures + 1] = featuret
                    else
                        missing_features[#missing_features + 1] = of
                    end
                end
            end
        end
    end

    for _, feature in ipairs(missing_features) do
        if feature == "tnum" then
            needs_virtual_font = true
            break
        end
    end

    if #fallback_fontdefinitions > 0 then
        needs_virtual_font = true
    end

    -- first define a virtual font
    if needs_virtual_font then
        local num = font.define(f)
        new_f = {
            fonts = { { id = num } },
        }
        for _, fnt in ipairs(fallback_fontdefinitions) do
            new_f.fonts[#new_f.fonts + 1] = { id = fnt.fontnum }
        end
        new_f.name = f.name
        new_f.fullname = f.fullname
        new_f.designsize = f.designsize
        new_f.size = f.size
        new_f.direction = f.direction
        new_f.filename = f.filename
        new_f.encodingbytes = f.encodingbytes
        new_f.tounicode = f.tounicode
        new_f.stretch = f.stretch
        new_f.shrink = f.shrink
        new_f.step = f.step
        new_f.auto_expand = f.auto_expand
        new_f.parameters = f.parameters
        new_f.characters = {}
        new_f.otfeatures = f.otfeatures
        new_f.fontloader = f.fontloader

        for _fntnum, fnt in ipairs(fallback_fontdefinitions) do
            for i, v in pairs(fnt.characters) do
                new_f.characters[i] = {
                    index = v.index,
                    width = v.width,
                    height = v.height,
                    depth = v.depth,
                    commands = { { "font", _fntnum + 1 }, { "char", i } },
                    lookups = v.lookups,
                    kerns = v.kerns,
                }
            end
        end

        for i, v in pairs(f.characters) do
            new_f.characters[i] = {
                index = v.index,
                width = v.width,
                height = v.height,
                depth = v.depth,
                commands = { { "char", i } },
                lookups = v.lookups,
                kerns = v.kerns,
            }
        end

        -- now we can add features to the font
        for _, feature in ipairs(missing_features) do
            if feature == "lnum" then
                local featuret = M.find_feature_string(fonttable, "lnum")
                if featuret and #featuret > 0 then
                    featuret = featuret[1]
                end
                for i = 48, 57 do
                    local lookups = new_f.characters[i].lookups
                    if lookups and lookups[featuret] then
                        local destname = lookups[featuret][1].specification.variant
                        local dest = fonttable.lookup_codepoint_by_name[destname]
                        new_f.characters[i] = new_f.characters[dest]
                    end
                end
            end
        end

        for _, feature in ipairs(missing_features) do
            if feature == "tnum" then
                local maxfigurewidth = 0
                local glyphwd
                for i = 48, 57 do
                    maxfigurewidth = math.max(maxfigurewidth, new_f.characters[i].width)
                end
                for i = 48, 57 do
                    local thisglyph = new_f.characters[i]
                    glyphwd = thisglyph.width
                    if maxfigurewidth ~= glyphwd then
                        thisglyph.width = maxfigurewidth
                        thisglyph.commands = {
                            { "right", (maxfigurewidth - glyphwd) / 2 },
                            thisglyph.commands[1],
                        }
                    end
                end
            end
        end

        return true, new_f
    end

    return true, f
end

file_end("fontloader.lua")

return M
-- End of file
