require "ISUI/ISPanel"
require "ISUI/ISButton"
require "ISUI/ISTickBox"
require "ISUI/ISTextEntryBox"
require "ISUI/ISLabel"
require "MS/SettingPanel/MS_SquareButton"
require "MS/SettingPanel/MS_PanelCollapseHelper"
require "MS/SettingPanel/MS_TitlePanelButtons"
require "MS/SettingPanel/MS_LongButton"
require "MS/MS_MatrixManager"
require "MS/MS_PerformanceManager"

MS_MatrixStylePanel = ISPanel:derive("MS_MatrixStylePanel")
local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small)


local function applyOptionTooltip(control, tooltip)
    if control and tooltip and control.setTooltip then
        control:setTooltip(tooltip)
    end
end

local function refreshButtonTooltip(button, tooltip)
    if not button then return end
    if tooltip then
        button:setTooltip(tooltip)
    end
    if button.updateTooltip then
        button:updateTooltip()
    end
end

MS_MatrixStylePanel.CurrentPanel = nil

function MS_MatrixStylePanel.calculateRequiredSize()
    local rowHeight = math.floor(FONT_HGT_SMALL * 1.5)
    local titleBarHeight = math.floor(FONT_HGT_SMALL * 1.2)
    local buttonHeight = math.floor(FONT_HGT_SMALL * 1.3)
    local padding = math.floor(FONT_HGT_SMALL * 0.2)
    local rowCount = 21

    local width = 420
    local height = titleBarHeight + padding + rowHeight * rowCount + padding * 7 + buttonHeight * 4
    return { width = width, height = height }
end

function MS_MatrixStylePanel:initialise()
    ISPanel.initialise(self)
end

function MS_MatrixStylePanel:new(x, y, width, height, playerIndex)
    local screenHeight = getCore():getScreenHeight()
    local maxHeight = math.max(220, screenHeight - 40)
    local size = MS_MatrixStylePanel.calculateRequiredSize()
    width = width or size.width
    height = math.min(height or size.height, maxHeight)

    local o = ISPanel:new(x, y, width, height)
    setmetatable(o, self)
    self.__index = self

    o.playerIndex = playerIndex
    o.padding = math.floor(FONT_HGT_SMALL * 0.2)
    o.titleBarHeight = math.floor(FONT_HGT_SMALL * 1.2)
    o.rowHeight = math.floor(FONT_HGT_SMALL * 1.5)
    o.buttonHeight = math.floor(FONT_HGT_SMALL * 1.2)
    o.contentButtonHeight = math.floor(FONT_HGT_SMALL * 1.2)
    o.buttonSize = math.floor(FONT_HGT_SMALL * 1.2)
    o.moving = false
    o.moveWithMouse = false
    o:noBackground(true)

    o.minusIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Minus.png")
    o.plusIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Plus.png")
    o.closeIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Close.png")
    o.resetIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Reset.png")

    o.circularIcon = MS_MatrixManager.getModeIconTexture("circular")
    o.horizontalBarIcon = MS_MatrixManager.getModeIconTexture("horizontal")
    o.verticalBarIcon = MS_MatrixManager.getModeIconTexture("vertical")

    o.titlebarbkg = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/TitleBarBG.png")
    o.contentbkg = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/ContentBackground.png")

    return o
end

function MS_MatrixStylePanel.openForPlayer(playerIndex, anchorX, anchorY)
    if MS_MatrixStylePanel.CurrentPanel then
        MS_MatrixStylePanel.CurrentPanel:close()
    end

    local screenWidth = getCore():getScreenWidth()
    local screenHeight = getCore():getScreenHeight()
    local size = MS_MatrixStylePanel.calculateRequiredSize()

    local x = anchorX or math.floor((screenWidth - size.width) / 2)
    local y = anchorY or math.floor((screenHeight - math.min(size.height, screenHeight - 40)) / 2)

    local panel = MS_MatrixStylePanel:new(x, y, nil, nil, playerIndex)
    panel:initialise()
    panel:addToUIManager()
    panel:setVisible(true)

    panel:setX(math.max(10, math.min(screenWidth - panel:getWidth() - 10, panel:getX())))
    panel:setY(math.max(10, math.min(screenHeight - panel:getHeight() - 10, panel:getY())))

    MS_MatrixStylePanel.CurrentPanel = panel
    return panel
end

