require "ISUI/ISPanel"
require "ISUI/ISButton"
require "ISUI/ISTickBox"
require "ISUI/ISLabel"
require "MS/SettingPanel/MS_LongButton"
require "MS/MS_MatrixManager"
require "MS/MS_IndicatorVisualConfig"
require "MS/MS_PerformanceManager"
require "ISUI/ISTextEntryBox"

MS_CircularStylePanel = ISPanel:derive("MS_CircularStylePanel")

local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small)
function MS_CircularStylePanel:isCarryWeightIndicator()
    local indicatorType = self.indicator and self.indicator:getType()
    return indicatorType == "MS_CarryWeightIndicator" or indicatorType == "ISCarryWeightStatusIndicator"
end


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


function MS_CircularStylePanel:getOptionTooltip(optionKey)
    local indicatorType = self.indicator and self.indicator.getType and self.indicator:getType() or nil
    if not MS_IndicatorVisualConfig or not MS_IndicatorVisualConfig.getOptionTooltip then
        return nil
    end
    return MS_IndicatorVisualConfig.getOptionTooltip(indicatorType, optionKey, false)
end

function MS_CircularStylePanel:supportsDisplayThresholds()
    local indicatorType = self.indicator and self.indicator:getType()
    if self:isCarryWeightIndicator() then
        return true
    end

    return MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.supportsThresholds(indicatorType)
end

function MS_CircularStylePanel:getThresholdInputSpec()
    if MS_IndicatorVisualConfig and self.indicator and self.indicator.getType then
        return MS_IndicatorVisualConfig.getThresholdInputSpec(self.indicator:getType())
    end

    return { min = 0, max = 100, step = 5, precision = 0 }
end

function MS_CircularStylePanel:supportsDirectionInversion()
    local indicatorType = self.indicator and self.indicator.getType and self.indicator:getType() or nil
    return MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.supportsDirectionInversion and MS_IndicatorVisualConfig.supportsDirectionInversion(indicatorType) or false
end

function MS_CircularStylePanel:isMatrixLinked()
    return self.indicator and self.indicator.matrixLinked == true
end

function MS_CircularStylePanel:getStoredConfig()
    if not self.indicator then return {} end
    return MSConfig.getIndicatorConfig(self.indicator.playerIndex, self.indicator:getType()) or {}
end

function MS_CircularStylePanel:getStoredPosition(axis)
    local config = self:getStoredConfig()
    if config.position and config.position[axis] ~= nil then
        return config.position[axis]
    end
    if axis == "x" then
        return self.indicator and self.indicator:getX() or 0
    end
    return self.indicator and self.indicator:getY() or 0
end

function MS_CircularStylePanel:setStoredPosition(axis, value)
    if not self.indicator then return end

    local config = self:getStoredConfig()
    local position = {
        x = config.position and config.position.x or self:getStoredPosition("x"),
        y = config.position and config.position.y or self:getStoredPosition("y"),
    }
    position[axis] = value

    -- Matrix-linked indicators keep their personal position stored for a later
    -- release, but the live matrix position must remain owned by the matrix.
    if not self:isMatrixLinked() then
        self.indicator:setX(position.x)
        self.indicator:setY(position.y)
    end

    MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "position", position)
end

function MS_CircularStylePanel:getStoredCircularSize()
    local config = self:getStoredConfig()
    if config.circularSize ~= nil then
        return config.circularSize
    end
    return self.indicator and self.indicator:getWidth() or 0
end

function MS_CircularStylePanel:setStoredCircularSize(value)
    if not self.indicator then return end

    -- Matrix-linked indicators keep their personal size stored for a later
    -- release, but the live matrix size must remain owned by the matrix.
    if not self:isMatrixLinked() then
        self.indicator:setWidth(value)
        self.indicator:setHeight(value)
    end

    MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "circularSize", value)
end


-- ---------------------------------------------------------------------------------------- --
-- initialise
-- ---------------------------------------------------------------------------------------- --

function MS_CircularStylePanel:createScrollView()
    self.scrollView = NIScrollView:new(0, 0, self.width, self.height)
    self.scrollView:initialise()
    self.scrollView:setScrollDirection("vertical")
    self.scrollView:setScrollSensitivity(self.rowHeight)
    self:addChild(self.scrollView)
