require "ISUI/ISUIElement"
require "ISUI/ISToolTip"
require "MS/MS_IndicatorToolTip"
require "MS/MS_IndicatorTooltipManager"
require "MS/MS_MatrixManager"
require "MS/MS_GridManager"
require "MS/MS_IndicatorVisualConfig"
require "MS/MS_IndicatorTooltipData"

MS_StatusIndicator = ISUIElement:derive("MS_StatusIndicator")
local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small)

local CARRY_WEIGHT_THRESHOLD_COLOR = { r = 0.741, g = 0.741, b = 0.741, a = 0.9 }
local CARRY_WEIGHT_THRESHOLD_MINOR_COLOR = { r = 0.416, g = 0.416, b = 0.416, a = 0.95 }

local RUNTIME_AUTO_HIDDEN_X = -20000
local RUNTIME_AUTO_HIDDEN_Y = -20000
-- ---------------------------------------------------------------------------------------- --
-- initialise
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:initialise()
    ISUIElement.initialise(self)
    
    -- Icon Scale
    self.iconAnimActive = false
    self.iconAnimTime = 0
    self.iconAnimDir = 1
    self.iconAnimScale = 1.0
    self.lastAnimTriggerTime = 0
    
    -- Icon Sway
    self.iconSwayActive = false
    self.iconSwayTime = 0
    self.iconSwayOffset = 0
    
    -- Bar Blink
    self.barAnimActive = false
    self.barAnimTime = 0
    self.barAnimTransition = 0

    self:loadConfig()
end

function MS_StatusIndicator:new(x, y, width, height, player)
    local o = ISUIElement:new(x, y, width, height)
    setmetatable(o, self)
    self.__index = self
    
    o:setOnMouseDoubleClick(o, StatusIndicatorActions.onDoubleClick)
    o.player = player
    o.playerIndex = player:getPlayerNum()
    o.moveWithMouse = true
    o.padding = math.floor(FONT_HGT_SMALL * 0.2)
    o.iconPath = nil
    o.indicatorColor = {r=1, g=1, b=1}
    o.opacity = 1.0
    o.useGradient = false
    o.defaultX = x
    o.defaultY = y
    o.isHovered = false
    o._tooltipVisible = false
    o._matrixAutoHidden = false
    o.showIcon = true
    o.useMonoIcon = false
    o.iconPosition = "left"
    o.isBarStyle = false
    o.isVertical = false
    o.autoHide = false 
    o.autoHideThreshold = 0
    o.alarmEnabled = false
    o.invertDirection = false

    o.segmentTextures = {}
    for i=1, 12 do
        local texturePath = "media/ui/ModernStatus/CircularStatus/segment_" .. i .. ".png"
        o.segmentTextures[i] = getTexture(texturePath)
    end
    
    o.circularBgTexture = getTexture("media/ui/ModernStatus/CircularStatus/background.png")
    o.circularLineTexture = getTexture("media/ui/ModernStatus/CircularStatus/background_Line.png")
    o.circularBgShader = getTexture("media/ui/ModernStatus/CircularStatus/background_Shader.png")
    o.iconbackground = getTexture("media/ui/ModernStatus/BarStatus/BarStatus_iconBG.png")

    o.barBackgroundLeft = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_Left_64.png")
    o.barBackgroundMiddle = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_Middle_64.png")
    o.barBackgroundRight = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_Right_64.png")
    
    o.barFillLeft = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_FLeft_64.png")
    o.barFillMiddle = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_FMiddle_64.png")
    o.barFillRight = getTexture("media/ui/ModernStatus/HorizontalBar/ModernStatus_Horizontal_FRight_64.png")

    o.barBackgroundTop = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_Top_64.png")
    o.barBackgroundMiddleVertical = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_Middle_64.png")
    o.barBackgroundBottom = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_Bottom_64.png")
    
    o.barFillTop = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_FTop_64.png")
    o.barFillMiddleVertical = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_FMiddle_64.png")
    o.barFillBottom = getTexture("media/ui/ModernStatus/VerticalBar/ModernStatus_Vertical_FBottom_64.png")
    
    return o
end

function MS_StatusIndicator:updateIconPath()
    if not self.baseIconName then return end

    local iconName = self.iconName or self.baseIconName

    if self.useMonoIcon then
        self.iconPath = "media/ui/ModernStatus/MonoIcon/" .. iconName .. ".png"
    elseif self.preferModernStatusColorIcon == true then
        -- Some mod integrations share icon names with their own moodle icon packs.
        -- Prefer Modern Status bundled color icons when the indicator requests it.
        self.iconPath = "media/ui/ModernStatus/ColorIcon/" .. iconName .. ".png"
    else
        local moddlePath = "media/ui/Moodles/128/" .. iconName .. ".png"
        local moddleTexture = getTexture(moddlePath)

        if moddleTexture then
            self.iconPath = moddlePath
        else
            self.iconPath = "media/ui/ModernStatus/ColorIcon/" .. iconName .. ".png"
        end
    end

    if self.iconPath and self.iconPath ~= "" then
        self.iconTexture = getTexture(self.iconPath)
    end
end

-- ---------------------------------------------------------------------------------------- --
-- loadConfig
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:loadConfig()
    if not self.playerIndex then return end
    
    local config = MSConfig.getIndicatorConfig(self.playerIndex, self:getType())

    -- Presets can switch indicators between normal, hidden, auto-hidden and matrix layouts.
    -- Clear transient runtime placement flags before applying the saved config so stale
    -- off-screen auto-hide positions cannot survive the first preset load pass.
    self._runtimeAutoHidden = false
    self._runtimeAutoHiddenSavedX = nil
    self._runtimeAutoHiddenSavedY = nil
    self._matrixAutoHidden = false
    self.isHovered = false
    self.moving = false
    self.matrixDragging = false
    if self.setCapture then
        self:setCapture(false)
    end
    if self.hideTooltipUI then
        self:hideTooltipUI()
    end
    
    self.isBarStyle = config.style == "bar"
    
    if config.position then
        self:setX(config.position.x)
        self:setY(config.position.y)
    end
    
    if self.isBarStyle then
        if config.barSize then
            self:setWidth(config.barSize.width)
            self:setHeight(config.barSize.height)
        end
    else
        if config.circularSize then
            local size = config.circularSize
            self:setWidth(size)
            self:setHeight(size)
        end
    end

    if config.color then
        self.indicatorColor = {
            r = config.color.r,
            g = config.color.g,
            b = config.color.b
        }
    end
    
    if config.opacity ~= nil then
        self.opacity = config.opacity
    end

    if config.iconOpacity ~= nil then
        self.iconOpacity = config.iconOpacity
    end
    
    if config.useGradient ~= nil then
        self.useGradient = config.useGradient
    end
    
    if config.showIcon ~= nil then
        self.showIcon = config.showIcon
    end
    
    if config.useMonoIcon ~= nil then
        self.useMonoIcon = config.useMonoIcon
    end

    if config.iconPosition ~= nil then
        self.iconPosition = config.iconPosition
    end
    
    if config.isVertical ~= nil and self.isBarStyle then
        self.isVertical = config.isVertical
    end
    
    if config.locked ~= nil then
        self.locked = config.locked
        self.moveWithMouse = not self.locked
    end

    if config.alwaysShowValue ~= nil then
        self.alwaysShowValue = config.alwaysShowValue
    end

    if config.invertDirection ~= nil then
        self.invertDirection = config.invertDirection
    end
    
    if config.hidden ~= nil then
        self.hidden = config.hidden
        self:setVisible(not self.hidden)
    end

    if config.alarmEnabled ~= nil then
        self.alarmEnabled = config.alarmEnabled
    end

    if config.animationThreshold ~= nil then
        self.animationThreshold = config.animationThreshold
    end
    
    if config.showName ~= nil then
        self.showName = config.showName
    end
    
    if config.textScale ~= nil then
        self.textScale = config.textScale
    end

    if config.autoHide ~= nil then
        self.autoHide = config.autoHide
    end

    if config.autoHideThreshold ~= nil then
        self.autoHideThreshold = config.autoHideThreshold
    end

    if config.displayThresholds ~= nil then
        self.displayThresholds = config.displayThresholds
    else
        self.displayThresholds = false
    end

    if config.updateIntervalMs ~= nil then
        self.updateIntervalMs = config.updateIntervalMs
    elseif MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs then
        self.updateIntervalMs = MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(self:getType())
    else
        self.updateIntervalMs = 1000
    end

    self.matrixLinked = config.matrixLinked == true
    self.matrixLinkOrder = config.matrixLinkOrder or 0
    self.matrixDragging = false

    self._indicatorDisplayNameCacheKey = nil
    self._indicatorDisplayNameCacheValue = nil
    self:updateIconPath()
