-- Configuration
CONFIG_FILE = "mizflow-config.lua"

-- Functions
local currentWorkingDir
local mizflowConfig
local debuggerConfig
local watchList = {}

-- #region Utils

function JoinPath(...)
    local parts = { ... }
    local result = {}

    for _, part in ipairs(parts) do
        -- Skip completely empty parts
        if part and part ~= "" then
            -- Remove leading/trailing slashes to avoid double slashes
            part = part:gsub("^/*", ""):gsub("/*$", "")
            table.insert(result, part)
        end
    end

    return table.concat(result, "/")
end

function LoadConfiguration()
    local dir = GetWorkingDir()
    local configPath = JoinPath(dir, CONFIG_FILE)
    mizflowConfig = dofile(configPath)
end

function LuaFileExists(path)
    local chunk, err = loadfile(path)
    return chunk ~= nil
end

function SetWorkingDir()
    for _, dir in pairs(mizflowVariables.workingDirs) do
        local filePath = JoinPath(dir, CONFIG_FILE)
        if LuaFileExists(filePath) then
            currentWorkingDir = dir
            return true
        end
    end
    return false
end

function GetWorkingDir()
    if currentWorkingDir then
        return currentWorkingDir
    end
    SetWorkingDir()
    return currentWorkingDir
end

function run(path)
    local filePath = JoinPath(currentWorkingDir, mizflowConfig.scriptsDir, path)
    return assert(loadfile(filePath))()
end

function ReadDebugger()
    local filePath = JoinPath(
        currentWorkingDir,
        mizflowConfig.debuggerFilePath)
    if LuaFileExists(filePath) then
        return dofile(filePath)
    end
    -- return empty debugger configuration
    return {}
end

function DumpTable(tbl, indent)
    indent = indent or 0
    local toprint = string.rep("  ", indent) .. "{\n"
    indent = indent + 1
    for k, v in pairs(tbl) do
        toprint = toprint .. string.rep("  ", indent)
        if type(k) == "number" then
            toprint = toprint .. "[" .. k .. "] = "
        elseif type(k) == "string" then
            toprint = toprint .. '["' .. k .. '"] = '
        end
        if type(v) == "number" then
            toprint = toprint .. v .. ",\n"
        elseif type(v) == "string" then
            toprint = toprint .. '"' .. v .. '",\n'
        elseif type(v) == "table" then
            toprint = toprint .. DumpTable(v, indent) .. ",\n"
        elseif type(v) == "boolean" then
            toprint = toprint .. tostring(v) .. ",\n"
        else
            toprint = toprint .. '"<' .. type(v) .. '>",\n'
        end
    end
    indent = indent - 1
    toprint = toprint .. string.rep("  ", indent) .. "}"
    return toprint
end

function Count(table)
    local count = 0
    for _ in pairs(table) do
        count = count + 1
    end
    return count
end