end

function MS_CircularStylePanel:addContentChild(child)
    if self.scrollView then
        self.scrollView:addScrollChild(child)
    else
        self:addChild(child)
    end
end

function MS_CircularStylePanel:createMatrixLinkButton(x, y)
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_Matrix"), 1, 1, 1, 1, UIFont.Small, getText("IGUI_ModernStatus_Tooltip_MatrixLinkToggle"))
    local button = MS_LongButton:new(x, y + ((self.rowHeight-self.buttonSize)/2), 90, self.buttonSize, "", self, MS_CircularStylePanel.onToggleMatrixLink)
    button:initialise()
    button.prerender = function(control)
        local linked = self.indicator and self.indicator.matrixLinked == true
        control:setTitle(linked and getText("IGUI_ModernStatus_Matrix") or getText("IGUI_ModernStatus_Single"))
        control:setActive(linked)
        if control.setTooltip then
            control:setTooltip(getText("IGUI_ModernStatus_Tooltip_MatrixLinkToggle"))
        end
    end
    self:addContentChild(button)
    return button
end

function MS_CircularStylePanel:onToggleMatrixLink()
    if not self.indicator then return end
    MS_MatrixManager.setIndicatorLinked(self.indicator.playerIndex, self.indicator, not (self.indicator.matrixLinked == true))
end

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

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

function MS_CircularStylePanel:new(x, y, width, height, indicator)
    local o = ISPanel:new(x, y, width, height)
    setmetatable(o, self)
    self.__index = self
    
    o:noBackground(true)
    o.indicator = indicator
    o.rowHeight = math.floor(FONT_HGT_SMALL*1.5)
    o.padding = math.floor(FONT_HGT_SMALL*0.2)
    o.buttonSize = math.floor(FONT_HGT_SMALL*1.2)

    o.MinusButton = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Minus.png")
    o.PlusButton = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Plus.png")
    o.resetIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Reset.png")

    o.TextEntryLeft = getTexture("media/ui/ModernStatus/SettingPanel/TextEntry_Left.png")
    o.TextEntryMiddle = getTexture("media/ui/ModernStatus/SettingPanel/TextEntry_Middle.png")
    o.TextEntryRight = getTexture("media/ui/ModernStatus/SettingPanel/TextEntry_Right.png")
    
    return o
end

-- ---------------------------------------------------------------------------------------- --
-- createChildren
-- ---------------------------------------------------------------------------------------- --