end

-- ---------------------------------------------------------------------------------------- --
-- Animation
-- ---------------------------------------------------------------------------------------- --

function MS_StatusIndicator:updateIconAnimation(delta)
    if not self.iconAnimActive then return end
    self.iconAnimTime = self.iconAnimTime + delta

    if self.iconAnimDir == 1 and self.iconAnimTime < 0.5 then
        self.iconAnimScale = 1.0 - (self.iconAnimTime / 0.5) * 0.3
    elseif self.iconAnimDir == 1 and self.iconAnimTime < 1.0 then
        self.iconAnimScale = 0.7 + ((self.iconAnimTime - 0.5) / 0.5) * 0.3
    else
        if self.iconAnimTime >= 1.0 then
            self.iconAnimActive = false
            self.iconAnimScale = 1.0
        end
    end
end

function MS_StatusIndicator:updateBarAnimation(delta)
    if not self.barAnimActive then return end
    self.barAnimTime = self.barAnimTime + delta
    local cycle = self.barAnimTime / 1.0
    local transitionFactor = (math.sin(cycle * math.pi * 2) + 1) / 2
    self.barAnimTransition = transitionFactor
    
    if self.barAnimTime >= 1.0 then
        self.barAnimTime = 0
        local value = self:getValue()
        if not self:checkThresholdExceeded(value) then
            self.barAnimActive = false
        end
    end
end

function MS_StatusIndicator:updateIconSwayAnimation(delta)
    if not self.iconSwayActive then return end
    
    self.iconSwayTime = self.iconSwayTime + delta
    
    local frequency = 1.0  -- shake frequency
    local amplitude = 3.0  -- shake amplitude
    
    self.iconSwayOffset = math.sin(self.iconSwayTime * frequency * math.pi) * amplitude
    
    if self.iconSwayTime >= 1.0 then
        self.iconSwayTime = 0
        local value = self:getValue()
        if not self:checkThresholdExceeded(value) then
            self.iconSwayActive = false
        end
    end
end

function MS_StatusIndicator:checkTriggerAnimation(value, thresholdExceeded)
    if self.alarmEnabled == false then
        self.iconAnimActive = false
        self.barAnimActive = false
        self.iconSwayActive = false
        self.iconAnimScale = 1.0
        self.iconSwayOffset = 0
        self.barAnimTransition = 0
        return
    end

    local currentTime = getTimestampMs()
    if currentTime - self.lastAnimTriggerTime < 1000 then
        return
    end
    local triggerAnim = thresholdExceeded
    if triggerAnim == nil then
        triggerAnim = self:checkThresholdExceeded(value)
    end
    
    if triggerAnim then
        if self.isBarStyle then
            if not self.barAnimActive then
                self.barAnimActive = true
                self.barAnimTime = 0
                self.barAnimColor = 1
                self.lastAnimTriggerTime = currentTime
                StatusWidget.hasActiveAnimations = true
            end

            if self.showIcon and not self.iconSwayActive then
                self.iconSwayActive = true
                self.iconSwayTime = 0
                StatusWidget.hasActiveAnimations = true
            end
        else
            if not self.iconAnimActive then
                self.iconAnimActive = true
                self.iconAnimTime = 0
                self.iconAnimDir = 1
                self.lastAnimTriggerTime = currentTime
                StatusWidget.hasActiveAnimations = true
            end
        end
    end
end

function MS_StatusIndicator:isValuePastThreshold(value, thresholdValue)
    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.isThresholdExceeded then
        local ok, result = pcall(MS_IndicatorVisualConfig.isThresholdExceeded, self, value, thresholdValue)
        if ok and result ~= nil then
            return result == true
        end
    end

    local indicatorType = self:getType()
    local numericThreshold = tonumber(thresholdValue) or 0
    if MS_PlayerStatus.isHigherBetter(indicatorType) then
        if numericThreshold <= 0 then
            return value < 0.9999
        end
        return value <= (numericThreshold / 100)
    end

    if numericThreshold <= 0 then
        return value > 0.0001
    end
    return value >= (numericThreshold / 100)
end

function MS_StatusIndicator:checkThresholdExceeded(value)
    if self.alarmEnabled == false then
        return false
    end

    local indicatorType = self:getType()
    local config = MSConfig.getIndicatorConfig(self.playerIndex, indicatorType)
    local userThreshold = config.animationThreshold
    if userThreshold == nil then
        userThreshold = self.animationThreshold
    end
    return self:isValuePastThreshold(value, userThreshold)
end

function MS_StatusIndicator:checkAutoHideExceeded(value)
    local config = MSConfig.getIndicatorConfig(self.playerIndex, self:getType())
    local threshold = config.autoHideThreshold
    if threshold == nil then
        threshold = self.autoHideThreshold or 0
    end

    -- Auto-hide can use a different comparison mode from alarms for indicators
    -- whose useful/neutral point is in the middle of the scale.
    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.isAutoHideExceeded then
        local ok, result = pcall(MS_IndicatorVisualConfig.isAutoHideExceeded, self, value, threshold)
        if ok and result ~= nil then
            return result == true
        end
    end

    return self:isValuePastThreshold(value, threshold)
end

function MS_StatusIndicator:isCarryWeightIndicator()
    local indicatorType = self:getType()
    return indicatorType == "MS_CarryWeightIndicator" or indicatorType == "ISCarryWeightStatusIndicator"
end

function MS_StatusIndicator:isNoxiousExposureIndicator()
    local indicatorType = self:getType()
    return indicatorType == "MS_NoxiousExposureIndicator" or indicatorType == "ISNoxiousExposureStatusIndicator"
end

function MS_StatusIndicator:getCarryWeightData(includeDetails)
    local emptyData = {
        weight = 0,
        capacity = 0,
        loadRatio = 0,
        visualRatio = 0,
        equippedItemsWeight = 0,
        otherInventoryWeight = 0,
        equippedContainersGross = 0,
        equippedContainersReduced = 0,
        equippedContainers = {},
        capacityEffects = {},
        capacityPenaltyTotal = 0,
        baseCapacity = 0,
        strengthMultiplier = 1,
        traitMultiplier = 1,
    }

    if not self:isCarryWeightIndicator() or not self.player then
        return emptyData
    end

    local currentTime = getTimestampMs and getTimestampMs() or 0

    if includeDetails == true then
        -- Detailed Carry Weight information is expensive because it iterates the
        -- inventory and builds tooltip-only breakdowns. Cache it separately.
        local detailedCache = self._carryWeightDetailedCache
        if detailedCache and (currentTime - (detailedCache.time or 0)) < 1000 then
            return detailedCache.data or emptyData
        end

        local detailedData = emptyData
        if MS_PlayerStatus.Get.CarryWeightData then
            detailedData = MS_PlayerStatus.Get.CarryWeightData(self.player, true)
        end
        self._carryWeightDetailedCache = { time = currentTime, data = detailedData }
        return detailedData
    end

    local quickInterval = self:getUpdateIntervalMs()
    local quickCache = self._carryWeightQuickCache
    if quickCache and (currentTime - (quickCache.time or 0)) < quickInterval then
        return quickCache.data or emptyData
    end

    local quickData = emptyData
    if MS_PlayerStatus.Get.CarryWeightData then
        quickData = MS_PlayerStatus.Get.CarryWeightData(self.player, false)
    end
    self._carryWeightQuickCache = { time = currentTime, data = quickData }
    return quickData
