require "MS/MS_IndicatorOrder"
MS_MatrixManager = MS_MatrixManager or {}

local INDICATOR_TITLE_KEYS = {
    ["MS_ThirstIndicator"] = "IGUI_ModernStatus_Thirst",
    ["MS_HungerIndicator"] = "IGUI_ModernStatus_Hunger",
    ["MS_EnduranceIndicator"] = "IGUI_ModernStatus_Endurance",
    ["MS_FatigueIndicator"] = "IGUI_ModernStatus_Fatigue",
    ["MS_StressIndicator"] = "IGUI_ModernStatus_Stress",
    ["MS_PanicIndicator"] = "IGUI_ModernStatus_Panic",
    ["MS_TemperatureIndicator"] = "IGUI_ModernStatus_Temperature",
    ["ISWindchillStatusIndicator"] = "IGUI_ModernStatus_Windchill",
    ["MS_WindchillIndicator"] = "IGUI_ModernStatus_Windchill",
    ["MS_HealthIndicator"] = "IGUI_ModernStatus_Health",
    ["MS_BoredomIndicator"] = "IGUI_ModernStatus_Boredom",
    ["MS_UnhappynessIndicator"] = "IGUI_ModernStatus_Unhappyness",
    ["MS_DiscomfortIndicator"] = "IGUI_ModernStatus_Discomfort",
    ["MS_SicknessIndicator"] = "IGUI_ModernStatus_Sickness",
    ["MS_PoisonIndicator"] = "IGUI_ModernStatus_Poison",
    ["MS_PainIndicator"] = "IGUI_ModernStatus_Pain",
    ["MS_BleedingIndicator"] = "IGUI_ModernStatus_Bleeding",
    ["MS_CarryWeightIndicator"] = "IGUI_invpanel_weight",
    ["ISCarryWeightStatusIndicator"] = "IGUI_invpanel_weight",
    ["MS_CalorieIndicator"] = "IGUI_ModernStatus_Calorie",
    ["MS_ProteinsIndicator"] = "IGUI_ModernStatus_Proteins",
    ["MS_CarbohydratesIndicator"] = "IGUI_ModernStatus_Carbohydrates",
    ["MS_LipidsIndicator"] = "IGUI_ModernStatus_Lipids",
    ["ISFoodHealingStatusIndicator"] = "IGUI_ModernStatus_FoodHealing",
    ["MS_FoodHealingIndicator"] = "IGUI_ModernStatus_FoodHealing",
    ["MS_WeightIndicator"] = "IGUI_ModernStatus_Weight",
    ["MS_MedicineIndicator"] = "IGUI_ModernStatus_Medicine",
    ["MS_AngerIndicator"] = "IGUI_ModernStatus_Anger",
    ["MS_DirtinessIndicator"] = "IGUI_ModernStatus_Dirtiness",
    ["ISBloodinessStatusIndicator"] = "IGUI_ModernStatus_Bloodiness",
    ["MS_BloodinessIndicator"] = "IGUI_ModernStatus_Bloodiness",
    ["MS_WetnessIndicator"] = "IGUI_ModernStatus_Wetness",
    ["MS_DrunkennessIndicator"] = "IGUI_ModernStatus_Drunkenness",
    ["MS_StiffnessIndicator"] = "IGUI_ModernStatus_Stiffness",
}

local INDICATOR_TITLE_OVERRIDES = {
    ["ISWORCombinedStatusIndicator"] = "IGUI_ModernStatus_WORGrime",
}

local INDICATOR_TITLE_SUFFIXES = {
    ["ISTABASBodyGrimeStatusIndicator"] = " (TABAS)",
    ["ISWORCombinedStatusIndicator"] = " (WOR)",
    ["ISBathroomNeedStatusIndicator"] = " (LS)",
    ["ISHygieneNeedStatusIndicator"] = " (LS)",
    ["ISExcStomachStatusIndicator"] = " (EXC)",
    ["ISExcColonStatusIndicator"] = " (EXC)",
    ["ISExcUrineStatusIndicator"] = " (EXC)",
    ["ISExcSmellStatusIndicator"] = " (EXC)",
    ["ISExcShameStatusIndicator"] = " (EXC)",
}

