require "MS/MS_MatrixManager"
require "ISUI/ISTextBox"
require "MS/MS_IndicatorOrder"

MS_PresetManager = MS_PresetManager or {}

local function tr(key, fallback)
    if getTextOrNull then
        local translated = getTextOrNull(key)
        if translated and translated ~= key then
            return translated
        end
    end

    if getText then
        local ok, translated = pcall(getText, key)
        if ok and translated and translated ~= key then
            return translated
        end
    end

    return fallback
end

MS_PresetManager.USER_PRESETS_FILE = "ModernStatusPresets.lua"

local BUNDLED_PRESET_NAME_KEYS = {
    ["Circular 5 Columns"] = "IGUI_ModernStatus_BundledPreset_Circular5Columns",
    ["Horizontal 1 Column"] = "IGUI_ModernStatus_BundledPreset_Horizontal1Column",
    ["Horizontal 1 Wide Column InvertReserves"] = "IGUI_ModernStatus_BundledPreset_Horizontal1WideColumnInvertReserves",
    ["Horizontal 1 Wide Column InvertRisks"] = "IGUI_ModernStatus_BundledPreset_Horizontal1WideColumnInvertRisks",
    ["HorizMatrix - AutoHide (NotNutrition) - FillHiddenGaps"] = "IGUI_ModernStatus_BundledPreset_HorizMatrixAutoHideNotNutritionFillHiddenGaps",
    ["Vertical 1 Row"] = "IGUI_ModernStatus_BundledPreset_Vertical1Row",
    ["Vertical 2 Rows"] = "IGUI_ModernStatus_BundledPreset_Vertical2Rows",
}

-- Lightweight bundled preset index used by the Presets panel.
-- This avoids loading and parsing the full bundled preset table just to show names.
local BUNDLED_PRESET_NAME_INDEX = {
    "Circular 5 Columns",
    "Horizontal 1 Column",
    "Horizontal 1 Wide Column InvertReserves",
    "Horizontal 1 Wide Column InvertRisks",
    "HorizMatrix - AutoHide (NotNutrition) - FillHiddenGaps",
    "Vertical 1 Row",
    "Vertical 2 Rows",
}

local function _deepCopy(value)
    if type(value) ~= "table" then
        return value
    end

    local copy = {}
    for key, entry in pairs(value) do
        copy[_deepCopy(key)] = _deepCopy(entry)
    end
    return copy
end

local function _deepMerge(target, source)
    if type(source) ~= "table" then
        return target
    end

    for key, value in pairs(source) do
        if type(value) == "table" then
            if type(target[key]) ~= "table" then
                target[key] = {}
            end
            _deepMerge(target[key], value)
        else
            target[key] = value
        end
    end

    return target
end

local function _storePresetRestoreState(config)
    -- Keep the Global Settings Restore buttons in sync with the last loaded preset
    -- without requiring MS_GlobalSettingsPanel here. Requiring that panel from the
    -- preset manager creates a PresetsPanel -> PresetManager -> GlobalSettingsPanel
    -- -> PresetsPanel recursive require chain during startup.
    if type(config) ~= "table" or type(config.indicators) ~= "table" then
        return
    end

    local restore = _deepCopy((config.globalSettings and config.globalSettings.globalSettingsPanelRestore) or {})
    restore.visibility = {}
    restore.lock = {}

    for indicatorType, indicatorConfig in pairs(config.indicators) do
        if type(indicatorConfig) == "table" then
            restore.visibility[indicatorType] = not (indicatorConfig.hidden == true)
            restore.lock[indicatorType] = indicatorConfig.locked == true
        end
    end

    config.globalSettings = config.globalSettings or {}
    config.globalSettings.globalSettingsPanelRestore = restore
end

local function _serializeKey(key)
    if type(key) == "string" and string.match(key, "^[%a_][%w_]*$") then
        return key
    end

    return "[" .. MS_PresetManager.serializeValue(key, 0, nil) .. "]"
end