function MS_MatrixStylePanel:close()
    if MS_MatrixStylePanel.CurrentPanel == self then
        MS_MatrixStylePanel.CurrentPanel = nil
    end
    self:setVisible(false)
    self:removeFromUIManager()
end

function MS_MatrixStylePanel:createChildren()
    self:createTitleBar()
    self:createScrollView()
    self:createContent()
    self:createBottomButtons()
end

function MS_MatrixStylePanel:createTitleBar()
    local titleButtonSize = FONT_HGT_SMALL
    MS_PanelCollapseHelper.addTitleBarButton(self, titleButtonSize, "MatrixStylePanel")

    self.titleLabel = ISLabel:new(MS_PanelCollapseHelper.getTitleLabelX(self, titleButtonSize), (self.titleBarHeight - FONT_HGT_SMALL) / 2, self.titleBarHeight, getText("IGUI_ModernStatus_MatrixIndicatorsTitle"), 1, 1, 1, 1, UIFont.Small, true)
    self.titleLabel:initialise()
    self:addChild(self.titleLabel)

    local closeSize = FONT_HGT_SMALL
    self.closeButton = MS_SquareButton:new(self.width - closeSize - self.padding, (self.titleBarHeight - FONT_HGT_SMALL) / 2, closeSize, self.closeIcon, self, MS_MatrixStylePanel.close)
    self.closeButton:initialise()
    self.closeButton:setActive(true)
    self.closeButton:setActiveColor(0.8, 0.2, 0.2)
    self:addChild(self.closeButton)

    MS_TitlePanelButtons.addButtons(self, { "all", "unlinked" }, closeSize)
end

function MS_MatrixStylePanel:createScrollView()
    local contentY = self.titleBarHeight + self.padding
    local contentHeight = self.height - contentY - (self.buttonHeight * 3) - self.padding * 5

    self.scrollView = NIScrollView:new(0, contentY, self.width, contentHeight)
    self.scrollView:initialise()
    self.scrollView:setScrollDirection("vertical")
    self.scrollView:setScrollSensitivity(self.rowHeight)
    self:addChild(self.scrollView)
end

function MS_MatrixStylePanel:getDefaultMatrixConfig()
    return MSConfig.getDefaultMatrixConfig()
end

function MS_MatrixStylePanel:getMatrixConfig()
    return MS_MatrixManager.getMatrixConfig(self.playerIndex)
end

function MS_MatrixStylePanel:getLinkedSummary(getter, defaultValue, indicatorList)
    return MS_MatrixManager.summarizeLinkedIndicators(self.playerIndex, getter, defaultValue, indicatorList)
end

function MS_MatrixStylePanel:getSharedOptionSummary(optionKey, getter, defaultValue, indicatorList)
    -- Use the saved matrix setting when there are no editable linked indicators.
    -- Otherwise a click is saved but the checkbox redraws from the hard-coded default.
    local indicators = indicatorList or MS_MatrixManager.getEditableLinkedIndicators(self.playerIndex, true)
    if not indicators or #indicators == 0 then
        local config = self:getMatrixConfig() or {}
        if config[optionKey] ~= nil then
            return config[optionKey], false, 0
        end
        return defaultValue, false, 0
    end

    return MS_MatrixManager.summarizeLinkedIndicators(self.playerIndex, getter, defaultValue, indicators)
end

function MS_MatrixStylePanel:getCurrentMode()
    local config = self:getMatrixConfig()
    return (config and config.mode) or "circular"
end

function MS_MatrixStylePanel:isBarMode()
    local mode = self:getCurrentMode()
    return mode == "horizontal" or mode == "vertical"
end

function MS_MatrixStylePanel:createColorResetControl(x, y)
    self:createLabel(getText("IGUI_ModernStatus_Color"), y, self:getOptionTooltip("color"))
    self.resetColorBtn = MS_SquareButton:new(x, y + ((self.rowHeight - self.buttonSize) / 2), self.buttonSize, self.resetIcon, self, MS_MatrixStylePanel.onResetLinkedColor)
    self.resetColorBtn:initialise()
    applyOptionTooltip(self.resetColorBtn, self:getOptionTooltip("color"))
    self.scrollView:addScrollChild(self.resetColorBtn)
end