end

function MS_StatusIndicator:getCarryWeightDetailedData()
    return self:getCarryWeightData(true)
end

function MS_StatusIndicator:getCarryWeightBandColor()
    local carryData = self:getCarryWeightData()
    local ratio = carryData.loadRatio or 0

    if ratio <= 0 then
        return { r = 0x7A / 255, g = 0x7A / 255, b = 0x7A / 255 }
    elseif ratio <= 0.5 then
        return { r = 0x5F / 255, g = 0x7D / 255, b = 0x95 / 255 }
    elseif ratio <= 1.0 then
        return { r = 0x2F / 255, g = 0xAF / 255, b = 0x88 / 255 }
    elseif ratio <= 1.25 then
        return { r = 0xC8 / 255, g = 0xB4 / 255, b = 0x4E / 255 }
    elseif ratio <= 1.5 then
        return { r = 0xD9 / 255, g = 0x89 / 255, b = 0x2E / 255 }
    elseif ratio <= 1.75 then
        return { r = 0xD5 / 255, g = 0x54 / 255, b = 0x2F / 255 }
    end

    return { r = 0xB7 / 255, g = 0x1C / 255, b = 0x1C / 255 }
end

function MS_StatusIndicator:getBarIconPosition()
    if self.isVertical then
        return self.iconPosition == "top" and "top" or "bottom"
    end
    return self.iconPosition == "right" and "right" or "left"
end

function MS_StatusIndicator:supportsDirectionInversion()
    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.supportsDirectionInversion then
        return MS_IndicatorVisualConfig.supportsDirectionInversion(self:getType())
    end

    return false
end

function MS_StatusIndicator:isDisplayDirectionInverted()
    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.isDisplayDirectionInverted then
        return MS_IndicatorVisualConfig.isDisplayDirectionInverted(self)
    end

    return self.invertDirection == true and self:supportsDirectionInversion()
end

function MS_StatusIndicator:getVisualFillRatio(baseValue)
    local ratio = 0

    if self:isCarryWeightIndicator() then
        ratio = self:getCarryWeightData().visualRatio or 0
    elseif MS_IndicatorVisualConfig then
        ratio = MS_IndicatorVisualConfig.getVisualRatio(self, baseValue)
    else
        ratio = tonumber(baseValue) or 0
    end

    if ratio < 0 then ratio = 0 end
    if ratio > 1 then ratio = 1 end

    if self:isDisplayDirectionInverted() then
        ratio = 1 - ratio
    end

    return ratio
end

-- Some inverted risk indicators read more clearly with a pure-green perfect state,
-- but only while Gradient Colors is enabled. Fixed-color mode should keep the
-- configured base color across the whole range.
function MS_StatusIndicator:shouldUsePureFullColor(baseValue, visualValue)
    if not self.useGradient then
        return false
    end

    if not self:isDisplayDirectionInverted() then
        return false
    end

    if not MS_IndicatorVisualConfig or not MS_IndicatorVisualConfig.usesPureFullColorWhenInverted then
        return false
    end

    if not MS_IndicatorVisualConfig.usesPureFullColorWhenInverted(self:getType()) then
        return false
    end

    local fillRatio = tonumber(visualValue)
    if fillRatio == nil then
        fillRatio = self:getVisualFillRatio(baseValue)
    end

    return fillRatio ~= nil and fillRatio >= 0.999
end

-- Resolve the final indicator fill color after gradients, threshold bands, and special inverted-full overrides.
function MS_StatusIndicator:getResolvedIndicatorColor(baseValue, visualValue)
    if self:shouldUsePureFullColor(baseValue, visualValue) and MS_IndicatorVisualConfig.getFullColor then
        return MS_IndicatorVisualConfig.getFullColor()
    end

    local color = {
        r = self.indicatorColor and self.indicatorColor.r or 1.0,
        g = self.indicatorColor and self.indicatorColor.g or 1.0,
        b = self.indicatorColor and self.indicatorColor.b or 1.0,
    }

    local configuredColor = self:getConfiguredBandColor(baseValue)
    if configuredColor then
        color.r = configuredColor.r
        color.g = configuredColor.g
        color.b = configuredColor.b
    elseif self.useGradient then
        local gradColor = self:getGradientColor(baseValue)
        color.r = gradColor.r
        color.g = gradColor.g
        color.b = gradColor.b
    end

    return color
end

function MS_StatusIndicator:getConfiguredBandColor(baseValue)
    if not self.useGradient then
        return nil
    end

    if self:isCarryWeightIndicator() then
        return self:getCarryWeightBandColor()
    end

    if MS_IndicatorVisualConfig then
        return MS_IndicatorVisualConfig.getBandColor(self, baseValue)
    end

    return nil
end

function MS_StatusIndicator:shouldDisplayConfiguredThresholds(baseValue)
    if self:isCarryWeightIndicator() then
        return self.displayThresholds == true and (self:getCarryWeightData().capacity or 0) > 0
    end

    if self.displayThresholds ~= true or not MS_IndicatorVisualConfig then
        return false
    end

    return MS_IndicatorVisualConfig.supportsThresholds(self:getType())
end

