---library for `texdef` and `latexdef`
---@module texdef
---@copyright 2026
local tex = require 'tex'
local kpse = require 'kpse'
local utils = require 'prompt.utils'
local argparse = require 'argparse'
local minijinja = require 'minijinja'
local M = {
    templates = {},
}

---https://github.com/lumen-oss/lux/issues/922
---@param name string
---@return string
function M.read(name)
    local root = debug.getinfo(1).source:match("@?(.*).lua$")
    local f = io.open(root .. "/templates/" .. name)
    local content = ""
    if f then
        content = f:read("*a")
        f:close()
    end
    return content
end

for _, name in ipairs { "main.tex", "sub.tex" } do
    M.templates[name] = M.read(name)
end

---get parser
---@param progname string program name
---@param formatname string TeX format name
---@return table parser
function M.get_parser(progname, formatname)
    local parser = argparse(progname):add_complete()
    parser:argument('macro', 'macro name without \\'):args('*')
    parser:option('--value -v', [[Show value of \the\macro instead]]):args(0)
    local hidden = formatname:match 'latex' == nil
    parser:option('--list -l', 'List all command sequences of the given packages by -l, -ll'):args(0):count("*")
        :hidden(hidden)
    parser:option('--find -f', 'Show full filepath of the file where the command sequence was defined by -f, -ff')
        :args(0):count("*")
        :hidden(hidden)
    parser:option('--ignore-regex -I',
        'Ignore all command sequences in the above lists which match lua match(), use ""',
        '[@_]'):convert(M.convert_regex)
        :hidden(hidden)
    parser:option('--Environment -E', 'Every command name is taken as an environment name'):args(0)
        :hidden(hidden)
    parser:option('--class -c', 'class name', not hidden and 'article'):convert(M.add_brace)
        :hidden(hidden)
    parser:option('--package -p', 'package name'):count("*"):convert(M.add_braces)
        :hidden(hidden)
    parser:option('--environment -e', 'environment name'):count("*"):convert(M.add_braces)
        :hidden(hidden)
    parser:option('--othercode -o', 'Add other code into the preamble before the definition is shown'):count("*")
        :hidden(hidden)
    parser:option('--preamble -P', 'Show definition of the command inside the preamble'):args(0)
        :hidden(hidden)
    parser:option('--beforeclass -B', [[Show definition of the command before \documentclass]]):args(0)
        :hidden(hidden)
    parser:option('--before -b', 'Place code before definition is shown'):count("*")
    parser:option('--after -a', 'Place code after definition is shown'):count("*")

    parser:option('--dry-run -n', 'Do not run'):args(0)
    parser:option('--output', 'output file name', tex.jobname .. '.tex')
    parser:option('--entering', 'entering file prompt', '>> entering file ')
    parser:option('--leaving', 'leaving file prompt', '<< leaving file ')
    parser:option('--defined', 'defined prompt', ': defined by ')
    parser:option('--formatname', [[tex.formatname]], formatname)
    return parser
end

--- '' -> '$^'
---@param name string
---@return string
function M.convert_regex(name)
    return name == '' and '$^' or name
end