function MS_CircularStylePanel:createChildren()
    self:createScrollView()
    local config = MSConfig.getIndicatorConfig(self.indicator.playerIndex, self.indicator:getType())
    if config.circularSize and not self:isMatrixLinked() then
        self.indicator:setWidth(config.circularSize)
        self.indicator:setHeight(config.circularSize)
    end

    local y = 0
    local middleX = self.width / 2
    local controlX = middleX + self.padding

    self.matrixLinkButton = self:createMatrixLinkButton(controlX, y)
    y = y + self.rowHeight


    -- X Position
    self.xPositionControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_X"),
        function() return self:getStoredPosition("x") end,
        function(value)
            self:setStoredPosition("x", value)
        end, 0, getCore():getScreenWidth(), 1, false)
    
    y = y + self.rowHeight
    
    -- y Position
    self.yPositionControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Y"),
        function() return self:getStoredPosition("y") end,
        function(value)
            self:setStoredPosition("y", value)
        end, 0, getCore():getScreenHeight(), 1, false)
    
    y = y + self.rowHeight
    
    -- size Controls
    local config = MSConfig.getIndicatorConfig(self.indicator.playerIndex, self.indicator:getType())
    self.sizeControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Size"),
        function() return self:getStoredCircularSize() end,
        function(value)
            self:setStoredCircularSize(value)
        end, 1, 500, 4, false)
    
    y = y + self.rowHeight
    
    -- Color Picker
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_Color"), 1, 1, 1, 1, UIFont.Small)
    self.colorPreview = ISButton:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize * 2, self.buttonSize, "", self, MS_CircularStylePanel.openColorPicker)
    self.colorPreview:initialise()
    self.colorPreview:instantiate()
    
    local indicatorColor = {r=1, g=1, b=1}
    if self.indicator and self.indicator.indicatorColor then
        indicatorColor = self.indicator.indicatorColor
    end
    
    self.colorPreview.backgroundColor = {
        r = indicatorColor.r,
        g = indicatorColor.g,
        b = indicatorColor.b,
        a = 1
    }
    
    self.colorPreview.borderColor = {r=1, g=1, b=1, a=1}
    self:addContentChild(self.colorPreview)

    -- Reset Color
    self.resetColorBtn = MS_SquareButton:new(controlX + self.buttonSize * 2 + 10, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.resetIcon, self, MS_CircularStylePanel.onResetColor)
    self.resetColorBtn:initialise()
    self:addContentChild(self.resetColorBtn)

    y = y + self.rowHeight
    
    -- opacity Controls
    self.opacityControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_Opacity"),
        function() return self.indicator.opacity or 1.0 end,
        function(value) 
            self.indicator.opacity = value
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "opacity", value)
        end, 0.1, 1.0, 0.1, true)
    
    y = y + self.rowHeight
    
    -- use Gradient
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_UseGradient"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("useGradient"))
    self.useGradientBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onUseGradientChange)
    self.useGradientBox:initialise()
    self.useGradientBox:addOption("")

    local useGradient = false
    if self.indicator and self.indicator.useGradient ~= nil then
        useGradient = self.indicator.useGradient
    end
    self.useGradientBox:setSelected(1, useGradient)
    applyOptionTooltip(self.useGradientBox, self:getOptionTooltip("useGradient"))
    self:addContentChild(self.useGradientBox)

    y = y + self.rowHeight
    
    -- Show Icon
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_ShowIcon"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("showIcon"))
    self.showIconBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onShowIconChange)
    self.showIconBox:initialise()
    self.showIconBox:addOption("")
    
    local showIcon = true
    if self.indicator and self.indicator.showIcon ~= nil then
        showIcon = self.indicator.showIcon
    end
    self.showIconBox:setSelected(1, showIcon)
    applyOptionTooltip(self.showIconBox, self:getOptionTooltip("showIcon"))
    self:addContentChild(self.showIconBox)

    y = y + self.rowHeight

    -- use Mono Icon
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_MonoIcon"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("useMonoIcon"))
    self.useMonoIconBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onUseMonoIconChange)
    self.useMonoIconBox:initialise()
    self.useMonoIconBox:addOption("")
    
    local useMonoIcon = false
    if self.indicator and self.indicator.useMonoIcon ~= nil then
        useMonoIcon = self.indicator.useMonoIcon
    end
    self.useMonoIconBox:setSelected(1, useMonoIcon)
    applyOptionTooltip(self.useMonoIconBox, self:getOptionTooltip("useMonoIcon"))
    self:addContentChild(self.useMonoIconBox)
    
    y = y + self.rowHeight

    -- icon Opacity Controls
    self.iconOpacityControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_IconOpacity"),
        function() return self.indicator.iconOpacity or 1.0 end,
        function(value) 
            self.indicator.iconOpacity = value
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "iconOpacity", value)
        end, 0.1, 1.0, 0.1, true)

    y = y + self.rowHeight

    -- text Scale Controls
    self.textScaleControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_TextSize"),
        function() return self.indicator.textScale or 1.0 end,
        function(value) 
            self.indicator.textScale = value
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "textScale", value)
        end, 0.5, 2.0, 0.1, true)
    
    y = y + self.rowHeight

    -- Update Interval Controls
    self.updateIntervalControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_UpdateIntervalMs"),
        function() return self.indicator.updateIntervalMs or MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(self.indicator:getType()) end,
        function(value)
            value = math.floor(value)
            self.indicator.updateIntervalMs = value
            if self.indicator.invalidateRenderSnapshot then
                self.indicator:invalidateRenderSnapshot()
            end
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "updateIntervalMs", value)
            if MS_PerformanceManager and MS_PerformanceManager.markCustomMode then
                MS_PerformanceManager.markCustomMode(self.indicator.playerIndex)
            end
        end, 50, 10000, 50, false, 0, self:getOptionTooltip("updateIntervalMs"))

    y = y + self.rowHeight

    -- always Show Value
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_AlwaysShowValue"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("alwaysShowValue"))
    self.alwaysShowValueBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onAlwaysShowValueChange)
    self.alwaysShowValueBox:initialise()
    self.alwaysShowValueBox:addOption("")
    
    local alwaysShowValue = false
    if self.indicator and self.indicator.alwaysShowValue ~= nil then
        alwaysShowValue = self.indicator.alwaysShowValue
    end
    self.alwaysShowValueBox:setSelected(1, alwaysShowValue)
    applyOptionTooltip(self.alwaysShowValueBox, self:getOptionTooltip("alwaysShowValue"))
    self:addContentChild(self.alwaysShowValueBox)
    
    y = y + self.rowHeight

    if self:supportsDisplayThresholds() then
        -- CarryWeight can show fixed threshold markers around the circular indicator.
        self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_DisplayThresholds"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("displayThresholds"))
        self.displayThresholdsBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onDisplayThresholdsChange)
        self.displayThresholdsBox:initialise()
        self.displayThresholdsBox:addOption("")

        local displayThresholds = false
        if self.indicator and self.indicator.displayThresholds ~= nil then
            displayThresholds = self.indicator.displayThresholds
        end
        self.displayThresholdsBox:setSelected(1, displayThresholds)
        applyOptionTooltip(self.displayThresholdsBox, self:getOptionTooltip("displayThresholds"))
        self:addContentChild(self.displayThresholdsBox)

        y = y + self.rowHeight
    end

    if self:supportsDirectionInversion() then
        self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_InvertDirection"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("invertDirection"))
        self.invertDirectionBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onInvertDirectionChange)
        self.invertDirectionBox:initialise()
        self.invertDirectionBox:addOption("")
        self.invertDirectionBox:setSelected(1, self.indicator and self.indicator.invertDirection == true)
        applyOptionTooltip(self.invertDirectionBox, self:getOptionTooltip("invertDirection"))
        self:addContentChild(self.invertDirectionBox)

        y = y + self.rowHeight
    end

    -- Alarm Enabled
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_EnableAlarm"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("alarmEnabled"))
    self.alarmEnabledBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onAlarmEnabledChange)
    self.alarmEnabledBox:initialise()
    self.alarmEnabledBox:addOption("")

    local alarmEnabled = true
    if self.indicator and self.indicator.alarmEnabled ~= nil then
        alarmEnabled = self.indicator.alarmEnabled
    end
    self.alarmEnabledBox:setSelected(1, alarmEnabled ~= false)
    applyOptionTooltip(self.alarmEnabledBox, self:getOptionTooltip("alarmEnabled"))
    self:addContentChild(self.alarmEnabledBox)

    y = y + self.rowHeight

    -- threshold Controls
    local spec = self:getThresholdInputSpec()
    self.thresholdControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_AnimThreshold"),
        function() return self.indicator.animationThreshold or MS_IndicatorVisualConfig.getDefaultAlarmThreshold(self.indicator:getType()) end,
        function(value) 
            self.indicator.animationThreshold = value
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "animationThreshold", value)
        end, spec.min, spec.max, spec.step, false, spec.precision, self:getOptionTooltip("animationThreshold"))

    y = y + self.rowHeight

    -- auto Hide
    self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), getText("IGUI_ModernStatus_AutoHide"), 1, 1, 1, 1, UIFont.Small, self:getOptionTooltip("autoHide"))
    self.autoHideBox = ISTickBox:new(controlX, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.buttonSize, "", self, MS_CircularStylePanel.onAutoHideChange)
    self.autoHideBox:initialise()
    self.autoHideBox:addOption("")
    
    local autoHide = false
    if self.indicator and self.indicator.autoHide ~= nil then
        autoHide = self.indicator.autoHide
    end
    self.autoHideBox:setSelected(1, autoHide)
    applyOptionTooltip(self.autoHideBox, self:getOptionTooltip("autoHide"))
    self:addContentChild(self.autoHideBox)
    
    y = y + self.rowHeight

    self.autoHideThresholdControls = self:createStepperControl(controlX, y, getText("IGUI_ModernStatus_AutoHideThreshold"),
        function() return self.indicator.autoHideThreshold or MS_IndicatorVisualConfig.getDefaultAutoHideThreshold(self.indicator:getType()) end,
        function(value)
            self.indicator.autoHideThreshold = value
            MSConfig.updateIndicatorConfig(self.indicator.playerIndex, self.indicator:getType(), "autoHideThreshold", value)
        end, spec.min, spec.max, spec.step, false, spec.precision, self:getOptionTooltip("autoHideThreshold"))

    y = y + self.rowHeight
