---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