function Last(list)
    if #list == 0 then return nil end
    return list[#list]
end

function Sort(list)
    if list and next(list) then
        table.sort(list)
    end
end

function GetSortedKeys(tbl)
    if not tbl then return end
    local keys = {}
    for key, _ in pairs(tbl) do
        table.insert(keys, key)
    end
    table.sort(keys)
    return keys
end

-- #endregion Utils

-- #region F10 MENU

function LoadDebuggerMenu()
    debuggerConfig = ReadDebugger()
    BuildDebuggerMenu()
end

-- #region DEBUGGER

function ShouldShowDebugger(config)
    return (config.flags and next(config.flags)) or (config.macros and next(config.macros))
end

function SetFlag(args)
    trigger.action.setUserFlag(args.flag, args.value)
end

function ToggleFlag(flagName)
    local value = trigger.misc.getUserFlag(flagName)
    local newValue = (value == 1) and 0 or 1
    trigger.action.setUserFlag(flagName, newValue)
end

function IncrementFlag(args)
    local delta = args.delta or 1
    local value = trigger.misc.getUserFlag(args.flag)
    trigger.action.setUserFlag(args.flag, value + delta)
end

function BuildDebuggerMenu()
    local debuggerMenuTitle = "Debugger"
    missionCommands.removeItem({debuggerMenuTitle})
    if not ShouldShowDebugger(debuggerConfig) then
        return
    end

    Sort(debuggerConfig.flags)

    local debugMenu = missionCommands.addSubMenu(debuggerMenuTitle)

    missionCommands.addCommand("Reload Config", debugMenu, function()
        watchList = {}
        StopWatch()
        LoadDebuggerMenu()
        trigger.action.outText("Debugger configuration reloaded")
    end)

    BuildAddWatchMenu(debugMenu)
    BuildRemoveWatchMenu(debugMenu)
    BuildSetFlagMenu(debugMenu)
    BuildIncrementFlagMenu(debugMenu)
    BuildDecrementFlagMenu(debugMenu)
    BuildRunMacroMenu(debugMenu)
end

function BuildAddWatchMenu(parentMenu)
    local flags = debuggerConfig and debuggerConfig.flags
    if flags and next(flags) and Count(watchList) < #flags then
        local addWatchMenu = missionCommands.addSubMenu("Add Watch", parentMenu)

        missionCommands.addCommand("[All flags]", addWatchMenu, function()
            for _, flagName in pairs(flags) do
                watchList[flagName] = true
            end
            StartWatch()
            BuildDebuggerMenu()
        end)

        for _, flagName in pairs(flags) do
            if watchList[flagName] == nil then
                missionCommands.addCommand(flagName, addWatchMenu, function()
                    watchList[flagName] = true
                    StartWatch()
                    BuildDebuggerMenu()
                end)
            end
        end
    end
end

function BuildRemoveWatchMenu(parentMenu)
    if next(watchList) ~= nil then
        local removeWatchMenu = missionCommands.addSubMenu("Remove Watch", parentMenu)

        missionCommands.addCommand("[All flags]", removeWatchMenu, function()
            watchList = {}
            StopWatch()
            BuildDebuggerMenu()
        end)

        for flagName, _ in pairs(watchList) do
            missionCommands.addCommand(flagName, removeWatchMenu, function()
                watchList[flagName] = nil
                if next(watchList) == nil then
                    StopWatch()
                end
                BuildDebuggerMenu()
            end)
        end
    end
end

function BuildSetFlagMenu(parentMenu)
    local flags = debuggerConfig and debuggerConfig.flags
    if flags and next(flags) then
        local setFlagMenu = missionCommands.addSubMenu("Set Flag", parentMenu)

        for _, flagName in pairs(flags) do
            BuildSetFlagMenuItem(setFlagMenu, flagName)
        end
    end
end

function BuildSetFlagMenuItem(parentMenu, flagName)
    local flagMenu = missionCommands.addSubMenu(flagName, parentMenu)

    missionCommands.addCommand("OFF (0)", flagMenu, SetFlag, {
        flag = flagName,
        value = 0
    })

    missionCommands.addCommand("ON (1)", flagMenu, SetFlag, {
        flag = flagName,
        value = 1
    })

    missionCommands.addCommand("TOGGLE (0-1)", flagMenu, ToggleFlag, flagName)

    BuildSetFlagOtherValuesMenu(flagMenu, flagName)
end

function BuildSetFlagOtherValuesMenu(parentMenu, flagName)
    local flagAssignments = debuggerConfig and debuggerConfig.flagAssignments
    local values = flagAssignments and flagAssignments.values
    if values and #values > 0 then
        for _, otherValue in pairs(values) do
            missionCommands.addCommand(tostring(otherValue), parentMenu, SetFlag, {
                flag = flagName,
                value = otherValue
            })
        end
    end
end

function BuildIncrementFlagMenu(parentMenu)
    local flags = debuggerConfig and debuggerConfig.flags
    if flags and next(flags) then
        local incrementFlagMenu = missionCommands.addSubMenu("Increment Flag", parentMenu)

        for _, flagName in pairs(flags) do
            BuildIncrementFlagMenuItem(incrementFlagMenu, flagName)
        end
    end
end

function BuildIncrementFlagMenuItem(parentMenu, flagName)
    local flagAssignments = debuggerConfig and debuggerConfig.flagAssignments
    local steps = flagAssignments and flagAssignments.steps
    if steps and #steps > 0 then
        local flagMenu = missionCommands.addSubMenu(flagName, parentMenu)
        missionCommands.addCommand("+1", flagMenu, IncrementFlag, {
            flag = flagMenu[#flagMenu]
        })
        BuildIncrementFlagOtherValuesMenu(flagMenu, flagName)
    else
        -- skip +1 submenu, because it's obvious
        missionCommands.addCommand(flagName, parentMenu, IncrementFlag, {
            flag = flagName
        })
    end
end

function BuildIncrementFlagOtherValuesMenu(parentMenu, flagName)
    local flagAssignments = debuggerConfig and debuggerConfig.flagAssignments
    local steps = flagAssignments and flagAssignments.steps
    if steps and #steps > 0 then
        for _, otherStep in pairs(steps) do
            missionCommands.addCommand("+" .. tostring(otherStep), parentMenu, IncrementFlag, {
                flag = flagName,
                delta = otherStep
            })
        end
    end
end

function BuildDecrementFlagMenu(parentMenu)
    local flags = debuggerConfig and debuggerConfig.flags
    if flags and next(flags) then
        local decrementFlagMenu = missionCommands.addSubMenu("Decrement Flag", parentMenu)

        for _, flagName in pairs(flags) do
            BuildDecrementFlagMenuItem(decrementFlagMenu, flagName)
        end
    end
end

function BuildDecrementFlagMenuItem(parentMenu, flagName)
    local flagAssignments = debuggerConfig and debuggerConfig.flagAssignments
    local steps = flagAssignments and flagAssignments.steps
    if steps and #steps > 0 then
        local flagMenu = missionCommands.addSubMenu(flagName, parentMenu)
        missionCommands.addCommand("-1", flagMenu, IncrementFlag, {
            flag = flagName,
            delta = -1
        })
        BuildDecrementFlagOtherValuesMenu(flagMenu, flagName)
    else
        -- skip -1 submenu, because it's obvious
        missionCommands.addCommand(flagName, parentMenu, IncrementFlag, {
            flag = flagName,
            delta = -1
        })
    end
end

function BuildDecrementFlagOtherValuesMenu(parentMenu, flagName)
    local flagAssignments = debuggerConfig and debuggerConfig.flagAssignments
    local steps = flagAssignments and flagAssignments.steps
    if steps and #steps > 0 then
        for _, otherValue in pairs(steps) do
            missionCommands.addCommand("-" .. tostring(otherValue), parentMenu, IncrementFlag, {
                flag = flagName,
                delta = -otherValue
            })
        end
    end
end

function BuildRunMacroMenu(parentMenu)
    local macros = debuggerConfig and debuggerConfig.macros
    if macros and Count(macros) > 0 then
        local runMacroMenu = missionCommands.addSubMenu("Run macro", parentMenu)
        local sortedMacroTitles = GetSortedKeys(macros)
        if sortedMacroTitles then
            for _, macroTitle in ipairs(sortedMacroTitles) do
                missionCommands.addCommand(macroTitle, runMacroMenu, RefreshMacrosAndRun, macroTitle)
            end
        end
    end
end

-- this function allows the user to edit a macro and re-run it
-- without reloading the debugger configuration manually
function RefreshMacrosAndRun(macroKey)
    debuggerConfig = ReadDebugger()
    local macro = debuggerConfig.macros[macroKey]
    if type(macro) == "function" then
        local ok, err = pcall(macro)
        if not ok then
            trigger.action.outText("Error running macro '" .. tostring(macroKey) .. "': " .. tostring(err), 20)
        end
    end
end

local function ShowWatchStatus()
    local status = {}
    for flagName, _ in pairs(watchList) do
        local val = trigger.misc.getUserFlag(flagName)
        local line = flagName .. " = " .. tostring(val)
        table.insert(status, line)
    end
    trigger.action.outText(table.concat(status, "\n"), 1, true)
    return timer.getTime() + 1
end

function StartWatch()
    if not watchRunning then
        showWatchStatusId = timer.scheduleFunction(ShowWatchStatus, {}, timer.getTime() + 1)
        watchRunning = true
    end
end

function StopWatch()
    if watchRunning and showWatchStatusId then
        timer.removeFunction(showWatchStatusId)
        showWatchStatusId = nil
        watchRunning = false
    end
end

-- #endregion DEBUGGER

-- #endregion F10 MENU

-- init
LoadConfiguration()
LoadDebuggerMenu()