function MS_MatrixStylePanel:onResetLinkedColor()
    for _, indicator in ipairs(MS_MatrixManager.getEditableLinkedIndicators(self.playerIndex, true)) do
        local defaultConfig = MSConfig.getDefaultIndicatorConfig(indicator:getType())
        local defaultColor = defaultConfig and defaultConfig.color or { r = 1, g = 1, b = 1 }
        indicator.indicatorColor = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b }
        MSConfig.updateIndicatorConfig(indicator.playerIndex, indicator:getType(), "color", { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b })
    end
end

function MS_MatrixStylePanel:getOptionTooltip(optionKey)
    if not MS_IndicatorVisualConfig or not MS_IndicatorVisualConfig.getOptionTooltip then
        return nil
    end
    return MS_IndicatorVisualConfig.getOptionTooltip(nil, optionKey, true)
end

function MS_MatrixStylePanel:createContent()
    local y = self.padding
    local middleX = math.floor(self.width / 2)
    local controlX = middleX + self.padding
    local mode = self:getCurrentMode()
    local isBarMode = self:isBarMode()
    local config = self:getMatrixConfig()

    self.styleButton = self:createModeButton(controlX, y)
    y = y + self.rowHeight

    self.columnsControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Columns"),
        function() return self:getMatrixConfig().columns end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "columns", value) end,
        1, 40, 1, false)
    y = y + self.rowHeight

    self.sortButton = self:createToggleButton(controlX, y, getText("IGUI_ModernStatus_SortDirection"),
        function()
            local direction = self:getMatrixConfig().sortDirection
            return direction == "columns" and getText("IGUI_ModernStatus_Columns") or getText("IGUI_ModernStatus_Rows")
        end,
        function()
            local direction = self:getMatrixConfig().sortDirection
            local nextDirection = direction == "columns" and "rows" or "columns"
            MS_MatrixManager.updateMatrixConfig(self.playerIndex, "sortDirection", nextDirection)
        end,
        function() return self:getMatrixConfig().sortDirection == "columns" end)
    y = y + self.rowHeight

    self.compactHiddenBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_FillHiddenMatrixGaps"),
        function() return self:getMatrixConfig().compactHiddenIndicators == true, false, 1 end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "compactHiddenIndicators", selected) end, "compactHiddenIndicators")
    y = y + self.rowHeight

    self.xControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_X"),
        function() return self:getMatrixConfig().x end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "x", value) end,
        0, getCore():getScreenWidth(), 1, false)
    y = y + self.rowHeight

    self.yControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Y"),
        function() return self:getMatrixConfig().y end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "y", value) end,
        0, getCore():getScreenHeight(), 1, false)
    y = y + self.rowHeight

    self.deltaXControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_RowWidth"),
        function() return self:getMatrixConfig().deltaX end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "deltaX", value) end,
        1, getCore():getScreenWidth(), 1, false)
    y = y + self.rowHeight

    self.deltaYControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_RowHeight"),
        function() return self:getMatrixConfig().deltaY end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "deltaY", value) end,
        1, getCore():getScreenHeight(), 1, false)
    y = y + self.rowHeight

    if isBarMode then
        self.widthControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Wide"),
            function() return self:getMatrixConfig().width end,
            function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "width", value) end,
            8, 500, 2, false)
        y = y + self.rowHeight

        self.heightControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Height"),
            function() return self:getMatrixConfig().height end,
            function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "height", value) end,
            8, 500, 2, false)
        y = y + self.rowHeight
    else
        self.sizeControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Size"),
            function() return math.max(self:getMatrixConfig().width or 48, self:getMatrixConfig().height or 48) end,
            function(value)
                MS_MatrixManager.updateMatrixConfig(self.playerIndex, "width", value)
                MS_MatrixManager.updateMatrixConfig(self.playerIndex, "height", value)
            end,
            8, 500, 2, false)
        y = y + self.rowHeight
    end

    self:createColorResetControl(controlX, y)
    y = y + self.rowHeight

    self.opacityControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Opacity"),
        function() return self:getSharedOptionSummary("opacity", function(indicator) return indicator.opacity or 1.0 end, self:getDefaultMatrixConfig().opacity) end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "opacity", value) end,
        0.1, 1.0, 0.1, true)
    y = y + self.rowHeight

    self.useGradientBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_UseGradient"),
        function() return self:getSharedOptionSummary("useGradient", function(indicator) return indicator.useGradient == true end, self:getDefaultMatrixConfig().useGradient) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "useGradient", selected) end, "useGradient")
    y = y + self.rowHeight

    self.showIconBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_ShowIcon"),
        function() return self:getSharedOptionSummary("showIcon", function(indicator) return indicator.showIcon == true end, self:getDefaultMatrixConfig().showIcon) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "showIcon", selected) end, "showIcon")
    y = y + self.rowHeight

    self.iconPositionBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_IconPosition"),
        function()
            return self:getLinkedSummary(function(indicator)
                return indicator.iconPosition == "right" or indicator.iconPosition == "top"
            end, self:getDefaultMatrixConfig().iconPosition == "right" or self:getDefaultMatrixConfig().iconPosition == "top")
        end,
        function(selected)
            local currentMode = self:getCurrentMode()
            local pos = selected and ((currentMode == "vertical") and "top" or "right") or ((currentMode == "vertical") and "bottom" or "left")
            MS_MatrixManager.updateMatrixConfig(self.playerIndex, "iconPosition", pos)
        end,
        "iconPosition")
    y = y + self.rowHeight

    self.useMonoIconBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_MonoIcon"),
        function() return self:getSharedOptionSummary("useMonoIcon", function(indicator) return indicator.useMonoIcon == true end, self:getDefaultMatrixConfig().useMonoIcon) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "useMonoIcon", selected) end, "useMonoIcon")
    y = y + self.rowHeight

    self.iconOpacityControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_IconOpacity"),
        function() return self:getSharedOptionSummary("iconOpacity", function(indicator) return indicator.iconOpacity or 1.0 end, self:getDefaultMatrixConfig().iconOpacity) end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "iconOpacity", value) end,
        0.1, 1.0, 0.1, true)
    y = y + self.rowHeight

    self.textScaleControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_TextSize"),
        function() return self:getSharedOptionSummary("textScale", function(indicator) return indicator.textScale or 1.0 end, self:getDefaultMatrixConfig().textScale) end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "textScale", value) end,
        0.5, 2.0, 0.1, true)
    y = y + self.rowHeight

    self.updateIntervalControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_UpdateIntervalMs"),
        function() return self:getSharedOptionSummary("updateIntervalMs", function(indicator) return indicator.updateIntervalMs or MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(indicator:getType()) end, self:getDefaultMatrixConfig().updateIntervalMs) end,
        function(value)
            MS_MatrixManager.updateMatrixConfig(self.playerIndex, "updateIntervalMs", math.floor(value))
            if MS_PerformanceManager and MS_PerformanceManager.markCustomMode then
                MS_PerformanceManager.markCustomMode(self.playerIndex)
            end
        end,
        50, 10000, 50, false, 0, "updateIntervalMs")
    y = y + self.rowHeight

    if isBarMode then
        self.showNameBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_ShowName"),
            function() return self:getSharedOptionSummary("showName", function(indicator) return indicator.showName ~= false end, self:getDefaultMatrixConfig().showName) end,
            function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "showName", selected) end, "showName")
        y = y + self.rowHeight
    end

    self.alwaysShowValueBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_AlwaysShowValue"),
        function() return self:getSharedOptionSummary("alwaysShowValue", function(indicator) return indicator.alwaysShowValue == true end, self:getDefaultMatrixConfig().alwaysShowValue) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "alwaysShowValue", selected) end, "alwaysShowValue")
    y = y + self.rowHeight

    if MS_MatrixManager.hasThresholdLinkedIndicators(self.playerIndex) then
        self.displayThresholdsBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_DisplayThresholds"),
            function() return self:getSharedOptionSummary("displayThresholds", function(indicator) return indicator.displayThresholds == true end, self:getDefaultMatrixConfig().displayThresholds, MS_MatrixManager.getEditableThresholdLinkedIndicators(self.playerIndex, true)) end,
            function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "displayThresholds", selected) end, "displayThresholds")
        y = y + self.rowHeight
    end

    if MS_MatrixManager.hasInvertibleLinkedIndicators(self.playerIndex) then
        self.invertDirectionBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_InvertDirection"),
            function() return self:getSharedOptionSummary("invertDirection", function(indicator) return indicator.invertDirection == true end, self:getDefaultMatrixConfig().invertDirection, MS_MatrixManager.getEditableInvertibleLinkedIndicators(self.playerIndex, true)) end,
            function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "invertDirection", selected) end, "invertDirection")
        y = y + self.rowHeight
    end

    self.alarmEnabledBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_EnableAlarm"),
        function() return self:getSharedOptionSummary("alarmEnabled", function(indicator) return indicator.alarmEnabled ~= false end, self:getDefaultMatrixConfig().alarmEnabled) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "alarmEnabled", selected) end, "alarmEnabled")
    y = y + self.rowHeight

    self.thresholdControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_AnimThreshold"),
        function() return self:getSharedOptionSummary("animationThreshold", function(indicator) return indicator.animationThreshold or 20 end, self:getDefaultMatrixConfig().animationThreshold) end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "animationThreshold", value) end,
        0, 100, 1, false, 1, "animationThreshold")
    y = y + self.rowHeight

    self.autoHideBox = self:createMixedAwareTickBox(controlX, y, getText("IGUI_ModernStatus_AutoHide"),
        function() return self:getSharedOptionSummary("autoHide", function(indicator) return indicator.autoHide == true end, self:getDefaultMatrixConfig().autoHide) end,
        function(selected) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "autoHide", selected) end, "autoHide")
    y = y + self.rowHeight

    self.autoHideThresholdControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_AutoHideThreshold"),
        function() return self:getSharedOptionSummary("autoHideThreshold", function(indicator) return indicator.autoHideThreshold or 0 end, self:getDefaultMatrixConfig().autoHideThreshold) end,
        function(value) MS_MatrixManager.updateMatrixConfig(self.playerIndex, "autoHideThreshold", value) end,
        0, 100, 1, false, 1, "autoHideThreshold")
    y = y + self.rowHeight

    self.scrollView:setScrollHeight(y + self.padding)