end


function MS_CircularStylePanel:createStepperControl(x, y, labelText, getValue, setValue, minValue, maxValue, step, isPercentage, precision, tooltip)

    local label = self:createLabel(self.padding, y + ((self.rowHeight-FONT_HGT_SMALL)/2), labelText, 1, 1, 1, 1, UIFont.Small, tooltip)
    applyOptionTooltip(label, tooltip)

    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

    -- textBox
    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*2
    local currentValue = getValue()
    local lastSubmittedValue = formatDisplayValue(currentValue)
    local textBox = ISTextEntryBox:new(lastSubmittedValue, 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))
    
    local function updateFromText()
        local text = textBox:getText()
        if text == "" then return end
        local value = tonumber(text)
        if not value then return end
        
        local rawValue
        if isPercentage then
            value = math.max(minValue * 100, math.min(maxValue * 100, value))
            rawValue = value / 100
        else
            value = math.max(minValue, math.min(maxValue, value))
            rawValue = value
        end

        local formattedValue = formatDisplayValue(rawValue)
        textBox:setText(formattedValue)
        if formattedValue == lastSubmittedValue then
            return
        end

        setValue(rawValue)
        lastSubmittedValue = formattedValue
    end
    
    local function updateTextBox()
        local newValue = getValue()
        lastSubmittedValue = formatDisplayValue(newValue)
        textBox:setText(lastSubmittedValue)
    end
    
    textBox.onCommandEntered = updateFromText
    textBox.onOtherKey = function(key) if key == 28 then updateFromText() end end
    textBox.onMouseDownOutside = updateFromText
    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}
    applyOptionTooltip(textBox, tooltip)
    self:addContentChild(textBox)

    -- decBtn
    local decBtn = MS_SquareButton:new(x, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.MinusButton, self, 
        function() 
            setValue(math.max(minValue, getValue() - step))
            updateTextBox()
        end)
    decBtn:initialise()
    decBtn:instantiate()
    applyOptionTooltip(decBtn, tooltip)
    self:addContentChild(decBtn)
    
    -- incBtn
    local incBtn = MS_SquareButton:new(x + self.buttonSize + self.padding + textBoxWidth + self.padding, y + ((self.rowHeight-self.buttonSize)/2), self.buttonSize, self.PlusButton, self,
        function() 
            setValue(math.min(maxValue, getValue() + step))
            updateTextBox()
        end)
    incBtn:initialise()
    incBtn:instantiate()
    applyOptionTooltip(incBtn, tooltip)
    self:addContentChild(incBtn)
    
    return {decBtn = decBtn, textBox = textBox, incBtn = incBtn}