function MS_PresetManager.serializeValue(value, depth, path)
    depth = depth or 0
    path = path or {}

    if type(value) == "table" then
        local indent = string.rep("    ", depth)
        local childIndent = string.rep("    ", depth + 1)
        local lines = {"{"}
        local keys = MS_IndicatorOrder.getSortedKeys(value, path)

        for _, key in ipairs(keys) do
            local childPath = {}
            for index, entry in ipairs(path) do
                childPath[index] = entry
            end
            childPath[#childPath + 1] = key

            lines[#lines + 1] = childIndent .. _serializeKey(key) .. " = " .. MS_PresetManager.serializeValue(value[key], depth + 1, childPath) .. ","
        end

        lines[#lines + 1] = indent .. "}"
        return table.concat(lines, "\n")
    elseif type(value) == "string" then
        return string.format("%q", value)
    elseif type(value) == "number" or type(value) == "boolean" then
        return tostring(value)
    end

    return "nil"
end

local function _readFileContent(fileName, createIfMissing)
    local reader = getFileReader(fileName, createIfMissing == true)
    if not reader then
        return nil
    end

    -- Accumulate lines in an array and concatenate once.
    -- Repeated string concatenation becomes very expensive when many saved presets exist.
    local lines = {}
    local line = reader:readLine()
    while line do
        lines[#lines + 1] = line
        line = reader:readLine()
    end
    reader:close()

    if #lines == 0 then
        return nil
    end

    return table.concat(lines, "\n") .. "\n"
end

local function _readLuaTableFile(fileName, createIfMissing)
    local content = _readFileContent(fileName, createIfMissing == true)
    if not content then
        return nil
    end

    local fn = loadstring(content)
    if not fn then
        return nil
    end

    local ok, result = pcall(fn)
    if not ok or type(result) ~= "table" then
        return nil
    end

    return result
end

local function _writeLuaTableFile(fileName, value)
    local writer = getFileWriter(fileName, true, false)
    if not writer then
        return false
    end

    writer:write("return " .. MS_PresetManager.serializeValue(value, 0))
    writer:close()
    return true
end


local function _copyStringArray(values)
    local copy = {}
    for index, value in ipairs(values or {}) do
        copy[index] = value
    end
    return copy
end

local function _sortPresetNames(names)
    table.sort(names, function(a, b)
        return string.lower(tostring(a)) < string.lower(tostring(b))
    end)
    return names
end

local function _ensureBundledPresetsLoaded()
    if not MS_BundledPresets then
        require "MS/MS_BundledPresets"
    end
    if type(MS_BundledPresets) == "table" and type(MS_BundledPresets.presets) == "table" then
        return MS_BundledPresets.presets
    end
    return {}
end

local function _skipWhitespace(content, index)
    local length = string.len(content)
    while index <= length do
        local char = string.sub(content, index, index)
        if char ~= " " and char ~= "\t" and char ~= "\n" and char ~= "\r" then
            break
        end
        index = index + 1
    end
    return index
end

local function _skipLuaString(content, index)
    local quote = string.sub(content, index, index)
    local length = string.len(content)
    index = index + 1
    while index <= length do
        local char = string.sub(content, index, index)
        if char == "\\" then
            index = index + 2
        elseif char == quote then
            return index + 1
        else
            index = index + 1
        end
    end
    return length + 1
end

local function _decodeLuaQuotedString(literal)
    local fn = loadstring("return " .. literal)
    if not fn then
        return nil
    end
    local ok, result = pcall(fn)
    if ok and type(result) == "string" then
        return result
    end
    return nil
end

local function _parseBracketStringKey(content, index)
    local length = string.len(content)
    if string.sub(content, index, index) ~= "[" then
        return nil, index
    end

    index = _skipWhitespace(content, index + 1)
    local quote = string.sub(content, index, index)
    if quote ~= '"' and quote ~= "'" then
        return nil, index
    end

    local stringStart = index
    local stringEndExclusive = _skipLuaString(content, index)
    local literal = string.sub(content, stringStart, stringEndExclusive - 1)
    index = _skipWhitespace(content, stringEndExclusive)
    if string.sub(content, index, index) ~= "]" then
        return nil, index
    end

    return _decodeLuaQuotedString(literal), index + 1
end

local function _parseIdentifierKey(content, index)
    local remainder = string.sub(content, index)
    local key = string.match(remainder, "^[%a_][%w_]*")
    if not key then
        return nil, index
    end
    return key, index + string.len(key)
end

local function _findPresetsTableStart(content)
    local _, afterEquals = string.find(content, "presets%s*=%s*", 1)
    if not afterEquals then
        return nil
    end
    local braceIndex = string.find(content, "{", afterEquals + 1, true)
    return braceIndex
end

local function _extractUserPresetNames(content)
    if type(content) ~= "string" or content == "" then
        return nil
    end

    local startBrace = _findPresetsTableStart(content)
    if not startBrace then
        return nil
    end

    local names = {}
    local index = startBrace + 1
    local depth = 1
    local length = string.len(content)

    while index <= length and depth > 0 do
        local char = string.sub(content, index, index)

        if char == '"' or char == "'" then
            index = _skipLuaString(content, index)
        elseif depth == 1 then
            index = _skipWhitespace(content, index)
            char = string.sub(content, index, index)

            while char == "," do
                index = _skipWhitespace(content, index + 1)
                char = string.sub(content, index, index)
            end

            if char == "}" then
                depth = depth - 1
                index = index + 1
            else
                local name = nil
                if char == "[" then
                    name, index = _parseBracketStringKey(content, index)
                else
                    name, index = _parseIdentifierKey(content, index)
                end

                if name then
                    index = _skipWhitespace(content, index)
                    if string.sub(content, index, index) == "=" then
                        index = _skipWhitespace(content, index + 1)
                        if string.sub(content, index, index) == "{" then
                            names[#names + 1] = name
                        end
                    end
                else
                    if char == "{" then
                        depth = depth + 1
                    elseif char == "}" then
                        depth = depth - 1
                    end
                    index = index + 1
                end
            end
        else
            if char == "{" then
                depth = depth + 1
            elseif char == "}" then
                depth = depth - 1
            end
            index = index + 1
        end
    end

    return names
end

function MS_PresetManager.getBundledPresetDisplayName(presetName)
    -- Cache translated display names so the panel does not repeat translation lookups while filtering.
    MS_PresetManager._bundledPresetDisplayNameCache = MS_PresetManager._bundledPresetDisplayNameCache or {}
    if MS_PresetManager._bundledPresetDisplayNameCache[presetName] then
        return MS_PresetManager._bundledPresetDisplayNameCache[presetName]
    end

    local key = BUNDLED_PRESET_NAME_KEYS[presetName]
    local displayName = presetName
    if key then
        displayName = tr(key, presetName)
    end

    MS_PresetManager._bundledPresetDisplayNameCache[presetName] = displayName
    return displayName
end

function MS_PresetManager.getBundledPresets()
    return _deepCopy(_ensureBundledPresetsLoaded())
end

function MS_PresetManager.getBundledPresetNames()
    -- Build a lightweight name index once.
    -- If the full preset table is not already loaded, use the static index instead of requiring MS_BundledPresets.
    if MS_PresetManager._bundledPresetNamesCache then
        return _copyStringArray(MS_PresetManager._bundledPresetNamesCache)
    end

    local names = {}
    if type(MS_BundledPresets) == "table" and type(MS_BundledPresets.presets) == "table" then
        for presetName, _ in pairs(MS_BundledPresets.presets) do
            names[#names + 1] = presetName
        end
    else
        for _, presetName in ipairs(BUNDLED_PRESET_NAME_INDEX) do
            names[#names + 1] = presetName
        end
    end

    _sortPresetNames(names)
    MS_PresetManager._bundledPresetNamesCache = names
    return _copyStringArray(names)
end

function MS_PresetManager.getBundledPresetByName(presetName)
    local preset = _ensureBundledPresetsLoaded()[presetName]
    if type(preset) == "table" then
        return _deepCopy(preset)
    end
    return nil
end

function MS_PresetManager.getUserPresets()
    local loaded = _readLuaTableFile(MS_PresetManager.USER_PRESETS_FILE, false)
    if type(loaded) == "table" and type(loaded.presets) == "table" then
        return loaded.presets
    end
    return {}
end

function MS_PresetManager.invalidateUserPresetNameCache()
    MS_PresetManager._userPresetNamesCache = nil
end

function MS_PresetManager.getUserPresetNames()
    -- Cache saved preset names after the first scan. The cache is invalidated when Modern Status saves/deletes presets.
    if MS_PresetManager._userPresetNamesCache then
        return _copyStringArray(MS_PresetManager._userPresetNamesCache)
    end

    -- Read only top-level saved preset names where possible, falling back to a full load if the file was edited by hand.
    local content = _readFileContent(MS_PresetManager.USER_PRESETS_FILE, false)
    local names = _extractUserPresetNames(content)
    if names then
        _sortPresetNames(names)
        MS_PresetManager._userPresetNamesCache = names
        return _copyStringArray(names)
    end

    names = MS_PresetManager.getSortedPresetNames(MS_PresetManager.getUserPresets())
    MS_PresetManager._userPresetNamesCache = names
    return _copyStringArray(names)
end

function MS_PresetManager.getUserPresetByName(presetName)
    local presets = MS_PresetManager.getUserPresets()
    if type(presets[presetName]) == "table" then
        return presets[presetName]
    end
    return nil
end

function MS_PresetManager.saveUserPresets(presets)
    local payload = {
        version = 1,
        presets = presets or {}
    }
    local ok = _writeLuaTableFile(MS_PresetManager.USER_PRESETS_FILE, payload)
    if ok and MS_PresetManager.invalidateUserPresetNameCache then
        MS_PresetManager.invalidateUserPresetNameCache()
    end
    return ok
end

function MS_PresetManager.captureCurrentConfig(playerNum)
    local config = MSConfig.loadConfig(playerNum)
    if not config then
        config = MSConfig.createDefaultConfig(playerNum)
    end
    return _deepCopy(config)
end

function MS_PresetManager.saveCurrentPreset(playerNum, presetName)
    if not presetName or presetName == "" then
        return false
    end

    local presets = MS_PresetManager.getUserPresets()
    presets[presetName] = MS_PresetManager.captureCurrentConfig(playerNum)
    return MS_PresetManager.saveUserPresets(presets)
end

function MS_PresetManager.deleteUserPreset(presetName)
    if not presetName or presetName == "" then
        return false
    end

    local presets = MS_PresetManager.getUserPresets()
    presets[presetName] = nil
    return MS_PresetManager.saveUserPresets(presets)
end

function MS_PresetManager.getSortedPresetNames(presets)
    local names = {}
    for presetName, _ in pairs(presets or {}) do
        names[#names + 1] = presetName
    end
    return _sortPresetNames(names)
end

local _MSPresetDeferredRefreshPlayers = {}

local function _runDeferredPresetRefresh()
    local pending = _MSPresetDeferredRefreshPlayers
    _MSPresetDeferredRefreshPlayers = {}

    if Events and Events.OnTick and Events.OnTick.Remove then
        Events.OnTick.Remove(_runDeferredPresetRefresh)
    end

    for playerNum, _ in pairs(pending) do
        MS_PresetManager.refreshRuntime(playerNum)
    end
end

local function _scheduleDeferredPresetRefresh(playerNum)
    -- Some UI elements can still be settling matrix/auto-hide runtime state when a preset
    -- is applied from a button callback. A one-tick settle pass prevents needing to load
    -- the same preset twice to get the final positions.
    _MSPresetDeferredRefreshPlayers[playerNum or 0] = true

    if Events and Events.OnTick and Events.OnTick.Add then
        if Events.OnTick.Remove then
            Events.OnTick.Remove(_runDeferredPresetRefresh)
        end
        Events.OnTick.Add(_runDeferredPresetRefresh)
    else
        _runDeferredPresetRefresh()
    end
end

function MS_PresetManager.refreshRuntime(playerNum)
    MSConfig.configCache = {}
    MSConfig.globalConfigCache = {}

    local control = StatusWidget and StatusWidget.globalControls and StatusWidget.globalControls[playerNum]
    local globalConfig = MSConfig.getGlobalConfig(playerNum)

    if control and globalConfig.controlPosition then
        control:setX(globalConfig.controlPosition.x or control:getX())
        control:setY(globalConfig.controlPosition.y or control:getY())
    end

    local indicators = StatusWidget and StatusWidget.indicators and StatusWidget.indicators[playerNum]
    if indicators then
        for _, indicator in pairs(indicators) do
            indicator:loadConfig()
        end
    end

    MS_MatrixManager.refreshPlayer(playerNum)

    if control and control.applyHideOriginalMoodles then
        control:applyHideOriginalMoodles(globalConfig.hideOriginalMoodles == true)
    end
end

function MS_PresetManager.applyPreset(playerNum, presetConfig)
    if type(presetConfig) ~= "table" then
        return false
    end

    local baseConfig = MSConfig.loadConfig(playerNum)
    if not baseConfig then
        baseConfig = MSConfig.createDefaultConfig(playerNum)
    else
        baseConfig = _deepCopy(baseConfig)
    end

    _deepMerge(baseConfig, presetConfig)
    _storePresetRestoreState(baseConfig)
    MSConfig.saveConfig(baseConfig, playerNum)
    MS_PresetManager.refreshRuntime(playerNum)
    _scheduleDeferredPresetRefresh(playerNum)
    return true
end

function MS_PresetManager.promptSavePreset(playerNum)
    local modal = ISTextBox:new(0, 0, 320, 180, tr("IGUI_ModernStatus_EnterPresetName", "Enter preset name:"), "", nil, function(_, button)
        if not button or button.internal ~= "OK" then
            return
        end

        local presetName = button.parent.entry:getText()
        if not presetName or presetName == "" then
            return
        end

        MS_PresetManager.saveCurrentPreset(playerNum, presetName)
    end)
    modal:initialise()
    modal:addToUIManager()
    return modal
end

return MS_PresetManager