end

function MS_MatrixStylePanel:createLabel(text, y, tooltip)
    local label = ISLabel:new(self.padding, y + ((self.rowHeight - FONT_HGT_SMALL) / 2), 0, text, 1, 1, 1, 1, UIFont.Small, true)
    label:initialise()
    applyOptionTooltip(label, tooltip)
    self.scrollView:addScrollChild(label)
    return label
end

function MS_MatrixStylePanel:createModeButton(x, y)
    self:createLabel(getText("IGUI_ModernStatus_Style"), y, self:getOptionTooltip("mode"))

    local button = MS_SquareButton:new(x, y + ((self.rowHeight - self.buttonSize) / 2), self.buttonSize, self.circularIcon, self, MS_MatrixStylePanel.onCycleMode)
    button:initialise()
    button:setIconSizeRatio(0.7)
    button.prerender = function(control)
        local mode = self:getMatrixConfig().mode
        control:setIcon(MS_MatrixManager.getModeIconTexture(mode))
        control:setActive(mode ~= "circular")
    end
    self.scrollView:addScrollChild(button)
    return button
end

function MS_MatrixStylePanel:createToggleButton(x, y, labelText, getTitle, onClick, isActive)
    self:createLabel(labelText, y, self:getOptionTooltip(string.lower(labelText) == "lock" and "lock" or string.lower(labelText) == "hide" and "hide" or nil))

    local button = MS_LongButton:new(x, y + ((self.rowHeight - self.contentButtonHeight) / 2), 90, self.contentButtonHeight, getTitle(), self, function() onClick() end)
    button:initialise()
    button.prerender = function(control)
        local title = getTitle()
        control:setTitle(title)
        -- Keep the active state tied to the underlying sort mode, not to localized button text.
        control:setActive(isActive and isActive() == true or false)
    end
    self.scrollView:addScrollChild(button)
    return button
