require "MS/MS_IndicatorOrder"
require "MS/MS_IndicatorVisualConfig"

MSConfig = {}
MSConfig.configCache = {}
MSConfig.globalConfigCache = {}

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

    return "[" .. MSConfig.serializeTable(key, nil, false, 0, nil) .. "]"
end

-- ---------------------------------------------------------------------------------------- --
-- serializeTable
-- ---------------------------------------------------------------------------------------- --
function MSConfig.serializeTable(val, name, skipnewlines, depth, path)
    skipnewlines = skipnewlines or false
    depth = depth or 0
    path = path or {}

    local tmp = string.rep("    ", depth)

    if name then
        tmp = tmp .. name .. " = "
    end

    if type(val) == "table" then
        tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")

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

            local fieldName = _serializeConfigKey(key)
            tmp = tmp .. MSConfig.serializeTable(val[key], fieldName, skipnewlines, depth + 1, childPath) .. "," .. (not skipnewlines and "\n" or "")
        end

        tmp = tmp .. string.rep("    ", depth) .. "}"
    elseif type(val) == "number" then
        tmp = tmp .. tostring(val)
    elseif type(val) == "string" then
        tmp = tmp .. string.format("%q", val)
    elseif type(val) == "boolean" then
        tmp = tmp .. (val and "true" or "false")
    else
        tmp = tmp .. "\"[" .. type(val) .. "]\""
    end

    return tmp
end

function MSConfig.saveConfig(config, playerNum)
    if playerNum == nil or playerNum == 0 then playerNum = "" end
    playerNum = tostring(playerNum)

    local file = getFileWriter("ModernStatusConfig"..playerNum..".lua", true, false)
    if file == nil then return nil end

    local contents = "return " .. MSConfig.serializeTable(config)
    file:write(contents)
    file:close()
end

function MSConfig.loadConfig(playerNum)
    if playerNum == nil or playerNum == 0 then playerNum = "" end
    playerNum = tostring(playerNum)

    local file = getFileReader("ModernStatusConfig"..playerNum..".lua", true)
    if file == nil then return nil end

    local content = ""
    local line = file:readLine()
    while line do
        content = content .. line .. "\n"
        line = file:readLine()
    end
    file:close()
    
    if content == "" then return nil end
    
    local fn, errorMsg = loadstring(content)
    if fn then
        local config = fn()
        return config
    else
        return nil
    end
end



local function getDefaultGridPosition(indicatorType)
    local orderIndex = MS_IndicatorOrder.getOriginalSortOrder(indicatorType)
    local maxKnownOrder = MS_IndicatorOrder.getOriginalOrderCount() + 1

    if not orderIndex or orderIndex >= 10000 then
        orderIndex = maxKnownOrder
    end

    local zeroBasedIndex = math.max(0, orderIndex - 1)
    local columns = 5
    local startX = 120
    local startY = 120
    local deltaX = 60
    local deltaY = 60

    local column = zeroBasedIndex % columns
    local row = math.floor(zeroBasedIndex / columns)

    return startX + (column * deltaX), startY + (row * deltaY)
end