function MS_MatrixManager.getIndicatorTitleSuffix(indicatorType)
    return INDICATOR_TITLE_SUFFIXES[indicatorType] or ""
end

function MS_MatrixManager.getIndicatorDisplayTitle(indicatorType, fallbackName)
    local overrideTitle = INDICATOR_TITLE_OVERRIDES[indicatorType]
    if overrideTitle then
        local titleText = getText(overrideTitle)
        return tostring(titleText or overrideTitle) .. MS_MatrixManager.getIndicatorTitleSuffix(indicatorType)
    end

    local titleKey = INDICATOR_TITLE_KEYS[indicatorType]
    if not titleKey and MS_IndicatorSettingsPanel and MS_IndicatorSettingsPanel.IndicatorNames then
        titleKey = MS_IndicatorSettingsPanel.IndicatorNames[indicatorType]
    end

    local titleText = titleKey and getText(titleKey) or fallbackName or indicatorType
    return tostring(titleText or indicatorType or "") .. MS_MatrixManager.getIndicatorTitleSuffix(indicatorType)
end

function MS_MatrixManager.getModeIconTexture(mode)
    if mode == "horizontal" then
        return getTexture("media/ui/ModernStatus/ICON/Icon_Horizontal_Bar.png")
    end
    if mode == "vertical" then
        return getTexture("media/ui/ModernStatus/ICON/Icon_Vertical_Bar.png")
    end
    return getTexture("media/ui/ModernStatus/ICON/Icon_Circular.png")
end

function MS_MatrixManager.getNextMode(mode)
    if mode == "circular" then
        return "horizontal"
    end
    if mode == "horizontal" then
        return "vertical"
    end
    return "circular"
end

function MS_MatrixManager.getMatrixConfig(playerIndex)
    return MSConfig.getMatrixConfig(playerIndex)
end

function MS_MatrixManager.isIndicatorLinked(indicator)
    return indicator and indicator.matrixLinked == true
end

