---library for `luatex`, `lualatex`, `luatexinfo` and `texlua`
---@module texrocks
---@copyright 2025
---@diagnostic disable: undefined-field
-- luacheck: ignore 143
local lfs = require "lfs"

local M   = {
    fontmap_name = "luatex.map"
}

if os.type == "windows" then
    M.OSFONTDIR = "C:/Windows/System32/Fonts"
elseif os.type == "unix" then
    if os.getenv "XDG_DATA_DIRS" ~= nil then
        M.OSFONTDIR = "{" .. os.getenv("XDG_DATA_DIRS"):gsub(":", ",") .. "}/share/fonts//"
    else
        local prefixes = { "/usr" }
        if os.getenv "PREFIX" ~= nil then
            prefixes = { os.getenv "PREFIX" }
        elseif os.name ~= "cygwin" then
            table.insert(prefixes, "/usr/local")
        elseif os.getenv "MINGW_PREFIX" ~= nil then
            table.insert(prefixes, os.getenv "MINGW_PREFIX")
        end
        M.OSFONTDIR = "{" .. table.concat(prefixes, ",") .. "}/share/fonts//"
    end
end
if os.name == "macosx" then
    M.OSFONTDIR = M.OSFONTDIR .. ";{/System,}/Library/Fonts//"
elseif os.name == "cygwin" then
    M.OSFONTDIR = M.OSFONTDIR .. ";/proc/cygdrive/c/Windows/System32/Fonts"
end

---base name
---@param path string
---@return string path
function M.basename(path)
    path = path:gsub(".*/", "")
    return path
end

---path without extension name
---@param path string
---@return string path
function M.rootname(path)
    path = path:gsub("%.*", "")
    return path
end

---base name without extension name
---@param path string
---@return string path
function M.name(path)
    return M.rootname(M.basename(path))
end

---get the first non-nil element's index
---@param args string[] index can be negative
---@return integer begin begin index
function M.get_begin_index(args)
    local begin = -1
    while args[begin] do
        begin = begin - 1
    end
    begin = begin + 1
    return begin
end

---texlua has a behaviour about command line arguments.
---`arg` starts from index 0: `arg = {[0] = "ls", "-al"}`
---`os.exec()` starts from index 1: `os.exec{"ls", "-al"}`
---we need to shift it
---@param args string[] command line arguments
---@param offset integer e.g., `-1` means `args[i + 1] = args[i]`
---@return string[] cmd_args
function M.shift(args, offset)
    local begin = M.get_begin_index(args)

    local cmd_args = {}
    for i = begin, #args do
        cmd_args[i - offset] = args[i]
    end
    return cmd_args
end

---call `os.setenv()` when environment variable doesn't exist
---@param key string
---@param value string
function M.setenv(key, value)
    if os.getenv(key) == nil then
        os.setenv(key, value)
    end
end

---get paths from `package.path`/`package.cpath`. see tests.
---@param path string paths concatenated by `;`
---@param suffix string | nil add `../${suffix}//` to paths when it is not nil
---@return string[] paths
function M.getpaths(path, suffix)
    local parts = {}
    local paths = {}
    for part in string.gmatch(path, "([^;]+)") do
        part = part:gsub("/%?.*", "")
        if not parts[part] then
            parts[part] = true
            if suffix then
                part = part:gsub("/src$", ""):gsub("/lib$", "") .. '/etc/' .. suffix
                -- for test
                if lfs.isdir == nil or lfs.isdir(part) then
                    part = part .. "//"
                    table.insert(paths, part)
                end
            else
                table.insert(paths, part)
            end
        end
    end
    return paths
end

---concatenate `getpaths()`
---@param path string same as `getpaths()`
---@param suffix string | nil same as `getpaths()`
---@return string path concatenated by `;`
---@see getpaths
function M.getenv(path, suffix)
    local processed = M.getpaths(path, suffix)
    return table.concat(processed, ";")
end

---**entry for texlua**
---@param args string[] `arg`
function M.main(args)
    M.setenvs()
    -- progname should be texlua
    M.setotherenv(M.name(args[0]))

    -- luacheck: ignore 121
    arg = M.preparse(args)
    loadfile(arg[0])()
end

---wrap `os.setenv()` for font files due to `OSFONTDIR`
---@param key string
---@param value string
function M.setfontenv(key, value)
    os.setenv(key,
        "$TEXMFDOTDIR;" .. M.getenv(package.path, "fonts/" .. value) .. ";" .. M.OSFONTDIR)
end