-- ---------------------------------------------------------------------------------------- --
-- Defaul Config
-- ---------------------------------------------------------------------------------------- --
function MSConfig.getDefaultIndicatorConfig(indicatorType)
    local defaultWidth = 168
    local defaultHeight = 18
    local defaultCircularSize = 48
    local defaultStyle = "circular"
    local defaultColor = {r=1, g=1, b=1}
    
    local posX = 64
    local posY = 64
    
    -- Column 1 (posX=64)
    if string.find(indicatorType, "Thirst") then
        defaultColor = {r=0.55, g=0.75, b=0.85}
        posX = 64
        posY = 64
    elseif string.find(indicatorType, "Hunger") then
        defaultColor = {r=0.95, g=0.95, b=0.45}
        posX = 64
        posY = 124
    elseif string.find(indicatorType, "Endurance") then
        defaultColor = {r=0.85, g=0.85, b=0.85}
        posX = 64
        posY = 244
    elseif string.find(indicatorType, "Fatigue") then
        defaultColor = {r=0.7, g=0.65, b=0.55}
        posX = 64
        posY = 184
    elseif string.find(indicatorType, "Stress") then
        defaultColor = {r=0.6, g=0.6, b=0.6}
        posX = 64
        posY = 304
    elseif string.find(indicatorType, "Panic") then
        defaultColor = {r=0.95, g=0.65, b=0.4}
        posX = 64
        posY = 364
    elseif string.find(indicatorType, "Proteins") then
        defaultColor = {r=0.7, g=0.8, b=0.4}
        posX = 64
        posY = 424
    
    -- Column 2 (posX=124)
    elseif string.find(indicatorType, "Windchill") then
        defaultColor = {r=0.372549, g=0.623529, b=0.709804}
        posX = 124
        posY = 64
    elseif string.find(indicatorType, "Temperature") then
        defaultColor = {r=1.0, g=0.6, b=0.4}
        posX = 124
        posY = 64
    elseif string.find(indicatorType, "Health") then
        defaultColor = {r=1.0, g=0.6, b=0.6}
        posX = 124
        posY = 124
    elseif string.find(indicatorType, "Boredom") then
        defaultColor = {r=0.75, g=0.75, b=0.75}
        posX = 124
        posY = 184
    elseif string.find(indicatorType, "Unhappyness") then
        defaultColor = {r=0.6, g=0.6, b=0.9}
        posX = 124
        posY = 244
    elseif string.find(indicatorType, "Discomfort") then
        defaultColor = {r=0.65, g=0.65, b=0.9}
        posX = 124
        posY = 304
    elseif string.find(indicatorType, "Sickness") then
        defaultColor = {r=0.65, g=0.75, b=0.5}
        posX = 124
        posY = 364
    elseif string.find(indicatorType, "CatchACold") then
        defaultColor = {r=0.55, g=0.75, b=0.55}
        posX = 244
        posY = 364
    elseif string.find(indicatorType, "FoodHealing") then
        defaultColor = {r=0.392157, g=0.784314, b=0.470588}
        posX = 244
        posY = 424
    elseif string.find(indicatorType, "Poison") then
        defaultColor = {r=0.45, g=0.8, b=0.35}
        posX = 244
        posY = 304
    elseif string.find(indicatorType, "Carbohydrates") then
        defaultColor = {r=0.8, g=0.6, b=0.3}
        posX = 124
        posY = 424
    
    -- Column 3 (posX=184)
    elseif string.find(indicatorType, "Calorie") then
        defaultColor = {r=0.85, g=0.65, b=0.4}
        posX = 184
        posY = 64
    elseif string.find(indicatorType, "Pain") then
        defaultColor = {r=0.95, g=0.5, b=0.5}
        posX = 184
        posY = 124
    elseif string.find(indicatorType, "CarryWeight") then
        defaultColor = {r=0.6, g=0.4, b=0.2}
        posX = 184
        posY = 184 
    elseif string.find(indicatorType, "Lipids") then
        defaultColor = {r=0.9, g=0.8, b=0.2}
        posX = 184
        posY = 244
    elseif string.find(indicatorType, "Weight") then
        defaultColor = {r=0.0, g=1.0, b=0.0}
        posX = 184
        posY = 304
    elseif string.find(indicatorType, "Medicine") then
        defaultColor = {r=0.3, g=0.9, b=0.4}
        posX = 184
        posY = 364
    elseif string.find(indicatorType, "Dirtiness") then
        defaultColor = {r=0.5, g=0.4, b=0.3}
        posX = 184
        posY = 424
    elseif string.find(indicatorType, "Bloodiness") then
        defaultColor = {r=0.796078, g=0, b=0}
        posX = 184
        posY = 424

    -- Column 4 (posX=244)
    elseif string.find(indicatorType, "Anger") then
        defaultColor = {r=1, g=0.1, b=0.1}
        posX = 244
        posY = 64
    elseif string.find(indicatorType, "Wetness") then
        defaultColor = {r=0.4, g=0.6, b=0.9}
        posX = 244
        posY = 124
    elseif string.find(indicatorType, "Drunkenness") then
        defaultColor = {r=0.8, g=0.4, b=0.6}
        posX = 244
        posY = 184
    elseif string.find(indicatorType, "Stiffness") then
        defaultColor = {r=0.8, g=0.6, b=0.0}
        posX = 244
        posY = 244
    elseif string.find(indicatorType, "NoxiousExposure") then
        defaultColor = {r=0.79, g=0.71, b=0.35}
        posX = 304
        posY = 244
    end

    -- Use the shared original indicator order for the first-time layout grid.
    posX, posY = getDefaultGridPosition(indicatorType)

    -- Pull fixed base colors from the centralized visual config when available.
    -- This keeps first-time defaults and reset behavior in sync with bundled presets.
    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.getDefaultBaseColor then
        local configuredBaseColor = MS_IndicatorVisualConfig.getDefaultBaseColor(indicatorType)
        if configuredBaseColor then
            defaultColor = configuredBaseColor
        end
    end

    local config = {
        position = {x = posX, y = posY},
        circularSize = defaultCircularSize,
        barSize = {width = defaultWidth, height = defaultHeight},
        color = defaultColor,
        opacity = 1.0,
        iconOpacity = 1.0,
        style = defaultStyle,
        isVertical = false,
        showIcon = true,
        useGradient = false,
        iconPosition = "left",
        useMonoIcon = false,
        locked = false,
        hidden = false,
        alarmEnabled = false,
        animationThreshold = MS_IndicatorVisualConfig.getDefaultAlarmThreshold(indicatorType),
        alwaysShowValue = false,
        invertDirection = false,
        showName = true,
        textScale = 1.0,
        autoHide = false,
        autoHideThreshold = MS_IndicatorVisualConfig.getDefaultAutoHideThreshold(indicatorType),
        displayThresholds = false,
        updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType),
        matrixLinked = false,
        matrixLinkOrder = 0
    }
    
    if indicatorType == "ISWindchillStatusIndicator" or indicatorType == "MS_WindchillIndicator" then
        config.autoHide = false
        config.autoHideThreshold = 0
        config.animationThreshold = 5
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "ISFoodHealingStatusIndicator" then
        config.autoHide = false
        config.autoHideThreshold = 0
        config.alarmEnabled = false
        config.animationThreshold = 30
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "ISBloodinessStatusIndicator" or indicatorType == "MS_BloodinessIndicator" then
        config.autoHide = false
        config.autoHideThreshold = 0
        config.animationThreshold = 25
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "MS_BleedingIndicator" or indicatorType == "ISBleedingStatusIndicator" then
        config.alarmEnabled = true
        config.animationThreshold = 0
        config.autoHide = false
        config.autoHideThreshold = 0
    elseif indicatorType == "ISNCBladderStatusIndicator" or indicatorType == "ISNCBowelStatusIndicator" then
        -- Nature's Call fill meters are slow-changing, so they default to auto-hide and only reappear at warning level.
        config.autoHide = true
        config.autoHideThreshold = 70
        config.animationThreshold = 95
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "ISNCGutHealthStatusIndicator" then
        -- Gut Health is centered around the neutral value 5; auto-hide uses distance from that neutral point.
        config.autoHide = true
        config.autoHideThreshold = 0.5
        config.animationThreshold = 2
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "ISNCSoilageStatusIndicator" then
        -- Soilage usually matters only after accidents or missed cleaning, so it is hidden at 0 by default.
        config.autoHide = true
        config.autoHideThreshold = 0
        config.animationThreshold = 25
        config.displayThresholds = true
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    elseif indicatorType == "ISNCDigestiveEffectsStatusIndicator" then
        -- Digestive effects use a normalized severity value while the hover text can show time/intensity details.
        -- Threshold markers are off by default to avoid mixing visual severity with effect duration units.
        config.autoHide = true
        config.autoHideThreshold = 0
        config.animationThreshold = 0
        config.displayThresholds = false
        config.alwaysShowValue = true
        config.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicatorType)
    end

    return config