function MS_MatrixManager.hasLinkedIndicators(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then return false end

    for _, indicator in pairs(indicators) do
        if indicator.matrixLinked then
            return true
        end
    end

    return false
end

local function getOriginalSortOrder(indicatorType)
    if MS_IndicatorOrder and MS_IndicatorOrder.getOriginalSortOrder then
        return MS_IndicatorOrder.getOriginalSortOrder(indicatorType)
    end
    return math.huge
end

function MS_MatrixManager.getIndicatorsInAllIndicatorsOrder(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then
        return {}
    end

    local sortedIndicators = {}
    for _, indicator in pairs(indicators) do
        local indicatorType = indicator:getType()
        local titleText = MS_MatrixManager.getIndicatorDisplayTitle(indicatorType, indicatorType)
        table.insert(sortedIndicators, {
            indicator = indicator,
            titleText = titleText,
            indicatorType = indicatorType,
        })
    end

    local globalConfig = MSConfig.getGlobalConfig(playerIndex) or {}
    local listSettings = globalConfig.allIndicatorsSettings or {}
    local mode = listSettings.sortMode or "alphabetical"
    local customOrderIndex = nil

    if mode == "custom" then
        customOrderIndex = {}
        for index, indicatorType in ipairs(listSettings.customOrder or {}) do
            if customOrderIndex[indicatorType] == nil then
                customOrderIndex[indicatorType] = index
            end
        end
    end

    table.sort(sortedIndicators, function(a, b)
        if mode == "original" then
            local orderA = getOriginalSortOrder(a.indicatorType)
            local orderB = getOriginalSortOrder(b.indicatorType)
            if orderA ~= orderB then
                return orderA < orderB
            end
        elseif mode == "custom" then
            local orderA = customOrderIndex and customOrderIndex[a.indicatorType] or nil
            local orderB = customOrderIndex and customOrderIndex[b.indicatorType] or nil
            if orderA and orderB and orderA ~= orderB then
                return orderA < orderB
            end
            if orderA and not orderB then
                return true
            end
            if orderB and not orderA then
                return false
            end
        end

        if a.titleText == b.titleText then
            return a.indicatorType < b.indicatorType
        end
        return a.titleText < b.titleText
    end)

    local ordered = {}
    for _, item in ipairs(sortedIndicators) do
        ordered[#ordered + 1] = item.indicator
    end
    return ordered
end

function MS_MatrixManager.getOrderedIndicators(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then
        return {}
    end

    local ordered = {}
    for _, indicator in pairs(indicators) do
        local indicatorType = indicator:getType()
        local titleText = MS_MatrixManager.getIndicatorDisplayTitle(indicatorType, indicatorType)
        table.insert(ordered, {
            indicator = indicator,
            titleText = titleText,
            indicatorType = indicatorType
        })
    end

    table.sort(ordered, function(a, b)
        if a.titleText == b.titleText then
            return a.indicatorType < b.indicatorType
        end
        return a.titleText < b.titleText
    end)

    return ordered
end

function MS_MatrixManager.ensureLinkedOrderIntegrity(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then return end

    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    local nextLinkOrder = math.max(1, math.floor(matrixConfig.nextLinkOrder or 1))
    local changed = false
    local missing = {}

    for _, item in ipairs(MS_MatrixManager.getOrderedIndicators(playerIndex)) do
        local indicator = item.indicator
        if indicator.matrixLinked then
            local linkOrder = tonumber(indicator.matrixLinkOrder) or 0
            if linkOrder <= 0 then
                table.insert(missing, indicator)
            elseif linkOrder >= nextLinkOrder then
                nextLinkOrder = linkOrder + 1
            end
        end
    end

    for _, indicator in ipairs(missing) do
        indicator.matrixLinkOrder = nextLinkOrder
        MSConfig.updateIndicatorConfig(playerIndex, indicator:getType(), "matrixLinkOrder", indicator.matrixLinkOrder)
        nextLinkOrder = nextLinkOrder + 1
        changed = true
    end

    if changed or matrixConfig.nextLinkOrder ~= nextLinkOrder then
        matrixConfig.nextLinkOrder = nextLinkOrder
        MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", matrixConfig)
    end
end

function MS_MatrixManager.getLinkedIndicators(playerIndex, includeHidden)
    MS_MatrixManager.ensureLinkedOrderIntegrity(playerIndex)

    local linked = {}
    for _, item in ipairs(MS_MatrixManager.getOrderedIndicators(playerIndex)) do
        local indicator = item.indicator
        if indicator.matrixLinked and (includeHidden or not indicator.hidden) then
            table.insert(linked, {
                indicator = indicator,
                titleText = item.titleText,
                indicatorType = item.indicatorType,
                matrixLinkOrder = tonumber(indicator.matrixLinkOrder) or 0
            })
        end
    end

    table.sort(linked, function(a, b)
        if a.matrixLinkOrder == b.matrixLinkOrder then
            if a.titleText == b.titleText then
                return a.indicatorType < b.indicatorType
            end
            return a.titleText < b.titleText
        end
        return a.matrixLinkOrder < b.matrixLinkOrder
    end)

    local orderedIndicators = {}
    for _, item in ipairs(linked) do
        table.insert(orderedIndicators, item.indicator)
    end

    return orderedIndicators
end

local function supportsDirectionInversionForIndicator(indicator)
    return indicator and indicator.getType and MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.supportsDirectionInversion and MS_IndicatorVisualConfig.supportsDirectionInversion(indicator:getType())
end

local function supportsThresholdsForIndicator(indicator)
    return indicator and indicator.getType and MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.supportsThresholds and MS_IndicatorVisualConfig.supportsThresholds(indicator:getType())
end

local EPSILON = 0.0001

local STRUCTURAL_MATRIX_KEYS = {
    columns = true,
    sortDirection = true,
    x = true,
    y = true,
    deltaX = true,
    deltaY = true,
    width = true,
    height = true,
    mode = true,
    hidden = true,
    locked = true,
    compactHiddenIndicators = true,
    nextLinkOrder = true,
}

local SHARED_MATRIX_KEYS = {
    opacity = true,
    useGradient = true,
    showIcon = true,
    showName = true,
    alwaysShowValue = true,
    displayThresholds = true,
    invertDirection = true,
    iconPosition = true,
    textScale = true,
    useMonoIcon = true,
    iconOpacity = true,
    alarmEnabled = true,
    animationThreshold = true,
    autoHide = true,
    autoHideThreshold = true,
    updateIntervalMs = true,
}

local function valuesEqual(a, b)
    if type(a) == "number" and type(b) == "number" then
        return math.abs(a - b) < EPSILON
    end
    return a == b
end

local function summarizeIndicators(indicators, getter, defaultValue)
    if not indicators or #indicators == 0 then
        return defaultValue, false, 0
    end

    local firstValue = getter(indicators[1])
    for i = 2, #indicators do
        if not valuesEqual(firstValue, getter(indicators[i])) then
            return defaultValue, true, #indicators
        end
    end

    return firstValue, false, #indicators
end

function MS_MatrixManager.hasInvertibleLinkedIndicators(playerIndex)
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, true)) do
        if supportsDirectionInversionForIndicator(indicator) then
            return true
        end
    end

    return false
end

function MS_MatrixManager.getUnlinkedIndicators(playerIndex)
    local unlinked = {}
    for _, item in ipairs(MS_MatrixManager.getOrderedIndicators(playerIndex)) do
        local indicator = item.indicator
        if not indicator.matrixLinked then
            table.insert(unlinked, indicator)
        end
    end
    return unlinked
end

function MS_MatrixManager.applyStyleToIndicator(indicator, matrixConfig)
    if not indicator or not matrixConfig then return end

    -- Matrix linking only imposes shared layout/style primitives immediately.
    -- Other per-indicator style values are preserved until the user edits them
    -- from the matrix controls, at which point they are pushed in bulk.
    local mode = matrixConfig.mode or "circular"
    indicator.isBarStyle = mode ~= "circular"
    indicator.isVertical = mode == "vertical"

    if mode == "circular" then
        local size = math.max(8, math.floor(matrixConfig.width or matrixConfig.height or 48))
        indicator:setWidth(size)
        indicator:setHeight(size)
    else
        indicator:setWidth(matrixConfig.width)
        indicator:setHeight(matrixConfig.height)
    end
end

function MS_MatrixManager.applyMatrixRuntimeToIndicator(indicator, matrixConfig)
    if not indicator or not matrixConfig then return end

    -- Matrix runtime only imposes structural layout/style. Per-indicator
    -- states such as hidden/locked/value/icon/auto-hide stay owned by the indicator.
    MS_MatrixManager.applyStyleToIndicator(indicator, matrixConfig)
    indicator.moveWithMouse = not (indicator.locked == true)
    indicator:setVisible(indicator.hidden ~= true)
end

function MS_MatrixManager.isCompactHiddenIndicatorsEnabled(playerIndex)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    return matrixConfig and matrixConfig.compactHiddenIndicators == true
end

function MS_MatrixManager.doesIndicatorReserveMatrixSlot(indicator, matrixConfig)
    if not indicator or indicator.matrixLinked ~= true then
        return false
    end

    -- When disabled, all linked indicators keep their matrix slot even if they
    -- are manually hidden or auto-hidden. This preserves fixed layouts.
    if not matrixConfig or matrixConfig.compactHiddenIndicators ~= true then
        return true
    end

    -- When enabled, hidden and auto-hidden indicators stop reserving a slot,
    -- so later visible indicators move up to fill the gap while preserving order.
    return indicator.hidden ~= true and indicator._matrixAutoHidden ~= true
end

function MS_MatrixManager.notifyIndicatorMatrixVisibilityChanged(indicator)
    if not indicator or indicator.matrixLinked ~= true then
        return
    end

    local playerIndex = indicator.playerIndex
    if not playerIndex or not MS_MatrixManager.isCompactHiddenIndicatorsEnabled(playerIndex) then
        return
    end

    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

local MATRIX_HIDDEN_RUNTIME_X = -10000
local MATRIX_HIDDEN_RUNTIME_Y = -10000

local function clearMatrixIndicatorInteraction(indicator)
    if not indicator then return end

    -- A compacted/auto-hidden indicator must not keep stale hover, capture or tooltip state.
    indicator.isHovered = false
    indicator.moving = false
    indicator.matrixDragging = false

    if indicator.setCapture then
        indicator:setCapture(false)
    end
    if indicator.hideTooltipUI then
        indicator:hideTooltipUI()
    end
end

function MS_MatrixManager.moveNonReservedIndicatorOutOfHitArea(indicator, hiddenIndex)
    if not indicator then return end

    clearMatrixIndicatorInteraction(indicator)

    if indicator.hidden == true then
        -- Manually hidden indicators do not need to keep rendering.
        indicator:setVisible(false)
        return
    end

    -- Auto-hidden indicators still need render/update calls so they can become visible again,
    -- but they must be outside the playable UI area to avoid stealing hover/click events
    -- from visible indicators that moved into their old matrix slot.
    indicator:setVisible(true)
    indicator:setX(MATRIX_HIDDEN_RUNTIME_X - (hiddenIndex or 0) * 10)
    indicator:setY(MATRIX_HIDDEN_RUNTIME_Y - (hiddenIndex or 0) * 10)
end

function MS_MatrixManager.applyMatrixLayout(playerIndex)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then return end

    local linkedAll = MS_MatrixManager.getLinkedIndicators(playerIndex, true)

    -- Apply the shared matrix runtime state without overwriting each indicator's saved config.
    local linkedLayout = {}
    local hiddenRuntimeIndex = 0
    for _, indicator in ipairs(linkedAll) do
        MS_MatrixManager.applyMatrixRuntimeToIndicator(indicator, matrixConfig)
        if MS_MatrixManager.doesIndicatorReserveMatrixSlot(indicator, matrixConfig) then
            table.insert(linkedLayout, indicator)
        else
            hiddenRuntimeIndex = hiddenRuntimeIndex + 1
            MS_MatrixManager.moveNonReservedIndicatorOutOfHitArea(indicator, hiddenRuntimeIndex)
        end
    end

    local columns = math.max(1, math.floor(matrixConfig.columns or 1))
    local total = #linkedLayout
    if total == 0 then return end

    local rows = math.max(1, math.ceil(total / columns))

    for index, indicator in ipairs(linkedLayout) do
        local zeroIndex = index - 1
        local col = 0
        local row = 0

        if matrixConfig.sortDirection == "columns" then
            row = zeroIndex % rows
            col = math.floor(zeroIndex / rows)
        else
            col = zeroIndex % columns
            row = math.floor(zeroIndex / columns)
        end

        indicator:setX(matrixConfig.x + col * matrixConfig.deltaX)
        indicator:setY(matrixConfig.y + row * matrixConfig.deltaY)
    end
end

function MS_MatrixManager.restoreIndicator(indicator)
    if not indicator then return end
    indicator:loadConfig()
end

local function saveCurrentLayoutToIndicatorConfig(indicator)
    if not indicator or not indicator.playerIndex or not indicator.getType then
        return
    end

    local playerIndex = indicator.playerIndex
    local indicatorType = indicator:getType()

    -- Persist only the currently displayed structural layout so releasing from
    -- the matrix can either keep the live matrix layout or restore the saved one.
    MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "position", {
        x = math.floor(indicator:getX() or 0),
        y = math.floor(indicator:getY() or 0),
    })

    if indicator.isBarStyle then
        MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "style", "bar")
        MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "isVertical", indicator.isVertical == true)
        MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "barSize", {
            width = math.floor(indicator:getWidth() or 0),
            height = math.floor(indicator:getHeight() or 0),
        })
    else
        MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "style", "circular")
        MSConfig.updateIndicatorConfig(playerIndex, indicatorType, "circularSize", math.max(8, math.floor(math.max(indicator:getWidth() or 0, indicator:getHeight() or 0))))
    end