---set environment variables for kpathsea
---@source ../packages/kpathsea/lua/kpathsea.lua
function M.setenvs()
    M.setenv("TEXMFDOTDIR", ".")
    if os.getenv "USERPROFILE" == nil then
        M.setenv("HOME", "~")
    else
        M.setenv("HOME", os.getenv "USERPROFILE")
    end
    -- https://wiki.archlinux.org/title/XDG_Base_Directory#Partial
    if os.getenv "LOCALAPPDATA" == nil then
        M.setenv("XDG_CONFIG_HOME", (os.getenv "HOME") .. "/.config")
    else
        M.setenv("XDG_CONFIG_HOME", os.getenv "LOCALAPPDATA")
    end
    if os.getenv "APPDATA" == nil then
        M.setenv("XDG_DATA_HOME", (os.getenv "HOME") .. "/.local/share")
    else
        M.setenv("XDG_CONFIG_HOME", os.getenv "APPDATA")
    end
    if os.getenv "TEMP" == nil then
        M.setenv("XDG_CACHE_HOME", (os.getenv "HOME") .. "/.cache")
    else
        M.setenv("XDG_CACHE_HOME", os.getenv "TEMP")
    end
    -- some tex packages like hyperref support config file such as hyperref.cfg
    M.setenv("TEXMFCONFIG", "$XDG_CONFIG_HOME/texmf")
    M.setenv("TEXMFHOME", "$XDG_DATA_HOME/texmf")
    M.setenv("TEXMFVAR", "$XDG_CACHE_HOME/texmf")
    -- project setting > config > data > cache
    -- create ./*.cnf to override
    os.setenv("TEXMF", "$TEXMFDOTDIR;$TEXMFCONFIG;$TEXMFHOME;$TEXMFVAR")
    -- create ./texmf.cnf to override lua/texrocks/texmf.cnf
    os.setenv("TEXMFCNF",
        "$TEXMFDOTDIR;$TEXMFCONFIG;$TEXMFHOME;$TEXMFVAR;" .. debug.getinfo(1).source:match("@?(.*)/") .. '/texrocks')
    os.setenv("TEXMFDBS", "")

    os.setenv("LUAINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path))
    os.setenv("CLUAINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.cpath))
    os.setenv("TEXINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "tex"))
    os.setenv("BIBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/bib"))
    os.setenv("MLBIBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/mlbib"))
    os.setenv("BSTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/bst"))
    os.setenv("MLBSTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/mlbst"))
    os.setenv("RISINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "biber/ris"))
    os.setenv("BLTXMLINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "biber/bltxml"))
    os.setenv("TEXINDEXSTYLE", "$TEXMFDOTDIR;" .. M.getenv(package.path, "makeindex"))
    os.setenv("MFTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "mft"))
    os.setenv("MPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "mp"))
    os.setenv("OCPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "omega/ocp"))
    os.setenv("OTPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "omega/otp"))
    os.setenv("WEBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web"))
    os.setenv("CWEBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "cweb"))

    os.setenv("TEXFORMATS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c"))
    os.setenv("TEXDOCS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "doc"))
    os.setenv("TEXSOURCES", "$TEXMFDOTDIR;" .. M.getenv(package.path, "source"))
    os.setenv("MFINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "fonts/source"))
    os.setenv("MPSUPPORT", "$TEXMFDOTDIR;" .. M.getenv(package.path, "metapost/support"))
    os.setenv("TEXPICTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "images"))
    os.setenv("TEXPOOL", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c"))
    os.setenv("TEXPSHEADERS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "dvips"))
    os.setenv("WEB2C", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c"))
    os.setenv("TEXMFSCRIPTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "scripts"))

    os.setenv("TEXCONFIG", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf/dvips"))
    os.setenv("PDFTEXCONFIG", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf/pdftex"))

    os.setenv("TEXFONTMAPS", ".lux;$XDG_DATA_HOME/lux/tree")
    -- font metrics
    M.setfontenv("TFMFONTS", "tfm")
    M.setfontenv("OFMFONTS", "ofm")
    -- luatex
    M.setfontenv("T1FONTS", "type1")
    M.setfontenv("OVFFONTS", "ovf")
    M.setfontenv("OVPFONTS", "ovp")
    M.setfontenv("VFFONTS", "vf")
    -- luahbtex
    M.setfontenv("TTFONTS", "truetype")
    M.setfontenv("OPENTYPEFONTS", "opentype")
    -- other fonts
    -- /usr/share/groff/{current/font,site-font}/devps
    M.setfontenv("TRFONTS", "groff")
    M.setfontenv("GFFONTS", "gf")
    M.setfontenv("PKFONTS", "pk")
    M.setfontenv("OPLFONTS", "opl")
    M.setfontenv("T42FONTS", "type42")
    M.setfontenv("MISCFONTS", "misc")
    M.setfontenv("ENCFONTS", "enc")
    M.setfontenv("CMAPFONTS", "cmap")
    M.setfontenv("SFDFONTS", "sfd")
    M.setfontenv("LIGFONTS", "lig")
    M.setfontenv("FONTFEATURES", "fea")
    M.setfontenv("FONTCIDMAPS", "cid")
end

---set environment variables for `kpsewhich --show-path 'other text files'`
---@param progname string read <https://texdoc.org/serve/kpathsea/0>
function M.setotherenv(progname)
    M.setenv(progname:upper() .. "INPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf"))
end

---get offset from one script to another script
---such as `texlua --option main.lua --option` -> `main.lua --option`
---offset should be 2
---@param args string[] command line arguments
---@return integer offset
function M.get_offset(args)
    local offset
    for i, v in ipairs(args) do
        local char = v:sub(1, 1)
        -- skip \macro and --option
        if char ~= "\\" and char ~= "-" then
            offset = i
            break
        end
    end
    return offset
end

---refer `parse`
---@see parse
---@param args string[] command line arguments
---@return string[] cmd_args parsed result
function M.preparse(args)
    local offset = M.get_offset(args)
    if offset == nil then
        error("haven't support")
        os.exit(1)
    end

    return M.shift(args, offset)
end

---**entry for luatex**
---@param args string[] `arg`
function M.run(args)
    local cmd_args = M.parse(args)
    M.setotherenv(M.get_program_name(cmd_args))
    M.sync(false)
    M.exec(cmd_args)
end

---luahbtex --luaonly texlua luatex:
---texlua will call preparse(), then loadfile("luatex")()
---luatex will call parse(), then os.exec{[0]="luatex", "luahbtex"}
---@param args string[] command line arguments
---@return string[] cmd_args parsed result
---@see preparse
function M.parse(args)
    local cmd_args = M.shift(args, -1)
    local begin = M.get_begin_index(cmd_args)
    cmd_args[0] = cmd_args[begin]
    return cmd_args
end

---see <https://texdoc.org/serve/luatex/0>'s command line options
---@param args string[] command line arguments not `arg`
---@return string progname
function M.get_program_name(args)
    -- --progname is latter first
    for i = #args, 2, -1 do
        if args[i]:match("^--progname=") then
            local progname = args[i]:gsub("^--progname=", "")
            return progname
        elseif args[i - 1] == "--progname" then
            return args[i]
        end
    end

    -- --fmt/--ini is former first
    local opt
    for i = 2, #args do
        if args[i]:match("^--fmt=") then
            local progname = args[i]:gsub("^--fmt=", "")
            return progname
        elseif args[i] == "--fmt" or args[i] == "--ini" then
            opt = args[i]
        elseif args[i]:match("^%-") == args[i]:match("^\\") then
            if opt == "--fmt" then
                return args[i]
            elseif opt == "--ini" then
                local progname = args[i]:gsub(".*/", ""):gsub("%.*", "")
                return progname
            end
        end
    end

    -- usually be luahbtex
    return M.name(args[1])