end

function MSConfig.getDefaultMatrixConfig()
    return {
        columns = 5,
        sortDirection = "rows",
        x = 420,
        y = 120,
        deltaX = 60,
        deltaY = 60,
        width = 48,
        height = 48,
        opacity = 1.0,
        useGradient = false,
        showIcon = true,
        showName = true,
        alwaysShowValue = false,
        displayThresholds = false,
        invertDirection = false,
        iconPosition = "left",
        textScale = 1.0,
        useMonoIcon = false,
        iconOpacity = 1.0,
        alarmEnabled = false,
        animationThreshold = 20,
        autoHide = false,
        autoHideThreshold = 0,
        updateIntervalMs = 500,
        mode = "circular",
        hidden = false,
        locked = false,
        keepLayoutOnRelease = false,
        compactHiddenIndicators = false,
        nextLinkOrder = 1
    }
end

function MSConfig.getDefaultGlobalConfig()
    return {
        controlPosition = {x = 120, y = 60},
        performanceMode = "balanced",
        multiDragMode = false,
        hideOriginalMoodles = false,
        hideVanillaMoodles = false,
        gridSettings = {
            enabled = true,
            sizeX = 10,
            sizeY = 10
        },
        tooltipSettings = {
            showName = true,
            showValue = true,
            showThresholds = true,
            showAssociatedMoodle = true,
            showMoodleTitle = true,
            showMoodleDescription = true,
            showDetailedInfo = true,
            opacity = 90
        },
        matrixSettings = MSConfig.getDefaultMatrixConfig(),
        allIndicatorsSettings = {
            sortMode = "original",
            visibilityMode = "all",
            customOrder = {}
        },
        panelCollapseSettings = {
            AllIndicatorsPanel = { collapsed = true },
            MatrixStylePanel = { collapsed = true },
            UnlinkedStylePanel = { collapsed = true },
            BarStylePanel = { collapsed = true },
            CircularStylePanel = { collapsed = true },
            GlobalSettingsPanel = { collapsed = true },
        }
    }