end

function MS_MatrixManager.refreshPlayer(playerIndex)
    local indicators = StatusWidget.indicators[playerIndex]
    if not indicators then return end

    for _, indicator in pairs(indicators) do
        if indicator.matrixLinked then
            MS_MatrixManager.applyMatrixRuntimeToIndicator(indicator, MS_MatrixManager.getMatrixConfig(playerIndex))
        else
            MS_MatrixManager.restoreIndicator(indicator)
        end
    end

    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

function MS_MatrixManager.setIndicatorLinked(playerIndex, indicator, linked, skipLayout)
    if not indicator then return end

    local shouldLink = linked == true
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)

    indicator.matrixLinked = shouldLink
    MSConfig.updateIndicatorConfig(playerIndex, indicator:getType(), "matrixLinked", indicator.matrixLinked)

    if shouldLink then
        local nextLinkOrder = math.max(1, math.floor(matrixConfig.nextLinkOrder or 1))
        indicator.matrixLinkOrder = nextLinkOrder
        matrixConfig.nextLinkOrder = nextLinkOrder + 1
        MSConfig.updateIndicatorConfig(playerIndex, indicator:getType(), "matrixLinkOrder", indicator.matrixLinkOrder)
        MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", matrixConfig)
        MS_MatrixManager.applyMatrixRuntimeToIndicator(indicator, matrixConfig)
    else
        if matrixConfig.keepLayoutOnRelease == true then
            saveCurrentLayoutToIndicatorConfig(indicator)
        end
        indicator.matrixLinkOrder = 0
        MSConfig.updateIndicatorConfig(playerIndex, indicator:getType(), "matrixLinkOrder", 0)
        MS_MatrixManager.restoreIndicator(indicator)
    end

    if not skipLayout then
        MS_MatrixManager.applyMatrixLayout(playerIndex)
    end