end

---update font map file: `.lux/luatex.map`
---@param short boolean use relative (short)/absolute (long) path for font files
function M.sync(short)
    local dir = ".lux"
    if not lfs.isdir(dir) then
        lfs.mkdir(dir)
    end
    local fontmap_name = dir .. "/" .. M.fontmap_name
    local f = io.open(fontmap_name, 'w')
    if f == nil then
        print("fail to generate " .. fontmap_name)
        return
    end
    local fonts = {}

    local function walk(path)
        for file in lfs.dir(path) do
            if file:sub(1, 1) ~= '.' then
                local newpath = path .. '/' .. file
                if lfs.isdir(newpath) then
                    walk(newpath)
                elseif lfs.isfile(newpath) then
                    local ext = file:gsub(".*%.", "")
                    if ext == "pfb" or ext == "t3" then
                        table.insert(fonts, newpath)
                    end
                end
            end
        end
    end

    for _, path in ipairs(M.getpaths(package.path, "fonts")) do
        walk(path:gsub("//$", ""))
    end
    local lines = {}
    for _, font in ipairs(fonts) do
        local basename = font:gsub(".*/", '')
        local name = basename:gsub("%..*", '')
        local path = font
        if short then
            path = basename
        end
        table.insert(lines, string.format("%s %s <%s", name, name:upper(), path))
    end
    local template = debug.getinfo(1).source:match("@?(.*)/") .. '/texrocks/' .. M.fontmap_name
    local t = io.open(template)
    if t then
        f:write(t:read("*a"))
        t:close()
    end
    f:write(table.concat(lines, "\n"))
    f:close()
end

---texlua's `os.exec()` will not exec when meet error
---we wrap it to add `os.exit()`
---@param args string[] command line arguments
function M.exec(args)
    local _, msg, code = os.exec(args)
    error(msg)
    -- 2: No such file or directory
    -- nil: invalid command line passed
    os.exit(code or 1)
end

return M