end

function MSConfig.createDefaultConfig(playerNum)
    local config = {
        globalSettings = MSConfig.getDefaultGlobalConfig(),
        indicators = {}
    }
    
    local indicatorTypes = {
        "MS_BleedingIndicator",
        "MS_CarryWeightIndicator",
        "MS_HealthIndicator",
        "MS_HungerIndicator",
        "MS_ThirstIndicator",
        "MS_EnduranceIndicator",
        "MS_FatigueIndicator",
        "MS_StressIndicator",
        "MS_PanicIndicator",
        "MS_BoredomIndicator",
        "MS_DiscomfortIndicator",
        "MS_UnhappynessIndicator",
        "MS_AngerIndicator",
        "MS_DrunkennessIndicator",
        "MS_MedicineIndicator",
        "MS_StiffnessIndicator",
        "MS_PainIndicator",
        "MS_PoisonIndicator",
        "MS_NoxiousExposureIndicator",
        "MS_SicknessIndicator",
        "MS_WetnessIndicator",
        "ISWindchillStatusIndicator",
        "MS_TemperatureIndicator",
        "MS_CatchAColdIndicator",
        "ISFoodHealingStatusIndicator",
        "MS_WeightIndicator",
        "MS_CalorieIndicator",
        "MS_CarbohydratesIndicator",
        "MS_ProteinsIndicator",
        "MS_LipidsIndicator",
        "MS_DirtinessIndicator",
        "ISBloodinessStatusIndicator",
    }
    
    for _, indicatorType in ipairs(indicatorTypes) do
        config.indicators[indicatorType] = MSConfig.getDefaultIndicatorConfig(indicatorType)
    end
    
    return config
end

-- ---------------------------------------------------------------------------------------- --
-- Config update &Access
-- ---------------------------------------------------------------------------------------- --
local function mergeMissingConfigValues(target, defaults)
    local changed = false

    for key, value in pairs(defaults) do
        if type(value) == "table" then
            if type(target[key]) ~= "table" then
                target[key] = {}
                changed = true
            end

            if mergeMissingConfigValues(target[key], value) then
                changed = true
            end
        elseif target[key] == nil then
            target[key] = value
            changed = true
        end
    end

    return changed