end

function MS_MatrixManager.releaseAllLinkedIndicators(playerIndex)
    local changed = false
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, true)) do
        MS_MatrixManager.setIndicatorLinked(playerIndex, indicator, false, true)
        changed = true
    end
    if changed then
        MS_MatrixManager.applyMatrixLayout(playerIndex)
    end
end

function MS_MatrixManager.addAllUnlinkedIndicators(playerIndex)
    local changed = false
    for _, indicator in ipairs(MS_MatrixManager.getIndicatorsInAllIndicatorsOrder(playerIndex)) do
        if indicator and not indicator.matrixLinked then
            MS_MatrixManager.setIndicatorLinked(playerIndex, indicator, true, true)
            changed = true
        end
    end
    if changed then
        MS_MatrixManager.applyMatrixLayout(playerIndex)
    end
end

function MS_MatrixManager.cycleMatrixMode(playerIndex)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    local currentMode = matrixConfig.mode or "circular"
    local nextMode = MS_MatrixManager.getNextMode(currentMode)

    if nextMode == "vertical" or nextMode == "horizontal" then
        local oldDeltaX = matrixConfig.deltaX
        matrixConfig.deltaX = matrixConfig.deltaY
        matrixConfig.deltaY = oldDeltaX

        local oldWidth = matrixConfig.width
        matrixConfig.width = matrixConfig.height
        matrixConfig.height = oldWidth
    end

    matrixConfig.mode = nextMode
    if nextMode == "vertical" then
        if matrixConfig.iconPosition ~= "top" and matrixConfig.iconPosition ~= "bottom" then
            matrixConfig.iconPosition = "bottom"
        end
    elseif nextMode == "horizontal" then
        if matrixConfig.iconPosition ~= "left" and matrixConfig.iconPosition ~= "right" then
            matrixConfig.iconPosition = "left"
        end
    end
    MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", matrixConfig)
    MS_MatrixManager.refreshPlayer(playerIndex)
    return nextMode