end

function MS_MatrixStylePanel:createMixedAwareTickBox(x, y, labelText, getSummary, onApply, optionKey)
    self:createLabel(labelText, y, self:getOptionTooltip(optionKey))

    local box = ISTickBox:new(x, y + ((self.rowHeight - self.buttonSize) / 2), self.buttonSize, self.buttonSize, "", self, function(_, _, selected)
        -- Normalize the tickbox callback value across B42 point releases.
        onApply(selected == true)
    end)
    box:initialise()
    box:addOption("")
    box._mixed = false
    box.prerender = function(control)
        control._mixed = false
        local value, mixed = getSummary()
        control._mixed = mixed
        control:setSelected(1, value == true and not mixed)
        if mixed then
            control:drawRect(0, 0, control.width, control.height, 0.28, 0.45, 0.45, 0.45)
        end
        ISTickBox.prerender(control)
    end
    applyOptionTooltip(box, self:getOptionTooltip(optionKey))
    self.scrollView:addScrollChild(box)
    return box
end

function MS_MatrixStylePanel:createStepperControl(x, y, labelText, getSummary, onApply, minValue, maxValue, step, isPercentage, precision, optionKey)
    local tooltip = self:getOptionTooltip(optionKey)
    self:createLabel(labelText, y, tooltip)

    local textDigits = math.max(4, string.len(tostring(math.floor(maxValue or 9999))))
    local textBoxWidth = getTextManager():MeasureStringX(UIFont.Small, string.rep("9", textDigits)) + self.padding * 6

    local function formatDisplayValue(rawValue)
        local displayValue = rawValue
        if isPercentage then
            displayValue = displayValue * 100
        end

        if precision and precision > 0 then
            return string.format("%." .. tostring(precision) .. "f", displayValue)
        end

        return tostring(math.floor(displayValue + 0.00001))
    end

    local value = getSummary()
    local textBox = ISTextEntryBox:new(formatDisplayValue(value), x + self.buttonSize + self.padding, y + ((self.rowHeight - self.buttonSize) / 2), textBoxWidth, self.buttonSize)
    textBox:initialise()
    textBox:instantiate()
    textBox:setOnlyNumbers(not (precision and precision > 0))
    textBox.borderColor = { r = 0.4, g = 0.4, b = 0.4, a = 0.0 }
    textBox.backgroundColor = { r = 0.1, g = 0.1, b = 0.1, a = 0.0 }
    textBox._userEdited = false
    textBox._suppressTextChange = false

    local function setDisplayText(value)
        textBox._suppressTextChange = true
        textBox:setText(tostring(value))
        textBox._suppressTextChange = false
    end

    local function updateDisplay(forceDefault)
        local newValue, mixed = getSummary()
        setDisplayText(formatDisplayValue(newValue))
        textBox._userEdited = false

        if forceDefault or mixed then
            textBox.backgroundColor = { r = 0.35, g = 0.35, b = 0.35, a = 0.22 }
        else
            textBox.backgroundColor = { r = 0.1, g = 0.1, b = 0.1, a = 0.0 }
        end
    end

    local function readValueFromControl()
        local textValue = textBox:getText()
        local raw = tonumber(textValue)
        if not raw then
            local fallback = getSummary()
            raw = isPercentage and (fallback * 100) or fallback
        end
        if isPercentage then
            raw = math.max(minValue * 100, math.min(maxValue * 100, raw))
            textBox:setText(formatDisplayValue(raw / 100))
            return raw / 100
        end
        raw = math.max(minValue, math.min(maxValue, raw))
        textBox:setText(formatDisplayValue(raw))
        return raw
    end

    textBox.onTextChange = function(control)
        if not control._suppressTextChange then
            control._userEdited = true
        end
    end
    textBox.onCommandEntered = function()
        onApply(readValueFromControl())
        updateDisplay(false)
    end
    textBox.onOtherKey = function(key)
        if key == 28 then
            onApply(readValueFromControl())
            updateDisplay(false)
        end
    end
    textBox.onMouseDownOutside = function(control)
        if control._userEdited then
            onApply(readValueFromControl())
        end
        updateDisplay(false)
    end
    textBox.prerender = function(control)
        ISTextEntryBox.prerender(control)
        if not control:isFocused() then
            updateDisplay(false)
        end
    end
    applyOptionTooltip(textBox, tooltip)
    self.scrollView:addScrollChild(textBox)

    local decBtn = MS_SquareButton:new(x, y + ((self.rowHeight - self.buttonSize) / 2), self.buttonSize, self.minusIcon, self, function()
        local current = readValueFromControl()
        onApply(math.max(minValue, current - step))
        updateDisplay(true)
    end)
    decBtn:initialise()
    applyOptionTooltip(decBtn, tooltip)
    self.scrollView:addScrollChild(decBtn)

    local incBtn = MS_SquareButton:new(x + self.buttonSize + self.padding + textBoxWidth + self.padding, y + ((self.rowHeight - self.buttonSize) / 2), self.buttonSize, self.plusIcon, self, function()
        local current = readValueFromControl()
        onApply(math.min(maxValue, current + step))
        updateDisplay(true)
    end)
    incBtn:initialise()
    applyOptionTooltip(incBtn, tooltip)
    self.scrollView:addScrollChild(incBtn)

    return { decBtn = decBtn, textBox = textBox, incBtn = incBtn }