end

function MSConfig.getIndicatorConfig(playerNum, indicatorType)
    local cacheKey = tostring(playerNum) .. "_" .. indicatorType

    if MSConfig.configCache[cacheKey] then
        return MSConfig.configCache[cacheKey]
    end

    local config = MSConfig.loadConfig(playerNum)

    if not config then
        config = MSConfig.createDefaultConfig(playerNum)
        MSConfig.saveConfig(config, playerNum)
    end

    if not config.indicators[indicatorType] then
        config.indicators[indicatorType] = MSConfig.getDefaultIndicatorConfig(indicatorType)
        MSConfig.saveConfig(config, playerNum)
    else
        local defaultConfig = MSConfig.getDefaultIndicatorConfig(indicatorType)
        if mergeMissingConfigValues(config.indicators[indicatorType], defaultConfig) then
            MSConfig.saveConfig(config, playerNum)
        end
    end

    MSConfig.configCache[cacheKey] = config.indicators[indicatorType]

    return config.indicators[indicatorType]
end

function MSConfig.updateIndicatorConfig(playerNum, indicatorType, key, value)
    local cacheKey = tostring(playerNum) .. "_" .. indicatorType
    MSConfig.configCache[cacheKey] = nil

    local config = MSConfig.loadConfig(playerNum)
    
    if not config then
        config = MSConfig.createDefaultConfig(playerNum)
    end
    
    if not config.indicators[indicatorType] then
        config.indicators[indicatorType] = MSConfig.getDefaultIndicatorConfig(indicatorType)
    end
    
    config.indicators[indicatorType][key] = value
    MSConfig.saveConfig(config, playerNum)
end

function MSConfig.getGlobalConfig(playerNum)
    local cacheKey = tostring(playerNum) .. "_global"

    if MSConfig.globalConfigCache[cacheKey] then
        return MSConfig.globalConfigCache[cacheKey]
    end

    local config = MSConfig.loadConfig(playerNum)

    if not config or not config.globalSettings then
        config = MSConfig.createDefaultConfig(playerNum)
        MSConfig.saveConfig(config, playerNum)
    else
        local defaultGlobal = MSConfig.getDefaultGlobalConfig()
        if mergeMissingConfigValues(config.globalSettings, defaultGlobal) then
            MSConfig.saveConfig(config, playerNum)
        end
    end

    MSConfig.globalConfigCache[cacheKey] = config.globalSettings

    return config.globalSettings
end

function MSConfig.getMatrixConfig(playerNum)
    local globalConfig = MSConfig.getGlobalConfig(playerNum)

    if not globalConfig.matrixSettings then
        globalConfig.matrixSettings = MSConfig.getDefaultMatrixConfig()
        MSConfig.updateGlobalConfig(playerNum, "matrixSettings", globalConfig.matrixSettings)
    end

    return globalConfig.matrixSettings
end

function MSConfig.updateMatrixConfig(playerNum, key, value)
    local globalConfig = MSConfig.getGlobalConfig(playerNum)
    local matrixConfig = globalConfig.matrixSettings or MSConfig.getDefaultMatrixConfig()
    matrixConfig[key] = value
    MSConfig.updateGlobalConfig(playerNum, "matrixSettings", matrixConfig)
end

function MSConfig.updateGlobalConfig(playerNum, key, value)
    local cacheKey = tostring(playerNum) .. "_global"
    MSConfig.globalConfigCache[cacheKey] = nil
    
    local config = MSConfig.loadConfig(playerNum)
    
    if not config then
        config = MSConfig.createDefaultConfig(playerNum)
    end
    
    if not config.globalSettings then
        config.globalSettings = MSConfig.getDefaultGlobalConfig()
    end
    
    config.globalSettings[key] = value
    MSConfig.saveConfig(config, playerNum)
end

return MSConfig