end

function MS_MatrixManager.getEditableLinkedIndicators(playerIndex, includeHidden)
    local result = {}
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, includeHidden == true)) do
        if indicator.locked ~= true then
            table.insert(result, indicator)
        end
    end
    return result
end

function MS_MatrixManager.getEditableInvertibleLinkedIndicators(playerIndex, includeHidden)
    local result = {}
    for _, indicator in ipairs(MS_MatrixManager.getEditableLinkedIndicators(playerIndex, includeHidden)) do
        if supportsDirectionInversionForIndicator(indicator) then
            table.insert(result, indicator)
        end
    end
    return result
end

function MS_MatrixManager.getEditableThresholdLinkedIndicators(playerIndex, includeHidden)
    local result = {}
    for _, indicator in ipairs(MS_MatrixManager.getEditableLinkedIndicators(playerIndex, includeHidden)) do
        if supportsThresholdsForIndicator(indicator) then
            table.insert(result, indicator)
        end
    end
    return result
end

function MS_MatrixManager.hasThresholdLinkedIndicators(playerIndex)
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, true)) do
        if supportsThresholdsForIndicator(indicator) then
            return true
        end
    end
    return false
end

function MS_MatrixManager.summarizeLinkedIndicators(playerIndex, getter, defaultValue, indicatorList)
    local indicators = indicatorList or MS_MatrixManager.getEditableLinkedIndicators(playerIndex, true)
    return summarizeIndicators(indicators, getter, defaultValue)