end

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

    local first = indicators[1].locked == true
    for i = 2, #indicators do
        if (indicators[i].locked == true) ~= first then
            return false, true, #indicators
        end
    end

    return first, false, #indicators
end

function MS_MatrixStylePanel:getLinkedLockState()
    return summarizeLockedState(MS_MatrixManager.getLinkedIndicators(self.playerIndex, true))
end

function MS_MatrixStylePanel:onToggleKeepLayoutOnRelease()
    local config = self:getMatrixConfig()
    MS_MatrixManager.updateMatrixConfig(self.playerIndex, "keepLayoutOnRelease", not (config.keepLayoutOnRelease == true))
end

function MS_MatrixStylePanel:createBottomButtons()
    local buttonWidth = math.floor((self:getWidth() - self.padding * 3) / 2)
    local fullWidth = self:getWidth() - self.padding * 2
    local bottomY = self:getHeight() - self.buttonHeight - self.padding
    local middleY = bottomY - self.buttonHeight - self.padding
    local topY = middleY - self.buttonHeight - self.padding

    self.keepLayoutButton = MS_LongButton:new(self.padding, topY, fullWidth, self.buttonHeight, getText("IGUI_ModernStatus_KeepLayoutOnRelease"), self, MS_MatrixStylePanel.onToggleKeepLayoutOnRelease)
    self.keepLayoutButton:initialise()
    self.keepLayoutButton:setTooltip(getText("IGUI_ModernStatus_Tooltip_KeepLayoutOnRelease"))
    self.keepLayoutButton.prerender = function(button)
        local enabled = self:getMatrixConfig().keepLayoutOnRelease == true
        button:setTitle(getText("IGUI_ModernStatus_KeepLayoutOnRelease"))
        button:setActive(enabled)
        button:setActiveColor(0.95, 0.5, 0.1)
        refreshButtonTooltip(button, getText("IGUI_ModernStatus_Tooltip_KeepLayoutOnRelease"))
    end
    self:addChild(self.keepLayoutButton)

    self.releaseAllButton = MS_LongButton:new(self.padding, middleY, fullWidth, self.buttonHeight, getText("IGUI_ModernStatus_ReleaseAll"), self, MS_MatrixStylePanel.onReleaseAllClick)
    self.releaseAllButton:initialise()
    self.releaseAllButton.prerender = function(button)
        button:setActive(MS_MatrixManager.hasLinkedIndicators(self.playerIndex))
        button:setActiveColor(0.95, 0.5, 0.1)
    end
    self:addChild(self.releaseAllButton)

    self.hideButton = MS_LongButton:new(self.padding, bottomY, buttonWidth, self.buttonHeight, getText("IGUI_ModernStatus_Hide"), self, MS_MatrixStylePanel.onHideLinkedClick)
    self.hideButton:initialise()
    self.hideButton.prerender = function(button)
        local allHidden = MS_MatrixManager.areAllLinkedIndicatorsHidden(self.playerIndex)
        button:setTitle(allHidden and getText("IGUI_ModernStatus_Show") or getText("IGUI_ModernStatus_Hide"))
        button:setActive(allHidden)
        button:setActiveColor(0.95, 0.5, 0.1)
    end
    self:addChild(self.hideButton)

    self.layoutButton = MS_LongButton:new(self:getWidth() - buttonWidth - self.padding, bottomY, buttonWidth, self.buttonHeight, getText("IGUI_ModernStatus_Lock"), self, MS_MatrixStylePanel.onToggleLayoutClick)
    self.layoutButton:initialise()
    self.layoutButton.prerender = function(button)
        local allLocked, mixed, count = self:getLinkedLockState()
        if count == 0 then
            button:setTitle(getText("IGUI_ModernStatus_Lock"))
            button:setActive(false)
            button:setActiveColor(0.95, 0.5, 0.1)
            return
        end

        if allLocked then
            button:setTitle(getText("IGUI_ModernStatus_Unlock"))
            button:setActive(true)
            button:setActiveColor(0.95, 0.5, 0.1)
        elseif mixed then
            button:setTitle(getText("IGUI_ModernStatus_Lock"))
            button:setActive(true)
            button:setActiveColor(0.45, 0.45, 0.45)
        else
            button:setTitle(getText("IGUI_ModernStatus_Lock"))
            button:setActive(false)
            button:setActiveColor(0.95, 0.5, 0.1)
        end
    end
    self:addChild(self.layoutButton)