end

function MS_CircularStylePanel:createLabel(x, y, text, r, g, b, a, font, tooltip)
    local label = ISLabel:new(x, y, 0, text, r, g, b, a, font, true)
    label:initialise()
    label:instantiate()
    applyOptionTooltip(label, tooltip)
    self:addContentChild(label)
    return label
end

-- ---------------------------------------------------------------------------------------- --
-- ColorPicker
-- ---------------------------------------------------------------------------------------- --
function MS_CircularStylePanel:openColorPicker()
    if not self.indicator then return end
    
    local pickerX = self.colorPreview:getAbsoluteX() + self.colorPreview:getWidth() + 5
    local pickerY = self.colorPreview:getAbsoluteY()
    
    if not self.indicator.indicatorColor then
        self.indicator.indicatorColor = {r=1, g=1, b=1}
    end

    local initialColor = ColorInfo.new(
        self.indicator.indicatorColor.r,
        self.indicator.indicatorColor.g,
        self.indicator.indicatorColor.b,
        1
    )
    
    local colorPicker = ISColorPickerHSB:new(pickerX, pickerY, initialColor)
    colorPicker:setPickedFunc(function(target, colorRGB) 
        self:onColorSelected(colorRGB.r, colorRGB.g, colorRGB.b) 
    end)
    colorPicker:initialise()
    colorPicker:addToUIManager()
    colorPicker:setVisible(true)