end

function MS_MatrixManager.isStructuralMatrixKey(key)
    return STRUCTURAL_MATRIX_KEYS[key] == true
end

function MS_MatrixManager.isSharedMatrixKey(key)
    return SHARED_MATRIX_KEYS[key] == true
end

local function applySharedMatrixValueToIndicator(indicator, key, value)
    if not indicator then return end

    if key == "invertDirection" then
        if not supportsDirectionInversionForIndicator(indicator) then
            return
        end
        indicator.invertDirection = value == true
    elseif key == "displayThresholds" then
        if not supportsThresholdsForIndicator(indicator) then
            return
        end
        indicator.displayThresholds = value == true
    elseif key == "showName" then
        indicator.showName = value == true
    elseif key == "useMonoIcon" then
        indicator.useMonoIcon = value == true
        if indicator.updateIconPath then
            indicator:updateIconPath()
        end
    elseif key == "alarmEnabled" then
        indicator.alarmEnabled = value ~= false
    elseif key == "autoHide" then
        indicator.autoHide = value == true
    elseif key == "showIcon" then
        indicator.showIcon = value == true
    elseif key == "alwaysShowValue" then
        indicator.alwaysShowValue = value == true
    elseif key == "useGradient" then
        indicator.useGradient = value == true
    else
        indicator[key] = value
    end

    MSConfig.updateIndicatorConfig(indicator.playerIndex, indicator:getType(), key, indicator[key])
end

function MS_MatrixManager.applySharedMatrixValue(playerIndex, key, value)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    matrixConfig[key] = value
    MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", matrixConfig)

    for _, indicator in ipairs(MS_MatrixManager.getEditableLinkedIndicators(playerIndex, true)) do
        applySharedMatrixValueToIndicator(indicator, key, value)
    end

    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