---add brace: article -> {article}
---@param name string?
---@return string?
function M.add_brace(name)
    if name and name:sub(#name, #name) ~= '}' then
        name = '{' .. name .. '}'
    end
    return name
end

---add braces: hyperref -> {hyperref}
---@param names string[]?
---@return string[]?
function M.add_braces(names)
    if names then
        for i, name in pairs(names) do
            names[i] = M.add_brace(name)
        end
    end
    return names
end

---parse command line arguments
---@param argv string[] command line arguments
---@return table args parsed result
function M.parse(argv)
    local args = utils.parse(argv)
    local parser = M.get_parser(args[0], tex.formatname)
    args = parser:parse(args)
    if args.Environment then
        for i = 1, #args.macro do
            table.insert(args.macro, 'end' .. args.macro[i])
        end
    end
    return args
end

---get environment for minijinja
---@return table
function M.get_environment()
    local env = minijinja.Environment:new()
    for name, template in pairs(M.templates) do
        env:add_template(name, template)
    end
    return env
end

---**first entry for texdef and latexdef**
---@param argv string[] command line arguments
---@return table? args parsed command line arguments
function M.main(argv)
    print()
    local args = M.parse(argv)
    local env = M.get_environment()
    local code = env:render_template('main.tex', args)
    if args.dry_run then
        print(code)
        return
    end
    local output = args.output
    if args.list > 0 then
        output = tex.jobname .. '.log'
    end
    args.f = io.open('.lux/' .. output, 'w+')
    if args.f then
        code = code:gsub("^%s+", ""):gsub("\n", "")
        tex.print(code)
    end
    return args
end

---replace package names with their full paths
---@param text string
---@param defined string
---@return string text
function M.replace(text, defined)
    local paths = {}
    for file in text:gmatch(defined .. '(%S+)') do
        paths[file] = kpse.lookup(file)
    end
    for file, path in pairs(paths) do
        text = text:gsub(defined .. file, defined .. path)
    end
    return text
end

---@alias cs {type: '=' | '->' | '-->', value: string}
---@alias pkg table<string, cs>
---@alias log table<string, pkg>
---extract packages' macros' information from log
---one log contains many packages, one package contains many control sequences
---control sequence can be `k = v`, `k -> v` (macro), `k --> v` (long macro)
---@param f table log file handler
---@param entering string entering prompt
---@param leaving string leaving prompt
---@return log log
function M.parse_log(f, entering, leaving)
    local log = {}
    local pkg_names = {}
    local pkg_name

    for line in f:lines() do
        if line:match(entering) then
            pkg_name = line:gsub(entering, '')
            table.insert(pkg_names, pkg_name)
            log[pkg_name] = {}
        elseif pkg_name and line:match(leaving .. pkg_name) then
            table.remove(pkg_names)
            pkg_name = table.remove(pkg_names)
            if pkg_name then
                table.insert(pkg_names, pkg_name)
            end
        elseif pkg_name and (line:sub(2):match('^into ') or line:sub(2):match('^reassigning ')) then
            line = line:sub(2, #line - 1):gsub("^%S+ ", ""):gsub("\\ETC%.", "..."):gsub(
                'used in a moving argument.', '(moving)')
            local cs_name = line:match("^[^=]+")
            local cs = {
                type = '=',
                value = line:gsub("[^=]+=", ""),
            }
            if cs.value:match("^\\long macro:") then
                cs.type = '-->'
                cs.value = cs.value:gsub("^\\long macro:", "")
            elseif cs.value:match("^macro:") then
                cs.type = '->'
                cs.value = cs.value:gsub("^macro:", "")
            end
            if cs.type ~= '=' then
                local name = cs.value:gsub("%s*->.*", "")
                cs_name = cs_name .. name
                cs.value = cs.value:gsub(name .. '%s*->', "")
            end
            log[pkg_name][cs_name] = cs
        end
    end
    return log
end

---filter log by regex
---@param log log
---@param regex string
---@return log log
function M.filter(log, regex)
    for pkg_name, pkg in pairs(log) do
        for cs_name, _ in pairs(pkg) do
            if cs_name:match(regex) then
                log[pkg_name][cs_name] = nil
            end
        end
    end
    return log
end

---sort dictionary's keys
---@param input table
---@return table names
function M.get_sorted_keys(input)
    local names = {}
    for name, _ in pairs(input) do
        table.insert(names, name)
    end
    table.sort(names)
    return names
end

---sort log and dump output
---@param log log
---@param is_detailed boolean if print control sequences' values
---@return string text
function M.dump(log, is_detailed)
    local pkg_names = M.get_sorted_keys(log)
    local lines = {}
    for _, pkg_name in ipairs(pkg_names) do
        local pkg = log[pkg_name]
        local cs_names = M.get_sorted_keys(pkg)
        local sublines = {}
        for _, cs_name in ipairs(cs_names) do
            local cs = pkg[cs_name]
            local line = cs_name
            if is_detailed then
                line = line .. ' ' .. cs.type .. ' ' .. cs.value
            end
            table.insert(sublines, line)
        end
        if #cs_names ~= 0 then
            table.insert(lines, pkg_name)
            table.insert(lines, table.concat(sublines, "\n"))
        end
    end
    return table.concat(lines, "\n\n")
end

---**final entry for texdef and latexdef**
---@param args table parsed command line arguments
function M.output(args)
    if args == nil or args.f == nil then
        return
    end
    local text
    if args.list ~= 0 then
        local log = M.parse_log(args.f, args.entering, args.leaving)
        log = M.filter(log, args.ignore_regex)
        text = M.dump(log, args.list > 1)
    else
        text = args.f:read("*a"):gsub('=\n', ' = '):gsub('= macro:%->', '-> ')
        if args.find > 1 then
            text = M.replace(text, args.defined)
        end
    end
    print(text)
    args.f:close()
end

return M