end

function MS_MatrixStylePanel:reopenPreservingPosition()
    local x = self:getX()
    local y = self:getY()
    local playerIndex = self.playerIndex
    self:close()
    MS_MatrixStylePanel.openForPlayer(playerIndex, x, y)
end

function MS_MatrixStylePanel:onCycleMode()
    -- Reopen the panel so the visible controls match the new style mode.
    MS_MatrixManager.cycleMatrixMode(self.playerIndex)
    self:reopenPreservingPosition()
end

function MS_MatrixStylePanel:onReleaseAllClick()
    MS_MatrixManager.releaseAllLinkedIndicators(self.playerIndex)
end

function MS_MatrixStylePanel:onHideLinkedClick()
    local shouldHide = not MS_MatrixManager.areAllLinkedIndicatorsHidden(self.playerIndex)
    MS_MatrixManager.setLinkedIndicatorsHidden(self.playerIndex, shouldHide)
end

function MS_MatrixStylePanel:onToggleLayoutClick()
    local allLocked = self:getLinkedLockState()
    local shouldLock = not allLocked
    MS_MatrixManager.setLinkedIndicatorsLocked(self.playerIndex, shouldLock)
end

function MS_MatrixStylePanel:onMouseWheel(del)
    if self.scrollView and self.scrollView:isVisible() then
        return self.scrollView:onMouseWheel(del)
    end
    return false