function MS_MatrixManager.areAllLinkedIndicatorsHidden(playerIndex)
    local linked = MS_MatrixManager.getLinkedIndicators(playerIndex, true)
    if #linked == 0 then
        return false
    end
    for _, indicator in ipairs(linked) do
        if indicator.hidden ~= true then
            return false
        end
    end
    return true
end

function MS_MatrixManager.areAllLinkedIndicatorsLocked(playerIndex)
    local linked = MS_MatrixManager.getLinkedIndicators(playerIndex, true)
    if #linked == 0 then
        return false
    end
    for _, indicator in ipairs(linked) do
        if indicator.locked ~= true then
            return false
        end
    end
    return true
end

function MS_MatrixManager.setLinkedIndicatorsHidden(playerIndex, hidden)
    local targetHidden = hidden == true
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, true)) do
        if indicator.locked ~= true or targetHidden == false then
            indicator.hidden = targetHidden
            indicator:setVisible(not targetHidden)
            MSConfig.updateIndicatorConfig(indicator.playerIndex, indicator:getType(), "hidden", targetHidden)
        end
    end
    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

function MS_MatrixManager.setLinkedIndicatorsLocked(playerIndex, locked)
    local targetLocked = locked == true
    for _, indicator in ipairs(MS_MatrixManager.getLinkedIndicators(playerIndex, true)) do
        indicator.locked = targetLocked
        indicator.moveWithMouse = not targetLocked
        MSConfig.updateIndicatorConfig(indicator.playerIndex, indicator:getType(), "locked", targetLocked)
    end
    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

function MS_MatrixManager.resetMatrixConfig(playerIndex)
    local current = MS_MatrixManager.getMatrixConfig(playerIndex)
    local defaultConfig = MSConfig.getDefaultMatrixConfig()
    defaultConfig.nextLinkOrder = current.nextLinkOrder or 1
    MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", defaultConfig)
    MS_MatrixManager.refreshPlayer(playerIndex)
    return defaultConfig
end

function MS_MatrixManager.updateMatrixConfig(playerIndex, key, value)
    if MS_MatrixManager.isSharedMatrixKey(key) then
        MS_MatrixManager.applySharedMatrixValue(playerIndex, key, value)
        return
    end

    MSConfig.updateMatrixConfig(playerIndex, key, value)
    MS_MatrixManager.refreshPlayer(playerIndex)
end


function MS_MatrixManager.setLinkedIndicatorsAlwaysShowValue(playerIndex, enabled)
    MS_MatrixManager.applySharedMatrixValue(playerIndex, "alwaysShowValue", enabled == true)
end

function MS_MatrixManager.setLinkedIndicatorsUseMonoIcon(playerIndex, enabled)
    MS_MatrixManager.applySharedMatrixValue(playerIndex, "useMonoIcon", enabled == true)
end

function MS_MatrixManager.setLinkedIndicatorsAutoHide(playerIndex, enabled)
    MS_MatrixManager.applySharedMatrixValue(playerIndex, "autoHide", enabled == true)
end

function MS_MatrixManager.isDragAnchor(indicator)
    return indicator and indicator.matrixLinked == true and indicator.locked ~= true
end

function MS_MatrixManager.moveMatrixToPosition(playerIndex, x, y)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    matrixConfig.x = math.floor(x or matrixConfig.x or 0)
    matrixConfig.y = math.floor(y or matrixConfig.y or 0)
    MS_MatrixManager.applyMatrixLayout(playerIndex)
end

function MS_MatrixManager.moveMatrixByDelta(playerIndex, dx, dy)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    MS_MatrixManager.moveMatrixToPosition(playerIndex, (matrixConfig.x or 0) + dx, (matrixConfig.y or 0) + dy)
end

function MS_MatrixManager.persistMatrixPosition(playerIndex)
    local matrixConfig = MS_MatrixManager.getMatrixConfig(playerIndex)
    MSConfig.updateGlobalConfig(playerIndex, "matrixSettings", matrixConfig)
end

return MS_MatrixManager