function MS_StatusIndicator:getThresholdMarkers(baseValue)
    if not self:shouldDisplayConfiguredThresholds(baseValue) then
        return nil
    end

    if self:isCarryWeightIndicator() then
        local markers = {}
        local positions = { 0.25, 0.5, 0.625, 0.75, 0.875 }
        for _, position in ipairs(positions) do
            local color = math.abs(position - 0.5) < 0.0001 and CARRY_WEIGHT_THRESHOLD_COLOR or CARRY_WEIGHT_THRESHOLD_MINOR_COLOR
            markers[#markers + 1] = { position = position, color = color }
        end
        return markers
    end

    if not MS_IndicatorVisualConfig then
        return nil
    end

    local thresholds = MS_IndicatorVisualConfig.getThresholds(self, baseValue)
    if not thresholds or #thresholds == 0 then
        return nil
    end

    local minValue, maxValue = MS_IndicatorVisualConfig.getVisualRange(self, baseValue)
    local span = (maxValue or 1) - (minValue or 0)
    if span <= 0 then
        return nil
    end

    local majorColor = { r = 0xD2 / 255, g = 0xD2 / 255, b = 0xD2 / 255, a = 0.9 }
    local minorColor = { r = 0xBD / 255, g = 0xBD / 255, b = 0xBD / 255, a = 0.9 }
    local markers = {}

    for _, threshold in ipairs(thresholds) do
        local position = ((threshold.value or 0) - minValue) / span
        if position >= 0 and position <= 1 then
            if self:isDisplayDirectionInverted() then
                position = 1 - position
            end

            markers[#markers + 1] = {
                position = position,
                color = threshold.major and majorColor or minorColor,
            }
        end
    end

    return markers
end

function MS_StatusIndicator:drawThresholdsHorizontal(markers)
    -- Use different halves to make normal/inverted threshold direction easier to distinguish.
    local useUpperHalf = self:isDisplayDirectionInverted()
    local thresholdY = useUpperHalf and 0 or math.floor(self.height / 2)
    local thresholdHeight = math.max(1, math.ceil(self.height / 2))

    for _, marker in ipairs(markers or {}) do
        local x = math.floor(self.width * marker.position)
        local color = marker.color
        self:drawRect(x, thresholdY, 2, thresholdHeight, color.a, color.r, color.g, color.b)
    end
end

function MS_StatusIndicator:drawThresholdsVertical(markers)
    -- Use left/right halves to separate normal and inverted threshold direction in vertical bars.
    local useRightHalf = self:isDisplayDirectionInverted()
    local thresholdX = useRightHalf and math.floor(self.width / 2) or 0
    local thresholdWidth = math.max(1, math.ceil(self.width / 2))

    for _, marker in ipairs(markers or {}) do
        local y = math.floor(self.height - (self.height * marker.position))
        local color = marker.color
        self:drawRect(thresholdX, y, thresholdWidth, 2, color.a, color.r, color.g, color.b)
    end
end

function MS_StatusIndicator:drawThresholdsCircular(markers)
    local centerX = self.width / 2
    local centerY = self.height / 2
    local size = math.min(self.width, self.height)
    local ringInnerRadius = size * 0.35
    local ringOuterRadius = size * 0.50
    local ringMidRadius = ringInnerRadius + ((ringOuterRadius - ringInnerRadius) / 2)
    local innerRadius = ringInnerRadius
    local outerRadius = ringMidRadius

    -- Use the inner half for the default direction and the outer half for inverted direction.
    if self:isDisplayDirectionInverted() then
        innerRadius = ringMidRadius
        outerRadius = ringOuterRadius
    end

    for _, marker in ipairs(markers or {}) do
        local angle = (-90 - (360 * marker.position)) * math.pi / 180
        local cosValue = math.cos(angle)
        local sinValue = math.sin(angle)
        local color = marker.color

        for step = innerRadius, outerRadius, 1 do
            local pointX = math.floor(centerX + (cosValue * step))
            local pointY = math.floor(centerY + (sinValue * step))
            self:drawRect(pointX, pointY, 2, 2, color.a, color.r, color.g, color.b)
        end
    end
end


function MS_StatusIndicator:getAlarmBorderColor()
    local transition = self.barAnimTransition or 0
    if not self.isBarStyle and self.iconAnimActive then
        transition = (math.sin((self.iconAnimTime or 0) * math.pi * 2) + 1) / 2
    end
    local r1, g1, b1 = 0.95, 0.28, 0.05
    local r2, g2, b2 = 0.0, 0.0, 0.0
    return {
        r = r1 * transition + r2 * (1 - transition),
        g = g1 * transition + g2 * (1 - transition),
        b = b1 * transition + b2 * (1 - transition),
        a = 0.95,
    }
end

function MS_StatusIndicator:drawAlarmBorder()
    local color = self:getAlarmBorderColor()
    if self.isBarStyle then
        self:drawRectBorder(0, 0, self.width, self.height, color.a, color.r, color.g, color.b)
        if self.width > 4 and self.height > 4 then
            self:drawRectBorder(1, 1, self.width - 2, self.height - 2, color.a * 0.45, color.r, color.g, color.b)
        end
        return
    end

    self:drawTextureScaled(self.circularLineTexture, 0, 0, self.width, self.height, color.a, color.r, color.g, color.b)
end


local function _tooltipSettingToFlag(value, defaultTrue)
    if value == nil then
        return defaultTrue and "1" or "0"
    end
    return value == true and "1" or "0"
end

local function _buildTooltipSettingsCacheKey(settings)
    if not settings then
        return "default"
    end

    return table.concat({
        _tooltipSettingToFlag(settings.showName, true),
        _tooltipSettingToFlag(settings.showValue, true),
        _tooltipSettingToFlag(settings.showThresholds, false),
        _tooltipSettingToFlag(settings.showAssociatedMoodle, true),
        _tooltipSettingToFlag(settings.showMoodleTitle, true),
        _tooltipSettingToFlag(settings.showMoodleDescription, true),
        _tooltipSettingToFlag(settings.showDetailedInfo, true),
        tostring(settings.opacity or 100),
    }, "|")
end

local function _buildTooltipValueCacheKey(value)
    local numericValue = tonumber(value)
    if numericValue == nil then
        return tostring(value)
    end

    if math.abs(numericValue) <= 10 then
        return string.format("%.3f", numericValue)
    end

    return string.format("%.2f", numericValue)
end

local function _clampUpdateIntervalMs(value)
    local interval = tonumber(value) or 1000
    if interval < 50 then interval = 50 end
    if interval > 60000 then interval = 60000 end
    return math.floor(interval)
end

function MS_StatusIndicator:getUpdateIntervalMs()
    if self.updateIntervalMs ~= nil then
        return _clampUpdateIntervalMs(self.updateIntervalMs)
    end

    if MS_IndicatorVisualConfig and MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs then
        return _clampUpdateIntervalMs(MS_IndicatorVisualConfig.getDefaultUpdateIntervalMs(self:getType()))
    end

    return 1000
end

local function _getIndicatorRenderCacheIntervalMs(indicator, mouseOver)
    -- Keep rendering smooth while rebuilding value/color/threshold snapshots only
    -- at the indicator's configured cadence. Heavy indicators keep their own
    -- interval while hovered; tooltip details are refreshed through a separate
    -- cache so hover does not force costly status recalculations.
    local interval = indicator and indicator.getUpdateIntervalMs and indicator:getUpdateIntervalMs() or 1000
    if mouseOver == true and interval > 500 then
        if indicator and ((indicator.isCarryWeightIndicator and indicator:isCarryWeightIndicator()) or (indicator.isNoxiousExposureIndicator and indicator:isNoxiousExposureIndicator())) then
            return interval
        end
        return 500
    end
    return interval
end

local function _getSmallFontHeight()
    return FONT_HGT_SMALL or getTextManager():getFontHeight(UIFont.Small)
end

local function _measureSmallTextCached(indicator, text)
    text = tostring(text or "")
    if indicator._smallTextWidthCacheText == text and indicator._smallTextWidthCacheValue then
        return indicator._smallTextWidthCacheValue
    end

    local width = getTextManager():MeasureStringX(UIFont.Small, text)
    indicator._smallTextWidthCacheText = text
    indicator._smallTextWidthCacheValue = width
    return width
end

function MS_StatusIndicator:invalidateRenderSnapshot()
    self._renderSnapshot = nil
end

function MS_StatusIndicator:getRenderSnapshot(mouseOver)
    local currentTime = getTimestampMs and getTimestampMs() or 0
    local refreshInterval = _getIndicatorRenderCacheIntervalMs(self, mouseOver == true)
    local snapshot = self._renderSnapshot

    if snapshot and (currentTime - (snapshot.time or 0)) < refreshInterval then
        return snapshot
    end

    local value = self:getValue()
    local autoHideExceeded = (not self.autoHide) or self:checkAutoHideExceeded(value)

    if not autoHideExceeded then
        -- Auto-hidden indicators only need the current value and auto-hide test.
        -- Avoid calculating visual fill, colors, threshold markers and alarm state
        -- while nothing is actually drawn or interactable.
        snapshot = {
            time = currentTime,
            value = value,
            autoHideExceeded = false,
        }
        self._renderSnapshot = snapshot
        return snapshot
    end

    local visualValue = self:getVisualFillRatio(value)
    local resolvedColor = self:getResolvedIndicatorColor(value, visualValue)
    local thresholdMarkers = self:getThresholdMarkers(value)
    local thresholdExceeded = self:checkThresholdExceeded(value)

    snapshot = {
        time = currentTime,
        value = value,
        visualValue = visualValue,
        autoHideExceeded = true,
        resolvedColor = resolvedColor,
        thresholdMarkers = thresholdMarkers,
        thresholdExceeded = thresholdExceeded,
    }

    if mouseOver or self.alwaysShowValue then
        snapshot.hoverText = self:getHoverText(value)
    end

    self._renderSnapshot = snapshot
    return snapshot
end

function MS_StatusIndicator:ensureTooltipUI()
    if self.tooltipUI then
        return self.tooltipUI
    end

    -- Create a reusable tooltip panel that follows the mouse while the user
    -- hovers the indicator.
    local tooltipUI = MS_IndicatorToolTip:new()
    tooltipUI:initialise()
    tooltipUI:addToUIManager()
    tooltipUI:setOwner(self)
    tooltipUI:setVisible(false)
    tooltipUI:setAlwaysOnTop(true)
    tooltipUI.followMouse = true
    tooltipUI.maxLineWidth = 420
    self.tooltipUI = tooltipUI

    return tooltipUI
end

function MS_StatusIndicator:hideTooltipUI()
    self._tooltipHoverFrameCounter = 0

    local tooltipUI = self.tooltipUI
    if not tooltipUI then
        self._tooltipVisible = false
        return
    end

    local isVisible = tooltipUI.getIsVisible and tooltipUI:getIsVisible() or false
    if not isVisible and self._tooltipVisible ~= true then
        return
    end

    if tooltipUI.setAlignedDetailRows then
        tooltipUI:setAlignedDetailRows(nil)
    end
    if isVisible then
        tooltipUI:setVisible(false)
        tooltipUI:removeFromUIManager()
    end
    self._tooltipVisible = false
end

local MOODLE_TOOLTIP_COLOR = { r = 1.0, g = 0.86, b = 0.18 }
local MOODLE_DESCRIPTION_COLOR = { r = 0.74, g = 0.60, b = 0.24 }

local function _appendTooltipTextRow(rows, text, color)
    if not text or text == "" then
        return
    end

    -- Some Moodle descriptions use the vanilla <LINE> marker for explicit line breaks.
    -- Structured tooltip rows do not pass through ISRichTextPanel, so split those markers here.
    local textValue = tostring(text):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("%s*<LINE>%s*", "\n")
    for line in string.gmatch(textValue .. "\n", "([^\n]*)\n") do
        if line ~= "" then
            rows[#rows + 1] = {
                label = line,
                value = nil,
                leftOnly = true,
                labelColor = color,
            }
        end
    end
end
function MS_StatusIndicator:getTooltipMoodleRows(value, settings)
    local rows = {}

    if not settings or settings.showAssociatedMoodle ~= false then
        local moodleLines = MS_IndicatorTooltipData and MS_IndicatorTooltipData.getAssociatedMoodleLines and MS_IndicatorTooltipData.getAssociatedMoodleLines(self, value) or nil
        if moodleLines then
            for _, line in ipairs(moodleLines) do
                _appendTooltipTextRow(rows, line, MOODLE_TOOLTIP_COLOR)
            end
        end
    end

    local moodleInfo = MS_IndicatorTooltipData and MS_IndicatorTooltipData.getAssociatedMoodle and MS_IndicatorTooltipData.getAssociatedMoodle(self, value) or nil
    if moodleInfo then
        if (not settings or settings.showMoodleTitle ~= false) and moodleInfo.title and moodleInfo.title ~= "" then
            _appendTooltipTextRow(rows, moodleInfo.title, MOODLE_TOOLTIP_COLOR)
        end
        if (not settings or settings.showMoodleDescription ~= false) and moodleInfo.description and moodleInfo.description ~= "" then
            _appendTooltipTextRow(rows, moodleInfo.description, MOODLE_DESCRIPTION_COLOR)
        end
    end

    return #rows > 0 and rows or nil
end

function MS_StatusIndicator:getTooltipDescription(value)
    local settings = MS_IndicatorTooltipManager and MS_IndicatorTooltipManager.getSettings and MS_IndicatorTooltipManager.getSettings(self.playerIndex) or nil
    local lines = {}

    if settings and settings.showThresholds == true then
        local thresholdLine = MS_IndicatorTooltipData and MS_IndicatorTooltipData.getTooltipThresholdLine and MS_IndicatorTooltipData.getTooltipThresholdLine(self, value) or nil
        if thresholdLine and thresholdLine ~= "" then
            lines[#lines + 1] = thresholdLine
        end
    end

    -- Moodle-related tooltip rows are rendered by MS_IndicatorToolTip as
    -- structured rows. Avoid using RichText color tags here because they can
    -- create same-y line fragments in ISRichTextPanel and break tooltip height.
    return table.concat(lines, " <LINE> ")
end

function MS_StatusIndicator:getTooltipDetailRows(value, settings)
    local rows = {}

    local moodleRows = self:getTooltipMoodleRows(value, settings)
    if moodleRows then
        for _, row in ipairs(moodleRows) do
            rows[#rows + 1] = row
        end
    end

    if not settings or settings.showDetailedInfo ~= false then
        if MS_IndicatorTooltipData and MS_IndicatorTooltipData.getDetailedInfoRows then
            local currentTime = getTimestampMs and getTimestampMs() or 0
            local valueKey = _buildTooltipValueCacheKey(value)
            local settingsKey = _buildTooltipSettingsCacheKey(settings)
            local refreshInterval = 750
            if self:isCarryWeightIndicator() then
                refreshInterval = 1500
            elseif self:isNoxiousExposureIndicator() then
                refreshInterval = 2000
            end

            local cachedRowsValid = self._tooltipDetailRows
                and self._tooltipDetailRowsValueKey == valueKey
                and self._tooltipDetailRowsSettingsKey == settingsKey
                and (currentTime - (self._tooltipDetailRowsTime or 0)) < refreshInterval

            local detailRows = nil
            if cachedRowsValid then
                detailRows = self._tooltipDetailRows
            else
                detailRows = MS_IndicatorTooltipData.getDetailedInfoRows(self, value)
                self._tooltipDetailRows = detailRows
                self._tooltipDetailRowsValueKey = valueKey
                self._tooltipDetailRowsSettingsKey = settingsKey
                self._tooltipDetailRowsTime = currentTime
            end

            if detailRows then
                for _, row in ipairs(detailRows) do
                    rows[#rows + 1] = row
                end
            end
        end
    else
        self._tooltipDetailRows = nil
        self._tooltipDetailRowsValueKey = nil
        self._tooltipDetailRowsSettingsKey = nil
    end

    return #rows > 0 and rows or nil
end

function MS_StatusIndicator:updateMatrixAutoHiddenState(autoHidden)
    local normalized = autoHidden == true
    if self._matrixAutoHidden == normalized then
        return
    end

    self._matrixAutoHidden = normalized
    if self.matrixLinked == true and MS_MatrixManager and MS_MatrixManager.notifyIndicatorMatrixVisibilityChanged then
        MS_MatrixManager.notifyIndicatorMatrixVisibilityChanged(self)
    end
end

function MS_StatusIndicator:clearHiddenInteractionState()
    -- Hidden indicators should behave as if their UI element was not there.
    -- Clear stale hover, drag, capture and tooltip state before moving them away.
    self.isHovered = false
    self.moving = false
    self.matrixDragging = false
    self.dragRawX = nil
    self.dragRawY = nil
    self.dragGroupAnchorX = nil
    self.dragGroupAnchorY = nil
    self.dragGroupStartPositions = nil
    self.iconAnimActive = false
    self.barAnimActive = false
    self.iconSwayActive = false
    self.iconAnimScale = 1.0
    self.iconSwayOffset = 0
    self.barAnimTransition = 0

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

function MS_StatusIndicator:setRuntimeAutoHidden(autoHidden)
    local shouldHide = autoHidden == true

    if shouldHide then
        if self._runtimeAutoHidden ~= true then
            self._runtimeAutoHidden = true
            self._runtimeAutoHiddenSavedX = self:getX()
            self._runtimeAutoHiddenSavedY = self:getY()
        end

        self:clearHiddenInteractionState()

        -- Keep the element visible so render() can keep checking when Auto-Hide
        -- should end, but move it far outside the playable UI hit area.
        local offset = ((self.playerIndex or 0) * 100) + (self.matrixLinkOrder or 0)
        self:setX(RUNTIME_AUTO_HIDDEN_X - offset)
        self:setY(RUNTIME_AUTO_HIDDEN_Y - offset)
        self:setVisible(self.hidden ~= true)
        return
    end

    if self._runtimeAutoHidden == true then
        local savedX = self._runtimeAutoHiddenSavedX
        local savedY = self._runtimeAutoHiddenSavedY
        self._runtimeAutoHidden = false
        self._runtimeAutoHiddenSavedX = nil
        self._runtimeAutoHiddenSavedY = nil

        if self.hidden ~= true and savedX ~= nil and savedY ~= nil then
            self:setX(savedX)
            self:setY(savedY)
        end
    end

    self:setVisible(self.hidden ~= true)
end

function MS_StatusIndicator:updateTooltipUI(value)
    if self.moving or self.isHovered ~= true then
        self:hideTooltipUI()
        return
    end

    self._tooltipHoverFrameCounter = (self._tooltipHoverFrameCounter or 0) + 1

    local tooltipSettings = MS_IndicatorTooltipManager and MS_IndicatorTooltipManager.getSettings and MS_IndicatorTooltipManager.getSettings(self.playerIndex) or nil
    local settingsKey = _buildTooltipSettingsCacheKey(tooltipSettings)
    local valueKey = _buildTooltipValueCacheKey(value)
    local currentTime = getTimestampMs and getTimestampMs() or 0
    local tooltipRefreshInterval = 750
    if self:isCarryWeightIndicator() then
        tooltipRefreshInterval = 1500
    elseif self:isNoxiousExposureIndicator() then
        tooltipRefreshInterval = 2000
    end
    local shouldRefreshTimed = (currentTime - (self._tooltipCacheTime or 0)) >= tooltipRefreshInterval
    local shouldRefresh = shouldRefreshTimed
        or self._tooltipCacheInitialized ~= true
        or self._tooltipCacheSettingsKey ~= settingsKey
        or self._tooltipCacheValueKey ~= valueKey

    if shouldRefresh then
        self._tooltipCacheTitle = MS_IndicatorTooltipData and MS_IndicatorTooltipData.getTooltipTitle and MS_IndicatorTooltipData.getTooltipTitle(self, value, tooltipSettings) or ((not tooltipSettings or tooltipSettings.showName ~= false) and self:getIndicatorDisplayName() or nil)
        self._tooltipCacheDescription = self:getTooltipDescription(value)
        self._tooltipCacheDetailRows = self:getTooltipDetailRows(value, tooltipSettings)
        self._tooltipCacheSettingsKey = settingsKey
        self._tooltipCacheValueKey = valueKey
        self._tooltipCacheTime = currentTime
        self._tooltipCacheInitialized = true
    end

    local tooltipUI = self:ensureTooltipUI()
    local titleText = self._tooltipCacheTitle
    local descriptionText = self._tooltipCacheDescription
    local detailRows = self._tooltipCacheDetailRows

    if (not titleText or titleText == "") and (not descriptionText or descriptionText == "") and (not detailRows or #detailRows == 0) then
        self:hideTooltipUI()
        return
    end

    if tooltipUI.setOpacityPercent then
        local opacityPercent = tooltipSettings and tooltipSettings.opacity or 100
        tooltipUI:setOpacityPercent(opacityPercent)
    end

    if tooltipUI.title ~= titleText then
        tooltipUI:setTitle(titleText)
    end
    if tooltipUI.description ~= (descriptionText or "") then
        tooltipUI.description = descriptionText or ""
    end
    if tooltipUI.setAlignedDetailRows then
        tooltipUI:setAlignedDetailRows(detailRows)
    end

    if not tooltipUI:getIsVisible() then
        tooltipUI:addToUIManager()
        tooltipUI:setVisible(true)
    end
    self._tooltipVisible = true
end

-- ---------------------------------------------------------------------------------------- --
-- Render
-- ---------------------------------------------------------------------------------------- -

function MS_StatusIndicator:render()
    if self.hidden == true then
        self:clearHiddenInteractionState()
        self:setVisible(false)
        return
    end

    local mouseOver = self.isHovered == true and self.moving ~= true
    local snapshot = self:getRenderSnapshot(mouseOver)

    local autoHidden = self.autoHide and not snapshot.autoHideExceeded
    if autoHidden then
        self:setRuntimeAutoHidden(true)
        self:updateMatrixAutoHiddenState(true)
        return
    end

    self:setRuntimeAutoHidden(false)
    self:updateMatrixAutoHiddenState(false)

    local value = snapshot.value
    local visualValue = snapshot.visualValue

    if self.isBarStyle then
        self:renderBarStyle(value, visualValue, snapshot.resolvedColor, snapshot.thresholdMarkers, snapshot.thresholdExceeded)
    else
        self:renderCircularStyle(value, visualValue, snapshot.resolvedColor, snapshot.thresholdMarkers, snapshot.thresholdExceeded, mouseOver)
    end

    local textScale = self.textScale or 1.0

    if mouseOver then
        self:updateTooltipUI(value)
    elseif self._tooltipVisible == true then
        self:hideTooltipUI()
    end

    if (mouseOver or self.alwaysShowValue) and value >= 0 then
        local hoverText = snapshot.hoverText or self:getHoverText(value)
        local textX, textY

        local fontHeight = _getSmallFontHeight()
        local scaledFontHeight = fontHeight * textScale
        
        if self.isBarStyle then
            if self.isVertical then
                textX = math.ceil(self.width / 2 - (_measureSmallTextCached(self, hoverText) * textScale) / 2)
                local iconPos = self:getBarIconPosition()
                if self.showIcon and self.iconTexture and iconPos == "top" then
                    local iconSize = self.width * 1.4
                    textY = math.ceil(self.height + iconSize)
                else
                    textY = math.ceil(-scaledFontHeight)
                end
            else
                local textWidth = _measureSmallTextCached(self, hoverText) * textScale
                textX = math.ceil(self.width - textWidth - self.padding)
                textY = math.ceil((self.height - scaledFontHeight) / 2 )
            end
        else
            textX = math.ceil(self.width / 2 - (_measureSmallTextCached(self, hoverText) * textScale) / 2 )
            textY = math.ceil((self.height - scaledFontHeight) / 2)
        end
        
        -- Outline
        self:drawTextZoomed(hoverText, textX-1, textY, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX+1, textY, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX-1, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX+1, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX-1, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(hoverText, textX+1, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        -- Text
        self:drawTextZoomed(hoverText, textX, textY, textScale, 1, 1, 1, 1, UIFont.Small)
    end
end

function MS_StatusIndicator:getHoverText(value)
    local displayValue = tonumber(value) or 0

    if self:isDisplayDirectionInverted() then
        displayValue = 1 - displayValue
    end

    if displayValue < 0 then displayValue = 0 end
    if displayValue > 1 then displayValue = 1 end

    return tostring(round(displayValue * 100 ))
end

function MS_StatusIndicator:getIndicatorDisplayName()
    local indicatorType = self:getType()
    local inverted = self:isDisplayDirectionInverted() == true
    local cacheKey = tostring(indicatorType or "") .. "|" .. tostring(inverted)

    -- Indicator names are stable during gameplay, so avoid resolving translated
    -- text every frame while names are drawn on bars.
    if self._indicatorDisplayNameCacheKey == cacheKey and self._indicatorDisplayNameCacheValue then
        return self._indicatorDisplayNameCacheValue
    end

    local titleKey = MS_IndicatorSettingsPanel.IndicatorNames[indicatorType]
    local titleText

    if self:isCarryWeightIndicator() then
        titleText = getText("IGUI_invpanel_weight")
    else
        titleText = titleKey and getText(titleKey) or indicatorType
    end

    if inverted then
        titleText = tostring(titleText or indicatorType or "") .. "*"
    end

    self._indicatorDisplayNameCacheKey = cacheKey
    self._indicatorDisplayNameCacheValue = titleText
    return titleText
end


function MS_StatusIndicator:renderCircularStyle(value, visualValue, resolvedColor, thresholdMarkers, thresholdExceeded, mouseOver)
    self:drawTextureScaled(self.circularBgTexture, 0, 0, self.width, self.height, 0.6, 0.1, 0.1, 0.1)
    self:drawTextureScaled(self.circularLineTexture, 0, 0, self.width, self.height, 0.6, 0.2, 0.2, 0.2)
    self:drawTextureScaled(self.circularBgShader, 0, 0, self.width, self.height, 1, 1, 1, 1)
    local drawValue = visualValue or value
    local fullSegments = math.floor(drawValue * 12)

    local partialSegment = fullSegments + 1
    local partialAlpha = (drawValue * 12) - fullSegments

    local baseAlpha = self.opacity or 0.8
    resolvedColor = resolvedColor or self:getResolvedIndicatorColor(value, drawValue)
    local r = resolvedColor.r
    local g = resolvedColor.g
    local b = resolvedColor.b
    
    -- Draw segment Percentage
    for i=1, fullSegments do
        if self.segmentTextures[i] then
            self:drawTextureScaled(self.segmentTextures[i], 0, 0, self.width, self.height, baseAlpha, r, g, b)
        end
    end

    if partialSegment <= 12 and partialAlpha > 0 and self.segmentTextures[partialSegment] then
        self:drawTextureScaled(self.segmentTextures[partialSegment], 0, 0, self.width, self.height, baseAlpha * partialAlpha, r, g, b)
    end
    
    -- Draw Icon
    if self.showIcon and self.iconTexture then
        local iconSize = self.width* 0.95 * self.iconAnimScale 
        local x = math.floor((self.width - iconSize) / 2)
        local y = math.floor((self.height - iconSize) / 2)
        local iconAlpha = self.iconOpacity or 1.0

        if self.alwaysShowValue or mouseOver == true then
            iconAlpha = iconAlpha * 0.5
        end
        
        self:drawTextureScaled(self.iconTexture, x, y, iconSize, iconSize, iconAlpha, 1, 1, 1)
    end

    thresholdMarkers = thresholdMarkers or self:getThresholdMarkers(value)
    if thresholdMarkers and #thresholdMarkers > 0 then
        self:drawThresholdsCircular(thresholdMarkers)
    end

    if self.iconAnimActive then
        self:drawAlarmBorder()
    end

    self:checkTriggerAnimation(value, thresholdExceeded)
end

function MS_StatusIndicator:renderBarStyle(value, visualValue, resolvedColor, thresholdMarkers, thresholdExceeded)
    -- Resolve the final fill color after band colors, gradients, and inverted-full overrides.
    resolvedColor = resolvedColor or self:getResolvedIndicatorColor(value, visualValue)
    local r = resolvedColor.r
    local g = resolvedColor.g
    local b = resolvedColor.b

    local opacity = self.opacity or 1.0

    if self.isVertical then
        self:renderVerticalBar(visualValue or value, opacity, r, g, b, value, thresholdMarkers)
    else
        self:renderHorizontalBar(visualValue or value, opacity, r, g, b, value, thresholdMarkers)
    end

    if self.barAnimActive then
        self:drawAlarmBorder()
    end

    self:checkTriggerAnimation(value, thresholdExceeded)
end

function MS_StatusIndicator:renderVerticalBar(value, opacity, r, g, b, baseValue, thresholdMarkers)
    local bgWidth = self.width
    local bgAlpha = opacity * 0.8
    
    MS_UIHelper.drawVertical(self, 0, 0, bgWidth, self.height,self.barBackgroundTop,self.barBackgroundMiddleVertical,self.barBackgroundBottom,bgAlpha, 0.6, 0.6, 0.6)

    local fillHeight = (self.height * value)

    if fillHeight > 0 then
        local fillWidth = bgWidth
        local fillY = self.height - fillHeight

        self:setStencilRect(0, fillY, fillWidth, fillHeight)
        MS_UIHelper.drawVertical(self, 0, 0, fillWidth, self.height,self.barFillTop,self.barFillMiddleVertical,self.barFillBottom,opacity, r, g, b)
        self:clearStencilRect()
    end
    
    -- Top/Bottom Icon
    if self.showIcon and self.iconTexture then
        local iconSize = self.width * 1.4
        local iconX = (self.width - iconSize) / 2

        if self.iconSwayActive then
            iconX = iconX + self.iconSwayOffset
        end

        local iconY
        if self:getBarIconPosition() == "top" then
            iconY = -iconSize
        else
            iconY = self.height
        end
        self:drawTextureScaled(self.iconTexture, iconX, iconY, iconSize, iconSize, self.iconOpacity or 1.0, 1, 1, 1)
    end
end

function MS_StatusIndicator:renderHorizontalBar(value, opacity, r, g, b, baseValue, thresholdMarkers)
    local bgAlpha = opacity * 0.8
    
    MS_UIHelper.drawHorizontal(self, 0, 0, self.width, self.height,self.barBackgroundLeft,self.barBackgroundMiddle,self.barBackgroundRight,bgAlpha, 0.6, 0.6, 0.6)
    
    local fillWidth = math.floor(self.width * value)
    
    if fillWidth > 0 then
        self:setStencilRect(0, 0, fillWidth, self.height)
        MS_UIHelper.drawHorizontal(self, 0, 0, self.width, self.height,self.barFillLeft,self.barFillMiddle,self.barFillRight,opacity, r, g, b)
        self:clearStencilRect()
    end
    
    -- Left/Right Icon
    if self.showIcon and self.iconTexture then
        local iconSize = self.height * 1.4
        local iconX
        if self:getBarIconPosition() == "right" then
            iconX = self.width + self.padding / 2
        else
            iconX = -iconSize - self.padding / 2
        end

        if self.iconSwayActive then
            iconX = iconX + self.iconSwayOffset
        end

        local iconY = (self.height - iconSize) / 2
        self:drawTextureScaled(self.iconTexture, iconX, iconY, iconSize, iconSize, self.iconOpacity or 1.0, 1, 1, 1)
    end
    
    -- Show Name
    if self.showName ~= false then
        local titleText = self:getIndicatorDisplayName()
        local textScale = self.textScale or 1.0

        local scaledFontHeight = FONT_HGT_SMALL * textScale
        local textX = self.padding
        local textY = math.floor((self.height - scaledFontHeight) / 2 )
        
        -- Outline
        self:drawTextZoomed(titleText, textX-1, textY, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX+1, textY, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX-1, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX+1, textY-1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX-1, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        self:drawTextZoomed(titleText, textX+1, textY+1, textScale, 0, 0, 0, 1, UIFont.Small)
        -- Text
        self:drawTextZoomed(titleText, textX, textY, textScale, 1, 1, 1, 1, UIFont.Small)
    end
end

-- ---------------------------------------------------------------------------------------- --
-- Gradient Color
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:getGradientColor(value)
    local indicatorType = self:getType()
    
    if not MS_PlayerStatus.isHigherBetter(indicatorType) then
        value = 1 - value
    end

    local h = (120 * value) / 360
    local s = 0.6
    local v = 0.9
    
    local i = math.floor(h * 6)
    local f = h * 6 - i
    local p = v * (1 - s)
    local q = v * (1 - f * s)
    local t = v * (1 - (1 - f) * s)
    
    local r, g, b
    if i % 6 == 0 then r, g, b = v, t, p
    elseif i % 6 == 1 then r, g, b = q, v, p 
    elseif i % 6 == 2 then r, g, b = p, v, t
    elseif i % 6 == 3 then r, g, b = p, q, v
    elseif i % 6 == 4 then r, g, b = t, p, v
    elseif i % 6 == 5 then r, g, b = v, p, q
    end
    
    return {r=r, g=g, b=b}
end

-- ---------------------------------------------------------------------------------------- --
-- Get Config
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:getValue()
    return 1.0
end

function MS_StatusIndicator:getType()
    return self.__type or "MS_StatusIndicator"
end

-- ---------------------------------------------------------------------------------------- --
-- Style Manager
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:switchStyle(useBarStyle)
    if self.isBarStyle == useBarStyle then return end
    
    self.isBarStyle = useBarStyle
    
    MSConfig.updateIndicatorConfig(
        self.playerIndex,
        self:getType(),
        "style",
        useBarStyle and "bar" or "circular"
    )
    
    local config = MSConfig.getIndicatorConfig(self.playerIndex, self:getType())
    
    if useBarStyle then
        local barSize = config.barSize or {width = 160, height = 24}
        self:setWidth(barSize.width)
        self:setHeight(barSize.height)
    else
        local circularSize = config.circularSize or 48
        self:setWidth(circularSize)
        self:setHeight(circularSize)
    end
    
    self:setVisible(true)
end

-- ---------------------------------------------------------------------------------------- --
-- Mouse Function
-- ---------------------------------------------------------------------------------------- --
local function _isMatrixInteractionSuppressed(indicator)
    if not indicator or indicator.matrixLinked ~= true then
        return false
    end
    if not MS_MatrixManager or not MS_MatrixManager.isCompactHiddenIndicatorsEnabled then
        return false
    end
    if not MS_MatrixManager.isCompactHiddenIndicatorsEnabled(indicator.playerIndex) then
        return false
    end

    -- Compact matrix indicators hidden manually or by Auto-Hide must not react
    -- to hover/clicks while they are not reserving a visible matrix slot.
    return indicator.hidden == true or indicator._matrixAutoHidden == true
end

local function _isIndicatorInteractionSuppressed(indicator)
    if not indicator then
        return true
    end

    -- Hidden and auto-hidden indicators should not consume mouse input from
    -- whatever UI/game element is visually underneath their old area.
    if indicator.hidden == true or indicator._runtimeAutoHidden == true or indicator._matrixAutoHidden == true then
        return true
    end

    return _isMatrixInteractionSuppressed(indicator)
end
local function _setIndicatorPosition(indicator, x, y)
    indicator:setX(math.floor(x))
    indicator:setY(math.floor(y))
end

local function _applyGridToPoint(playerIndex, x, y, useGrid)
    if useGrid then
        return MS_GridManager.snapPosition(playerIndex, x, y)
    end

    return math.floor(x), math.floor(y)
end

local function _collectUnlockedDragIndicators(playerIndex)
    local indicators = {}
    local seen = {}

    local function addIndicator(indicator)
        if indicator and not seen[indicator] and not indicator.locked and indicator.hidden ~= true and indicator._runtimeAutoHidden ~= true and indicator._matrixAutoHidden ~= true then
            seen[indicator] = true
            indicators[#indicators + 1] = indicator
        end
    end

    local byName = StatusWidget and StatusWidget.indicators and StatusWidget.indicators[playerIndex]
    if byName then
        for _, indicator in pairs(byName) do
            addIndicator(indicator)
        end
    end

    local byType = StatusWidget and StatusWidget.indicatorsByType and StatusWidget.indicatorsByType[playerIndex]
    if byType then
        for _, indicator in pairs(byType) do
            addIndicator(indicator)
        end
    end

    return indicators
end

function MS_StatusIndicator:applyDragDelta(dx, dy)
    if not self.moving then
        return
    end

    local globalConfig = MSConfig.getGlobalConfig(self.playerIndex)
    local useGrid = MS_GridManager.isEnabled(self.playerIndex)

    self.dragRawX = (self.dragRawX or self:getX()) + dx
    self.dragRawY = (self.dragRawY or self:getY()) + dy

    if self.matrixDragging then
        local snappedX, snappedY = _applyGridToPoint(self.playerIndex, self.dragRawX, self.dragRawY, useGrid)
        MS_MatrixManager.moveMatrixToPosition(self.playerIndex, snappedX, snappedY)
        return
    end

    if globalConfig and globalConfig.multiDragMode and self.dragGroupStartPositions then
        local deltaX = self.dragRawX - (self.dragGroupAnchorX or self:getX())
        local deltaY = self.dragRawY - (self.dragGroupAnchorY or self:getY())

        for indicator, startPosition in pairs(self.dragGroupStartPositions) do
            local targetX = startPosition.x + deltaX
            local targetY = startPosition.y + deltaY
            local snappedX, snappedY = _applyGridToPoint(indicator.playerIndex or self.playerIndex, targetX, targetY, useGrid)
            _setIndicatorPosition(indicator, snappedX, snappedY)
        end
        return
    end

    local snappedX, snappedY = _applyGridToPoint(self.playerIndex, self.dragRawX, self.dragRawY, useGrid)
    _setIndicatorPosition(self, snappedX, snappedY)
end

function MS_StatusIndicator:onMouseMove(dx, dy)
    if _isIndicatorInteractionSuppressed(self) then
        self.isHovered = false
        self:hideTooltipUI()
        return false
    end

    self.isHovered = true

    if self.moving then
        self:applyDragDelta(dx, dy)
    end

    return ISUIElement.onMouseMove(self, 0, 0)
end

function MS_StatusIndicator:onMouseMoveOutside(dx, dy)
    self.isHovered = false

    if self.moving then
        self:applyDragDelta(dx, dy)
    else
        self:hideTooltipUI()
    end

    return true
end

function MS_StatusIndicator:onRightMouseDown(x, y)
    if _isIndicatorInteractionSuppressed(self) then
        self.isHovered = false
        self:hideTooltipUI()
        return false
    end

    self:hideTooltipUI()
    self:showSettingsPanel()
    return ISUIElement.onRightMouseDown(self, x, y)
end

function MS_StatusIndicator:onMouseDown(x, y)
    if _isIndicatorInteractionSuppressed(self) then
        self.isHovered = false
        self:hideTooltipUI()
        return false
    end
    if self.locked then return false end

    self.isHovered = false
    self:hideTooltipUI()

    local globalConfig = MSConfig.getGlobalConfig(self.playerIndex)

    self.matrixDragging = self.matrixLinked == true
    self.dragStartX = x
    self.dragStartY = y
    self.moving = true
    self.dragGroupStartPositions = nil

    if self.matrixDragging then
        local matrixConfig = MS_MatrixManager.getMatrixConfig(self.playerIndex)
        self.dragRawX = matrixConfig.x or self:getX()
        self.dragRawY = matrixConfig.y or self:getY()
    else
        self.dragRawX = self:getX()
        self.dragRawY = self:getY()

        if globalConfig and globalConfig.multiDragMode then
            self.dragGroupAnchorX = self.dragRawX
            self.dragGroupAnchorY = self.dragRawY
            self.dragGroupStartPositions = {
                [self] = {
                    x = self:getX(),
                    y = self:getY(),
                }
            }

            local indicators = _collectUnlockedDragIndicators(self.playerIndex)
            for _, indicator in ipairs(indicators) do
                self.dragGroupStartPositions[indicator] = {
                    x = indicator:getX(),
                    y = indicator:getY(),
                }
            end
        end
    end

    self:setCapture(true)
    return true
end

function MS_StatusIndicator:onMouseUp(x, y)
    if self.moving then
        self.moving = false
        self:setCapture(false)

        if self.matrixDragging then
            self.matrixDragging = false
            MS_MatrixManager.persistMatrixPosition(self.playerIndex)
            self.dragRawX = nil
            self.dragRawY = nil
            return true
        end

        local globalConfig = MSConfig.getGlobalConfig(self.playerIndex)

        if globalConfig and globalConfig.multiDragMode and self.dragGroupStartPositions then
            for indicator, _ in pairs(self.dragGroupStartPositions) do
                if indicator and not indicator.locked then
                    MSConfig.updateIndicatorConfig(
                        indicator.playerIndex,
                        indicator:getType(),
                        "position",
                        {x = indicator:getX(), y = indicator:getY()}
                    )
                end
            end
        else
            MSConfig.updateIndicatorConfig(
                self.playerIndex,
                self:getType(),
                "position",
                {x = self:getX(), y = self:getY()}
            )
        end
    end

    self.dragRawX = nil
    self.dragRawY = nil
    self.dragGroupAnchorX = nil
    self.dragGroupAnchorY = nil
    self.dragGroupStartPositions = nil
    return true
end

-- ---------------------------------------------------------------------------------------- --
-- show SettingsPanel
-- ---------------------------------------------------------------------------------------- --
function MS_StatusIndicator:showSettingsPanel()

    if MS_IndicatorSettingsPanel.CurrentPanel then
        MS_IndicatorSettingsPanel.CurrentPanel:close()
    end

    local screenW = getCore():getScreenWidth()
    local screenH = getCore():getScreenHeight()
    
    local panelX = self:getX() + self:getWidth() + 10
    local panelY = self:getY()

    local panel = MS_IndicatorSettingsPanel:new(panelX, panelY, nil, nil, self)
    panel:initialise()
    panel:addToUIManager()
    panel:setVisible(true)
    MS_IndicatorSettingsPanel.CurrentPanel = panel
    
    if panelX + panel:getWidth() > screenW then
        panelX = self:getX() - panel:getWidth() - 10
    end
    
    if panelY + panel:getHeight() > screenH then
        panelY = screenH - panel:getHeight() - 10
    end
    
    if panelX < 0 then
        panelX = self:getX() + self:getWidth() + 10
    end
    
    if panelY < 0 then
        panelY = 10
    end
    
    panel:setX(panelX)
    panel:setY(panelY)
end