util.error = {}

util.error.missionFuncDir = {}

--- builds a custom stack trace message
--- @param err string initial error message. Gets appended on top of strack trace
--- @return string traceback
function util.error.buildStackTrace(err)
    -- start with 2 as its the first function wich is not the current
    local i, info = 2, debug.getinfo(2)
    local trace, flag = "" .. tostring(err) .. "\nStack trace:", ""

    while (info and info.what ~= "main") do
        -- NAME SECTION --
        local name = nil

        if info.what == "tail" then
            name = "n/a"
        elseif info.what == "Lua" then
            name = util.error.missionFuncDir[info.func]
        else
            name = (info.name ~= "") and info.name or nil
        end

        if not name then
            name = "?"
            flag = flag == ""
                    and flag .. "\tStacktrace contains at least one unknow ('?') function:\n"
                             .. "\t  + Attempting to run the mission with 'mission.EXT_DEBUG' flag set to true might fix this issue\n"
                             .. "\t  + Verify that 'util.error.registerModules' is run after module import."
                    or flag
        end

        -- LINE SECTION --
        local line = (info.currentline and info.currentline >= 0) and tostring(info.currentline) or nil

        -- BUILDING STACK --
        trace = trace .. "\n\t-->"
                      .. " ["..tostring(info.what).."]"
                      .. "\t'"..name.."'"
                      .. (line and " at line "..line or "")

        -- jump to next function in stack
        i = i+1
        info = debug.getinfo(i)
    end

    trace = trace .. (flag ~= "" and ("\n\nADITIONAL INFORMATION:\n" .. flag) or "")

    return trace
end

--- wrapped pcall. This function wraps the xpcall function with the mission stacktrace
---
--- INFO: Since DCS runs in lua 5.1 we have to wrap functions with parameters in a lambda (see param)
---
--- Credit: https://stackoverflow.com/a/30125834
--- @param lambda function function wrap in form 'function() return call(a,b,c) end'. Functions without parameter can be passed without wrapping
--- @return boolean success, any result, ... additional # boolean followed by results or error
function util.error.wpcall(lambda)
    return xpcall(lambda, util.error.buildStackTrace)
end

--- shold be used to forward errors to the DCS environment (e.g. Log etc.)
---
--- WARNING: this function is not stopping function flow. If you wish for your function to stop executing after error append 'return nil'
--- @param err any error thrown. Can / should include stack trace (see util.error.buildStrackTrace)
--- @return nil
function util.error.raise(err)
    -- print to LOG
    env.error(err, false)

    -- generate generic message in mission
    -- Since the mission has to be rebuild before changes have any effect this message does not have to mention the cause.
    trigger.action.outText("UNEXPECTED ERROR OCCURED. Aditional information is located in your DCS log file", 20, false)

    return nil
end

--- registers all loaded module functions for easier stack tracing
---
--- INFO: per default only mission modules are loaded. Setting mission.EXT_DEBUG to true will load the entire _G environment.
---       Be careful with memory usage and load times as all functions are saved in an aditional table.
---
--- WARNING: always call this function after importing all modules
function util.error.registerModules()
    local all = mission.EXT_DEBUG and _G or util.misc.filter(_G, function(v, k) return util.misc.find(mission.requires, k) or k == "mission" end, true)
    local flat = util.misc.flat(all, function(k, ik) return tostring(k).."."..tostring(ik) end, -1)
    local funcs = util.misc.filter(flat, function(v, k) return type(v) == "function" end, true)

    util.error.missionFuncDir = util.misc.map(funcs, function(v, k) return k, v end)

    return nil
end

--- packs argument missmatch into error string
--- @param arg any missmatched argument used for type
--- @param expected string expected type
--- @param index integer
--- @return string
function util.error.argErr(arg, expected, index)
    -- retrive function caller name
    local caller = debug.getinfo(2).name or "?"
    return "\nbad argument #" .. index .. " to '" .. caller  .."' (" .. expected .. " expected , got " .. type(arg) ..")"
end

--- packs not found argument (thrown when attempting to find something) into error string
--- @param value any value wich was attemted to find
--- @param container any container wich was searched in
--- @param limit integer limit wich was applied when seraching (depth and so on)
--- @return string
function util.error.notFoundErr(value, container, limit)
    return "\nvalue " .. value .. " could not be found in " .. tostring(container) .. " (depth " .. tostring(limit) .. ")"
end

--- packs lookup errors (thrown when attempting to retrieve something) into error string
--- @param value any value wich was attemted to find
--- @param container any container wich was searched in
--- @param received any value recieved when looking up
--- @param expected string expected value
--- @return string
function util.error.lookupErr(value, container, received, expected)
    return "\nlookup for '" .. value .. "' in " .. tostring(container) .. " failed (found " .. tostring(type(received)) .. " expected " .. tostring(expected) .. ")"
end

--- packs precondition errors (thrown when a precondition is not met) into error string
--- @param reason string
--- @return string
function util.error.preconditionFailedErr(reason)
    return "precondition not met: " .. tostring(reason)
end