end

function MS_MatrixStylePanel:onMouseDown(x, y)
    if y <= self.titleBarHeight then
        self.moving = true
        self:setCapture(true)
        return true
    end
    return ISPanel.onMouseDown(self, x, y)
end

function MS_MatrixStylePanel:onMouseUp(x, y)
    if self.moving then
        self.moving = false
        self:setCapture(false)
    end
    return ISPanel.onMouseUp(self, x, y)
end

function MS_MatrixStylePanel:onMouseMove(dx, dy)
    if self.moving then
        self:setX(self.x + dx)
        self:setY(self.y + dy)
        return true
    end
    return ISPanel.onMouseMove(self, dx, dy)
end

function MS_MatrixStylePanel:onMouseMoveOutside(dx, dy)
    if self.moving then
        self:setX(self.x + dx)
        self:setY(self.y + dy)
        return true
    end
    return ISPanel.onMouseMoveOutside(self, dx, dy)
end

function MS_MatrixStylePanel:onMouseUpOutside(x, y)
    if self.moving then
        self.moving = false
        self:setCapture(false)
    end
    if MS_PanelCollapseHelper.onMouseUpOutside(self) then
        return true
    end
    return ISPanel.onMouseUpOutside(self, x, y)
end

function MS_MatrixStylePanel:prerender()
    MS_PanelCollapseHelper.updateCollapsedPreviewFromMouse(self)

    local titlebarbkg = self.titlebarbkg
    if not titlebarbkg and NinePatchTexture and NinePatchTexture.getSharedTexture then
        titlebarbkg = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/TitleBarBG.png")
        self.titlebarbkg = titlebarbkg
    end

    if titlebarbkg and titlebarbkg.render then
        titlebarbkg:render(self:getAbsoluteX(), self:getAbsoluteY(), self:getWidth(), self.titleBarHeight, 0.5, 0.5, 0.5, 0.95)
    else
        self:drawRect(0, 0, self:getWidth(), self.titleBarHeight, 0.95, 0.5, 0.5, 0.5)
    end

    if MS_PanelCollapseHelper.isContentHidden(self) then
        return
    end

    local contentbkg = self.contentbkg
    if not contentbkg and NinePatchTexture and NinePatchTexture.getSharedTexture then
        contentbkg = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/ContentBackground.png")
        self.contentbkg = contentbkg
    end

    if contentbkg and contentbkg.render then
        contentbkg:render(self:getAbsoluteX(), self:getAbsoluteY() + self.titleBarHeight, self:getWidth(), self:getHeight() - self.titleBarHeight, 0.1, 0.1, 0.1, 0.8)
    else
        self:drawRect(0, self.titleBarHeight, self:getWidth(), self:getHeight() - self.titleBarHeight, 0.8, 0.1, 0.1, 0.1)
    end
end

return MS_MatrixStylePanel