end

function MS_CircularStylePanel:onColorSelected(r, g, b)
    if not self.indicator then return end
    
    if not self.indicator.indicatorColor then
        self.indicator.indicatorColor = {r=1, g=1, b=1}
    end
    
    self.indicator.indicatorColor.r = r
    self.indicator.indicatorColor.g = g
    self.indicator.indicatorColor.b = b
    
    self:updateColorPreview()
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "color",
        {r = r, g = g, b = b}
    )
end

-- ---------------------------------------------------------------------------------------- --
-- Button Callback
-- ---------------------------------------------------------------------------------------- --
function MS_CircularStylePanel:onUseGradientChange(index, selected)
    if not self.indicator then return end
    self.indicator.useGradient = selected
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "useGradient",
        selected
    )
end

function MS_CircularStylePanel:onShowIconChange(index, selected)
    if not self.indicator then return end
    self.indicator.showIcon = selected
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "showIcon",
        selected
    )
end

function MS_CircularStylePanel:onUseMonoIconChange(index, selected)
    if not self.indicator then return end
    self.indicator.useMonoIcon = selected
    
    self.indicator:updateIconPath()
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "useMonoIcon",
        selected
    )
end

function MS_CircularStylePanel:onResetColor()
    if not self.indicator then return end

    local indicatorType = self.indicator:getType()
    local defaultConfig = MSConfig.getDefaultIndicatorConfig(indicatorType)
    local defaultColor = defaultConfig.color

    self.indicator.indicatorColor = {
        r = defaultColor.r,
        g = defaultColor.g,
        b = defaultColor.b
    }

    self:updateColorPreview()

    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "color",
        {r = defaultColor.r, g = defaultColor.g, b = defaultColor.b}
    )
end

function MS_CircularStylePanel:onAlwaysShowValueChange(index, selected)
    if not self.indicator then return end
    self.indicator.alwaysShowValue = selected
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "alwaysShowValue",
        selected
    )
end

function MS_CircularStylePanel:onInvertDirectionChange(index, selected)
    if not self.indicator or not self:supportsDirectionInversion() then return end
    self.indicator.invertDirection = selected == true

    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "invertDirection",
        self.indicator.invertDirection
    )
end

function MS_CircularStylePanel:onDisplayThresholdsChange(index, selected)
    if not self.indicator then return end
    -- CarryWeight is the only indicator that uses threshold markers.
    self.indicator.displayThresholds = selected

    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "displayThresholds",
        selected
    )
end

function MS_CircularStylePanel:onAlarmEnabledChange(index, selected)
    if not self.indicator then return end
    self.indicator.alarmEnabled = selected

    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "alarmEnabled",
        selected
    )
end

function MS_CircularStylePanel:onAutoHideChange(index, selected)
    if not self.indicator then return end
    self.indicator.autoHide = selected
    
    MSConfig.updateIndicatorConfig(
        self.indicator.playerIndex,
        self.indicator:getType(),
        "autoHide",
        selected
    )
end

function MS_CircularStylePanel:updateColorPreview()
    if not self.indicator or not self.indicator.indicatorColor or not self.colorPreview then return end
    
    self.colorPreview.backgroundColor = {
        r = self.indicator.indicatorColor.r,
        g = self.indicator.indicatorColor.g,
        b = self.indicator.indicatorColor.b,
        a = 1
    }
    self.colorPreview:setBackgroundRGBA(
        self.indicator.indicatorColor.r,
        self.indicator.indicatorColor.g,
        self.indicator.indicatorColor.b,
        1
    )
end