-- Castbars/MSUF_PlayerCastbarRuntime.lua
--  extraction: Player castbar runtime (latency, color, interrupt, cast, OnEvent).
-- Loaded AFTER Empower + ChannelTicks (needs _G exports from those files).

local MSUF_FastCall = _G.MSUF_FastCall or function(...) return pcall(...) end
local _EnsureDBLazy = _G.MSUF_EnsureDBLazy or function()
    if not MSUF_DB and type(EnsureDB) == "function" then EnsureDB() end
end

-- Cross-file refs (call-time resolved, set by earlier TOC files)
local MSUF_PlayerCastbar_EmpowerStart       = function(s, id) local fn = _G.MSUF_PlayerCastbar_EmpowerStart; if fn then fn(s, id) end end
local MSUF_PlayerCastbar_ClearEmpower       = function(s, h) local fn = _G.MSUF_PlayerCastbar_ClearEmpower; if fn then fn(s, h) end end
local MSUF_PlayerChannelHasteMarkers_Update = function(s, f) local fn = _G.MSUF_PlayerChannelHasteMarkers_Update; if fn then fn(s, f) end end
local MSUF_PlayerChannelHasteMarkers_Hide   = function(s) local fn = _G.MSUF_PlayerChannelHasteMarkers_Hide; if fn then fn(s) end end

local function MSUF_PlayerCastbar_ApplyLatencyZone(frame, pct, isChanneled)
    if not frame or not frame.latencyBar or not frame.statusBar then
        return
    end

    local bw = frame.statusBar:GetWidth() or 0
    local ww = bw * (pct or 0)
    local reverse = _G.MSUF_GetReverseFillSafe(frame, isChanneled and true or false)
    local anchorOnLeft = reverse and true or false  -- finish edge (value always increases)

    frame.latencyBar:ClearAllPoints()
    if anchorOnLeft then
        frame.latencyBar:SetPoint("TOPLEFT", frame.statusBar, "TOPLEFT", 0, 0)
        frame.latencyBar:SetPoint("BOTTOMLEFT", frame.statusBar, "BOTTOMLEFT", 0, 0)
    else
        frame.latencyBar:SetPoint("TOPRIGHT", frame.statusBar, "TOPRIGHT", 0, 0)
        frame.latencyBar:SetPoint("BOTTOMRIGHT", frame.statusBar, "BOTTOMRIGHT", 0, 0)
    end

    frame.latencyBar:SetWidth(ww)
    if ww and ww > 0 then
        frame.latencyBar:Show()
    else
        frame.latencyBar:Hide()
    end
end

local function MSUF_PlayerCastbar_ApplyPendingLatencyZone(frame, generation)
    if not frame then
        return
    end

    local pending = frame._msufLatencyPending
    if not pending then
        return
    end

    if generation and pending.generation ~= generation then
        return
    end

    MSUF_PlayerCastbar_ApplyLatencyZone(frame, pending.pct or 0, pending.isChanneled and true or false)
end

local MSUF_PlayerCastbar_LatencyZoneTimerQueue = {}

local function MSUF_PlayerCastbar_RunQueuedLatencyZoneCallbacks()
    local queue = MSUF_PlayerCastbar_LatencyZoneTimerQueue
    local count = #queue
    if count <= 0 then
        return
    end

    local i = 1
    while i <= count do
        local frame = queue[i]
        local generation = queue[i + 1]
        MSUF_PlayerCastbar_ApplyPendingLatencyZone(frame, generation)
        queue[i] = nil
        queue[i + 1] = nil
        i = i + 2
    end
end

local function MSUF_PlayerCastbar_UpdateLatencyZone(self, isChanneled, durSec)
    if not self or not self.latencyBar or not self.statusBar then
        return
    end

    -- Honor Options -> Castbars -> Style -> Show latency indicator (default ON)
    _EnsureDBLazy()  -- P3 Fix #14: lazy guard
    local g = (MSUF_DB and MSUF_DB.general) or {}
    if g.castbarShowLatency == false then
        self.latencyBar:Hide()
        return
    end

    -- For Edit Mode previews / dummy casts: show the indicator only while MSUF Edit Mode is active.
    if (self.MSUF_testMode or self._msufIsPreview) and not MSUF_UnitEditModeActive then
        self.latencyBar:Hide()
        return
    end

    if not durSec or type(durSec) ~= "number" or durSec <= 0 then
        self.latencyBar:Hide()
        return
    end

    local _, _, homeMS, worldMS = GetNetStats()
    local latencyMS = math.max(homeMS or 0, worldMS or 0)
    local queueMS = tonumber(GetCVar("SpellQueueWindow") or "0") or 0
    local tolMS = math.max(latencyMS, queueMS)

    local durationMS = durSec * 1000
    local pct = 0
    if durationMS > 0 then
        pct = tolMS / durationMS
    end
    if pct > 1 then pct = 1 end
    if pct < 0 then pct = 0 end

    self.MSUF_latencyLastPct = pct
    self.MSUF_latencyLastIsChanneled = isChanneled and true or false
    self.MSUF_latencyLastDurSec = durSec

    local barW = self.statusBar:GetWidth() or 0

    local pending = self._msufLatencyPending
    if not pending then
        pending = {}
        self._msufLatencyPending = pending
    end
    pending.pct = pct
    pending.isChanneled = isChanneled and true or false
    pending.generation = (pending.generation or 0) + 1
    local generation = pending.generation

    if (not barW or barW <= 1) and C_Timer and C_Timer.After then
        local queue = MSUF_PlayerCastbar_LatencyZoneTimerQueue
        queue[#queue + 1] = self
        queue[#queue + 1] = generation
        C_Timer.After(0, MSUF_PlayerCastbar_RunQueuedLatencyZoneCallbacks)
        return
    end

    MSUF_PlayerCastbar_ApplyPendingLatencyZone(self, generation)
end

local function MSUF_PlayerCastbar_UpdateColorForInterruptible(self)
    if not self or not self.statusBar then
        return
    end

    _EnsureDBLazy()  -- P3 Fix #14: lazy guard
    local g = MSUF_DB and MSUF_DB.general or {}

    -- Optional player castbar color override (normal casts/channels).
    -- This overrides interruptible + non-interruptible colors for the player castbar.
    -- Interrupt feedback ("Interrupted") still uses the interrupt feedback color.
    if g.playerCastbarOverrideEnabled then
        -- If interrupt feedback is active, keep the interrupt feedback color.
        if not (self.interruptFeedbackEndTime and GetTime() < self.interruptFeedbackEndTime) then
            local mode = g.playerCastbarOverrideMode
            local orr, org, orb
            if mode == "CUSTOM" then
                orr = tonumber(g.playerCastbarOverrideR)
                org = tonumber(g.playerCastbarOverrideG)
                orb = tonumber(g.playerCastbarOverrideB)
            else
                local _, classToken = UnitClass("player")
                if classToken then
                    if type(MSUF_GetClassBarColor) == "function" then
                        orr, org, orb = MSUF_GetClassBarColor(classToken)
                    end
                    if (not orr) and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken] then
                        local c = RAID_CLASS_COLORS[classToken]
                        orr, org, orb = c.r, c.g, c.b
                    end
                end
            end

            if orr and org and orb then
                if type(_G.MSUF_SetStatusBarColorIfChanged) == "function" then
                    _G.MSUF_SetStatusBarColorIfChanged(self.statusBar, orr, org, orb, 1)
                else
                    self.statusBar:SetStatusBarColor(orr, org, orb, 1)
                end
                return
            end
        end
    end

    local nonInterruptibleKey = g.castbarNonInterruptibleColor or "red"

    local isNonInterruptible = false

    local unit = self.unit or "player"
    local nameplate = C_NamePlate
        and C_NamePlate.GetNamePlateForUnit
        and C_NamePlate.GetNamePlateForUnit(unit, issecure())

    if nameplate then
        local bar = (nameplate.UnitFrame and nameplate.UnitFrame.castBar)
            or nameplate.castBar
            or nameplate.CastBar

        local barType = bar and bar.barType
        if barType == "uninterruptable"
            or barType == "uninterruptible"
            or barType == "uninterruptibleSpell"
            or barType == "shield"
        then
            isNonInterruptible = true
        end
    end

    if self.isNotInterruptible then
        isNonInterruptible = true
    end

    local r, gCol, b, a

    if isNonInterruptible then
        if MSUF_GetNonInterruptibleCastColor then
            r, gCol, b = MSUF_GetNonInterruptibleCastColor()
            a = 1
        end

        if not r or not gCol or not b then
            local nonKey = g.castbarNonInterruptibleColor or "red"
            if MSUF_GetColorFromKey then
                local color = MSUF_GetColorFromKey(nonKey)
                if color then
                    r, gCol, b, a = color:GetRGBA()
                end
            end
        end

        if not r or not gCol or not b then
            r, gCol, b, a = 0.4, 0.01, 0.01, 1
        end
    else
        if MSUF_GetInterruptibleCastColor then
            r, gCol, b = MSUF_GetInterruptibleCastColor()
            a = 1
        end

        if not r or not gCol or not b then
            local interruptibleKey = g.castbarInterruptibleColor or "turquoise"
            if MSUF_GetColorFromKey then
                local color = MSUF_GetColorFromKey(interruptibleKey)
                if color then
                    r, gCol, b, a = color:GetRGBA()
                end
            end
        end

        if not r or not gCol or not b then
            r, gCol, b, a = 0, 1, 0.9, 1
        end
    end

    if type(_G.MSUF_SetStatusBarColorIfChanged) == "function" then
        _G.MSUF_SetStatusBarColorIfChanged(self.statusBar, r, gCol, b, a or 1)
    else
        self.statusBar:SetStatusBarColor(r, gCol, b, a or 1)
    end
end
local function MSUF_GetInterruptFeedbackColor()
    _EnsureDBLazy()  -- P3 Fix #14: lazy guard
    local g = MSUF_DB and MSUF_DB.general or {}

    local r    = tonumber(g.castbarInterruptR)
    local gCol = tonumber(g.castbarInterruptG)
    local b    = tonumber(g.castbarInterruptB)

    if r and gCol and b then
        return r, gCol, b, 1
    end

    local key = g.castbarInterruptColor or "red"

    if MSUF_GetColorFromKey then
        local color = MSUF_GetColorFromKey(key)
        if color then
            return color:GetRGBA()
        end
    end

    return 0.8, 0.1, 0.1, 1
end
local MSUF_PLAYER_INTERRUPT_FEEDBACK_DURATION = (_G.MSUF_INTERRUPT_FEEDBACK_DURATION or 0.5)

local function MSUF_PlayerCastbar_HideIfNoLongerCasting(timer)
    local self = timer and timer.msuCastbarFrame
    if not self or not self.unit then
        return
    end

    if self.MSUF_testMode then
        return
    end

    local unit = self.unit or "player"
    if unit == "player" and type(UnitHasVehicleUI) == "function" and UnitHasVehicleUI("player") then
        if type(UnitExists) == "function" and UnitExists("vehicle") then
            if (type(UnitCastingInfo) == "function" and UnitCastingInfo("vehicle"))
            or (type(UnitChannelInfo) == "function" and UnitChannelInfo("vehicle")) then
                unit = "vehicle"
            end
        end
    end
    local castName = UnitCastingInfo(unit)
    local chanName = UnitChannelInfo(unit)

    if castName or chanName then
        if MSUF_PlayerCastbar_Cast then
            MSUF_PlayerCastbar_Cast(self)
        end
        return
    end

    self:SetScript("OnUpdate", nil)
    if self.timeText then
        MSUF_SetTextIfChanged(self.timeText, "")
    end
    if MSUF_UnregisterCastbar then MSUF_UnregisterCastbar(self) end
    self._msufActiveCastGUID = nil
    self._msufActiveSpellID = nil
    self._msufActiveCastBarID = nil
    self._msufActiveCastUnit = nil
    self:Hide()
end

local function MSUF_PlayerCastbar_ShowInterruptFeedback(self, label)
    if not self or not self.statusBar then
        return
    end

    _EnsureDBLazy()  -- P3 Fix #14: lazy guard
    local p = (MSUF_DB and MSUF_DB.player) or {}
    if p.showInterrupt == false then
        -- Option disabled: no red 'Interrupted' feedback, just hide immediately.
        self:SetScript("OnUpdate", nil)
        self.interruptFeedbackEndTime = nil
        if self.timeText then MSUF_SetTextIfChanged(self.timeText, "") end
        if self.statusBar and self.statusBar.SetValue then MSUF_FastCall(self.statusBar.SetValue, self.statusBar, 0) end
        self:Hide()
        return
    end

    if self.hideTimer then
        self.hideTimer:Cancel()
        self.hideTimer = nil
    end

    self:SetScript("OnUpdate", nil)

    if MSUF_UnregisterCastbar then MSUF_UnregisterCastbar(self) end
    self.MSUF_durationObj = nil
    self.MSUF_channelDirect = nil
    self.MSUF_timerDriven = nil
    self.MSUF_timerRangeSet = nil
    if _G.MSUF_ClearCastbarTimerDuration and self.statusBar then
        _G.MSUF_ClearCastbarTimerDuration(self.statusBar)
    end

    self._msufActiveCastUnit = nil
    self._msufActiveCastGUID = nil
    self._msufActiveSpellID = nil
    self._msufActiveCastBarID = nil
    self._msufChanNilSince = nil
    self.interruptFeedbackEndTime = GetTime() + MSUF_PLAYER_INTERRUPT_FEEDBACK_DURATION

    -- Phase 2A: Use shared interrupt bar visuals (replaces ~25 lines of inline setup).
    local rf = _G.MSUF_GetReverseFillSafe and _G.MSUF_GetReverseFillSafe(self, false) or false
    _G.MSUF_ApplyInterruptBarVisuals(self, {
        -- Player interrupt feedback should fully fill the bar (matches target/focus/boss).
        barValue = 1,
        colorR = 0.8, colorG = 0.1, colorB = 0.1,
        reverseFill = rf,
        label = label or INTERRUPTED,
    })

    local grace = MSUF_PLAYER_INTERRUPT_FEEDBACK_DURATION
    if type(grace) ~= 'number' then grace = 0.5 end
    if grace < 0 then grace = 0 end

    self.hideTimer = C_Timer.NewTimer(grace, MSUF_PlayerCastbar_HideIfNoLongerCasting)
    self.hideTimer.msuCastbarFrame = self
end

local function MSUF_PlayerCastbar_GetEffectiveUnit(self)
    local u = (self and self.unit) or "player"
    if u == "player" and type(UnitHasVehicleUI) == "function" and UnitHasVehicleUI("player") then
        if type(UnitExists) == "function" and UnitExists("vehicle") then
            -- Prefer vehicle only if it actually has an active cast/channel.
            if (type(UnitCastingInfo) == "function" and UnitCastingInfo("vehicle"))
            or (type(UnitChannelInfo) == "function" and UnitChannelInfo("vehicle")) then
                return "vehicle"
            end
        end
    end
    return u
end

local function MSUF_PlayerCastbar_ClearActiveIdentifiers(self)
    if not self then return end
    self._msufActiveCastGUID = nil
    self._msufActiveSpellID = nil
    self._msufActiveCastBarID = nil
end

local function MSUF_PlayerCastbar_SetActiveIdentifiers(self, castGUID, spellID, castBarID)
    if not self then return end
    self._msufActiveCastGUID = castGUID
    self._msufActiveSpellID = spellID
    self._msufActiveCastBarID = castBarID
end

local function MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken)
    if not self then return false end
    if not unitToken then return true end
    if not self._msufActiveCastUnit then return true end
    return unitToken == self._msufActiveCastUnit
end

local function MSUF_PlayerCastbar_EventMatchesCastBarID(self, castBarID)
    if not self then return false end
    if not castBarID then return true end
    if not self._msufActiveCastBarID then return true end
    return castBarID == self._msufActiveCastBarID
end

local function MSUF_PlayerCastbar_ShouldIgnoreFail(self, castGUID, spellID, castBarID)
    if not self then return false end
    if castBarID and self._msufActiveCastBarID and castBarID ~= self._msufActiveCastBarID then
        return true
    end
    if castGUID and self._msufActiveCastGUID and castGUID ~= self._msufActiveCastGUID then
        return true
    end
    if spellID and self._msufActiveSpellID and spellID ~= self._msufActiveSpellID then
        return true
    end
    return false
end

local function MSUF_PlayerCastbar_ResetPlainTiming(self)
    if not self then return end
    self.endTime = nil
    self._msufPlainEndTime = nil
    self._msufPlainTotal = nil
    self._msufRemaining = nil
    self._msufLastTimeDecimal = nil
    self._msufZeroCount = nil
    self._msufLastDurationObj = nil
    self._msufTimerAssumeCountdown = nil
end

local function MSUF_PlayerCastbar_SetPlainTiming(self, startTimeMS, endTimeMS)
    if not self then return end
    MSUF_PlayerCastbar_ResetPlainTiming(self)

    local now = GetTime()
    if type(endTimeMS) == "number" then
        local endSec = endTimeMS / 1000
        local r = endSec - now
        self.endTime = endSec
        if type(r) == "number" and r > 0 then
            self._msufPlainEndTime = endSec
            self._msufRemaining = r
        end
    end
    if type(startTimeMS) == "number" and type(endTimeMS) == "number" then
        local tot = (endTimeMS - startTimeMS) / 1000
        if type(tot) == "number" and tot > 0 then
            self._msufPlainTotal = tot
        end
    end
end

local function MSUF_PlayerCastbar_ApplyManualTimerFallback(self, totalSec, remSec, reverseFill)
    if not (self and self.statusBar and self.statusBar.SetMinMaxValues and self.statusBar.SetValue) then return end

    local total = (type(totalSec) == "number" and totalSec or 0)
    local rem = (type(remSec) == "number" and remSec or total)
    if total < 0 then total = 0 end
    if rem < 0 then rem = 0 end
    if rem > total then rem = total end

    local span = total
    if span <= 0 then span = 0.001 end

    self.statusBar:SetMinMaxValues(0, span)
    if reverseFill then
        self.statusBar:SetValue(rem)
    else
        self.statusBar:SetValue(span - rem)
    end
end




-- Quick Win #1: Lookup tables hoisted to file-level upvalues (previously allocated per-call inside the function).
local _MSUF_UNHALTED_CAST_START = {
    UNIT_SPELLCAST_START = true,
    UNIT_SPELLCAST_INTERRUPTIBLE = true,
    UNIT_SPELLCAST_NOT_INTERRUPTIBLE = true,
    UNIT_SPELLCAST_SENT = true,
}
local _MSUF_UNHALTED_CAST_STOP = {
    UNIT_SPELLCAST_STOP = true,
    UNIT_SPELLCAST_CHANNEL_STOP = true,
}
local _MSUF_UNHALTED_CHANNEL_START = {
    UNIT_SPELLCAST_CHANNEL_START = true,
}

-- Unhalted-style non-empower player castbar (cast/channel): self-driven via OnUpdate and hard-stops when the cast/channel is gone.
local function MSUF_PlayerCastbar_UnhaltedUpdate(self, event)
    if not self or not self.unit or not self.statusBar then return end
    if self.isEmpower then return end

    local CAST_START = _MSUF_UNHALTED_CAST_START
    local CAST_STOP = _MSUF_UNHALTED_CAST_STOP
    local CHANNEL_START = _MSUF_UNHALTED_CHANNEL_START

    local unit = MSUF_PlayerCastbar_GetEffectiveUnit(self)

    if CAST_START[event] then
        local castName, castText, castTex, startTimeMS, endTimeMS, _, castGUID, notInterruptible, spellID, castBarID = UnitCastingInfo(unit)
        if not castName then
            if UnitChannelInfo(unit) then
                return MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_CHANNEL_START")
            end
            return MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_STOP")
        end

        local castDuration = nil
        if type(UnitCastingDuration) == "function" then
            local okDur, d = MSUF_FastCall(UnitCastingDuration, unit)
            if okDur then castDuration = d end
        end

        self.interruptFeedbackEndTime = nil
        self._msufActiveCastUnit = unit
        self._msufChanNilSince = nil
        self._msufCastNilSince = nil
        self._msufHardStopNilSince = nil
        MSUF_PlayerCastbar_SetActiveIdentifiers(self, castGUID, spellID, castBarID)

        self.MSUF_castDuration = castDuration
        self.MSUF_channelDuration = nil
        self.MSUF_channelTotal = nil

        local __msuf_rf = _G.MSUF_GetReverseFillSafe(self, false)
        local timerDriven = false
        if castDuration then
            timerDriven = _G.MSUF_ApplyTimerAndFill(self.statusBar, castDuration, __msuf_rf) and true or false
        end

        self.MSUF_isChanneled = false
        self._msufStripeReverseFill = (__msuf_rf and true or false)
        MSUF_PlayerChannelHasteMarkers_Hide(self)

        local apiNI = false
        if notInterruptible then apiNI = true end
        self.isNotInterruptible = apiNI
        if self.icon then self.icon:SetTexture(castTex or nil) end
        if self.castText then MSUF_SetTextIfChanged(self.castText, castName or "") end

        MSUF_PlayerCastbar_SetPlainTiming(self, startTimeMS, endTimeMS)
        if (not timerDriven) and self._msufPlainTotal and self._msufRemaining then
            MSUF_PlayerCastbar_ApplyManualTimerFallback(self, self._msufPlainTotal, self._msufRemaining, __msuf_rf)
        end

        MSUF_PlayerCastbar_UpdateColorForInterruptible(self)

        do
            local durSec = nil
            if castDuration and castDuration.GetTotalDuration then
                local okT, total = MSUF_FastCall(castDuration.GetTotalDuration, castDuration)
                if okT then durSec = total end
            end
            if durSec == nil and castDuration and castDuration.GetRemainingDuration then
                local okR, rem = MSUF_FastCall(castDuration.GetRemainingDuration, castDuration)
                if okR then durSec = rem end
            end
            if durSec == nil then durSec = self._msufPlainTotal end
            MSUF_PlayerCastbar_UpdateLatencyZone(self, false, durSec)
        end

        self:SetScript("OnUpdate", nil)
        self.MSUF_durationObj = castDuration
        self.MSUF_timerDriven = timerDriven and true or nil
        MSUF_EnsureCastbarManager()
        if MSUF_RegisterCastbar then
            MSUF_RegisterCastbar(self)
        end
        if _G.MSUF_UpdateCastbarFrame then
            local _now = (GetTimePreciseSec and GetTimePreciseSec()) or GetTime()
            _G.MSUF_UpdateCastbarFrame(self, 0, _now)
        end

        self:Show()
        return
    end

    if CHANNEL_START[event] then
        local chanName, chanText, chanTex, startTimeMS, endTimeMS, _, notInterruptible, spellID, isEmpowered, numEmpowerStages, castBarID = UnitChannelInfo(unit)
        if not chanName then
            if UnitCastingInfo(unit) then
                return MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_START")
            end
            return MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_STOP")
        end

        local channelDuration = nil
        if type(UnitChannelDuration) == "function" then
            local okDur, d = MSUF_FastCall(UnitChannelDuration, unit)
            if okDur then channelDuration = d end
        end

        self.interruptFeedbackEndTime = nil
        self._msufActiveCastUnit = unit
        self._msufChanNilSince = nil
        self._msufCastNilSince = nil
        self._msufHardStopNilSince = nil
        MSUF_PlayerCastbar_SetActiveIdentifiers(self, nil, spellID, castBarID)

        self.MSUF_channelDuration = channelDuration
        self.MSUF_castDuration = nil

        local __msuf_rf = _G.MSUF_GetReverseFillSafe(self, true)
        local timerDriven = false
        if channelDuration then
            timerDriven = _G.MSUF_ApplyTimerAndFill(self.statusBar, channelDuration, __msuf_rf) and true or false
        end

        self.MSUF_isChanneled = true
        self._msufStripeReverseFill = (__msuf_rf and true or false)

        local apiNI = false
        if notInterruptible then apiNI = true end
        self.isNotInterruptible = apiNI
        if self.icon then self.icon:SetTexture(chanTex or nil) end
        if self.castText then MSUF_SetTextIfChanged(self.castText, chanName or "") end

        MSUF_PlayerCastbar_SetPlainTiming(self, startTimeMS, endTimeMS)
        if (not timerDriven) and self._msufPlainTotal and self._msufRemaining then
            MSUF_PlayerCastbar_ApplyManualTimerFallback(self, self._msufPlainTotal, self._msufRemaining, __msuf_rf)
        end

        MSUF_PlayerCastbar_UpdateColorForInterruptible(self)

        do
            local total = nil
            if channelDuration and channelDuration.GetTotalDuration then
                local okTotal, t = MSUF_FastCall(channelDuration.GetTotalDuration, channelDuration)
                if okTotal then total = t end
            end
            if total == nil then total = self._msufPlainTotal end
            self.MSUF_channelTotal = total
        end

        do
            local durSec = self.MSUF_channelTotal
            if durSec == nil and channelDuration and channelDuration.GetRemainingDuration then
                local okR, rem = MSUF_FastCall(channelDuration.GetRemainingDuration, channelDuration)
                if okR then durSec = rem end
            end
            if durSec == nil then durSec = self._msufPlainTotal end
            MSUF_PlayerCastbar_UpdateLatencyZone(self, true, durSec)
        end

        self:SetScript("OnUpdate", nil)
        self.MSUF_durationObj = channelDuration
        self.MSUF_timerDriven = timerDriven and true or nil
        MSUF_EnsureCastbarManager()
        if MSUF_RegisterCastbar then
            MSUF_RegisterCastbar(self)
        end
        if _G.MSUF_UpdateCastbarFrame then
            local _now = (GetTimePreciseSec and GetTimePreciseSec()) or GetTime()
            _G.MSUF_UpdateCastbarFrame(self, 0, _now)
        end

        self:Show()
        MSUF_PlayerChannelHasteMarkers_Update(self, true)
        return
    end

    if CAST_STOP[event] then
        self:SetScript("OnUpdate", nil)
        self._msufChanNilSince = nil
        self._msufCastNilSince = nil
        self._msufHardStopNilSince = nil
        self.MSUF_durationObj = nil
        self.MSUF_timerDriven = nil
        self.MSUF_channelDuration = nil
        self.MSUF_castDuration = nil
        self.MSUF_channelTotal = nil
        self.MSUF_isChanneled = nil
        MSUF_PlayerCastbar_ResetPlainTiming(self)
        MSUF_PlayerCastbar_ClearActiveIdentifiers(self)
        self._msufActiveCastUnit = nil
        if MSUF_UnregisterCastbar then MSUF_UnregisterCastbar(self) end

        MSUF_PlayerChannelHasteMarkers_Hide(self)
        if self.latencyBar then self.latencyBar:Hide() end
        if self.timeText then MSUF_SetTextIfChanged(self.timeText, "") end
        if self.statusBar and self.statusBar.SetValue then MSUF_FastCall(self.statusBar.SetValue, self.statusBar, 0) end
        self:Hide()
        return
    end
end


local function MSUF_PlayerCastbar_Cast(self)
    if not self or not self.unit or not self.statusBar then return end
    if self.isEmpower then return end
    if self.MSUF_testMode then return end

    local unit = MSUF_PlayerCastbar_GetEffectiveUnit(self)

    if UnitCastingInfo(unit) then
        MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_START")
    elseif UnitChannelInfo(unit) then
        MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_CHANNEL_START")
    else
        MSUF_PlayerCastbar_UnhaltedUpdate(self, "UNIT_SPELLCAST_STOP")
    end
end

local function MSUF_PlayerCastbar_QueueSoftStateResync(self)
    if not self then return end

    local token = (self._msufSoftResyncToken or 0) + 1
    self._msufSoftResyncToken = token

    if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
            if not self or self._msufSoftResyncToken ~= token then return end
            if self.isEmpower or self.MSUF_testMode then return end
            MSUF_PlayerCastbar_Cast(self)
        end)
    else
        MSUF_PlayerCastbar_Cast(self)
    end
end

function _G.MSUF_UpdateCastbarLatencyIndicator()
    local f = _G.MSUF_PlayerCastBar or _G.MSUF_PlayerCastbar
    if not f or not f.latencyBar or not f.statusBar then return end

    EnsureDB()
    local g = (MSUF_DB and MSUF_DB.general) or {}
    if g.castbarShowLatency == false then
        f.latencyBar:Hide()
        return
    end

    if (f.MSUF_testMode or f._msufIsPreview) and not MSUF_UnitEditModeActive then
        f.latencyBar:Hide()
        return
    end

    local unit = (type(MSUF_PlayerCastbar_GetEffectiveUnit) == "function" and MSUF_PlayerCastbar_GetEffectiveUnit(f)) or f.unit or "player"
    if UnitChannelInfo(unit) then
        local durSec = f.MSUF_channelTotal
        local obj = f.MSUF_channelDuration
        if durSec == nil and obj and obj.GetTotalDuration then
            local okT, total = MSUF_FastCall(obj.GetTotalDuration, obj)
            if okT then durSec = total end
        end
        if durSec == nil and obj and obj.GetRemainingDuration then
            local okR, rem = MSUF_FastCall(obj.GetRemainingDuration, obj)
            if okR then durSec = rem end
        end
        if durSec == nil then durSec = f._msufPlainTotal end
        MSUF_PlayerCastbar_UpdateLatencyZone(f, true, durSec)
        return
    end

    if UnitCastingInfo(unit) then
        local durSec = nil
        local obj = f.MSUF_castDuration
        if obj and obj.GetTotalDuration then
            local okT, total = MSUF_FastCall(obj.GetTotalDuration, obj)
            if okT then durSec = total end
        end
        if durSec == nil and obj and obj.GetRemainingDuration then
            local okR, rem = MSUF_FastCall(obj.GetRemainingDuration, obj)
            if okR then durSec = rem end
        end
        if durSec == nil then durSec = f._msufPlainTotal end
        MSUF_PlayerCastbar_UpdateLatencyZone(f, false, durSec)
        return
    end

    f.latencyBar:Hide()
end

local function MSUF_PlayerCastbar_OnEvent(self, event, ...)
    if not MSUF_IsCastbarEnabledForUnit("player") then
        if type(_G.MSUF_PlayerGCDBar_Stop) == "function" then
            _G.MSUF_PlayerGCDBar_Stop(self)
        end
        self:SetScript("OnUpdate", nil)
        if MSUF_UnregisterCastbar then MSUF_UnregisterCastbar(self) end
        self.interruptFeedbackEndTime = nil
        MSUF_PlayerCastbar_ClearActiveIdentifiers(self)
        self._msufActiveCastUnit = nil
        if self.timeText then
            MSUF_SetTextIfChanged(self.timeText, "")
        end
        if self.latencyBar then
            self.latencyBar:Hide()
        end
        self:Hide()
        return
    end

    if self.MSUF_testMode then
        return
    end

    local unitToken = select(1, ...)

    if event == "UNIT_SPELLCAST_EMPOWER_START" or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
        if type(_G.MSUF_PlayerGCDBar_Stop) == "function" then
            _G.MSUF_PlayerGCDBar_Stop(self, true)
        end
        MSUF_PlayerCastbar_EmpowerStart(self, select(3, ...))
        return
    elseif event == "UNIT_SPELLCAST_EMPOWER_STOP" then
        MSUF_PlayerCastbar_ClearEmpower(self, true)
        return
    end

    if self.isEmpower then
        if event == "UNIT_SPELLCAST_INTERRUPTED" then
            if type(_G.MSUF_PlayerCastbar_ShowInterruptFeedback) == "function" then
                _G.MSUF_PlayerCastbar_ShowInterruptFeedback(self, "Interrupted")
            else
                MSUF_PlayerCastbar_ClearEmpower(self, true)
            end
            return
        elseif event == "UNIT_SPELLCAST_STOP"
            or event == "UNIT_SPELLCAST_FAILED"
            or event == "UNIT_SPELLCAST_SUCCEEDED" then
            MSUF_PlayerCastbar_ClearEmpower(self, true)
            return
        end
    end

    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        if not MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken) then
            return
        end

        local castGUID = select(2, ...)
        local spellID = select(3, ...)
        local castBarID = select(5, ...)
        if MSUF_PlayerCastbar_ShouldIgnoreFail(self, castGUID, spellID, castBarID) then
            return
        end

        if type(_G.MSUF_PlayerGCDBar_Stop) == "function" then
            _G.MSUF_PlayerGCDBar_Stop(self, true)
        end

        MSUF_PlayerCastbar_ClearActiveIdentifiers(self)
        MSUF_PlayerCastbar_ShowInterruptFeedback(self, INTERRUPTED)
        return
    end

    if not self.isEmpower then
        if event == "UNIT_SPELLCAST_START"
        or event == "UNIT_SPELLCAST_SENT"
        or event == "UNIT_SPELLCAST_CHANNEL_START"
        or event == "UNIT_SPELLCAST_INTERRUPTIBLE"
        or event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
            if type(_G.MSUF_PlayerGCDBar_Stop) == "function" then
                _G.MSUF_PlayerGCDBar_Stop(self, true)
            end
            MSUF_PlayerCastbar_UnhaltedUpdate(self, event)
            return
        elseif event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_DELAYED" then
            if not MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken) then
                return
            end
            local castBarID = select(4, ...)
            if not MSUF_PlayerCastbar_EventMatchesCastBarID(self, castBarID) then return end
            MSUF_PlayerCastbar_Cast(self)
            return
        elseif event == "UNIT_SPELLCAST_STOP" then
            if not MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken) then
                return
            end
            local castBarID = select(4, ...)
            if not MSUF_PlayerCastbar_EventMatchesCastBarID(self, castBarID) then return end
            MSUF_PlayerCastbar_UnhaltedUpdate(self, event)
            return
        elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
            if not MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken) then
                return
            end
            local castBarID = select(5, ...)
            if not MSUF_PlayerCastbar_EventMatchesCastBarID(self, castBarID) then return end
            MSUF_PlayerCastbar_QueueSoftStateResync(self)
            return
        elseif event == "UNIT_SPELLCAST_FAILED" then
            if not MSUF_PlayerCastbar_EventMatchesActiveUnit(self, unitToken) then
                return
            end
            local castGUID = select(2, ...)
            local spellID = select(3, ...)
            local castBarID = select(4, ...)
            if MSUF_PlayerCastbar_ShouldIgnoreFail(self, castGUID, spellID, castBarID) then
                return
            end
            MSUF_PlayerCastbar_QueueSoftStateResync(self)
            return
        end
    end

    if event == "UNIT_SPELLCAST_INTERRUPTIBLE" then
        self.isNotInterruptible = false
        MSUF_PlayerCastbar_UpdateColorForInterruptible(self)
        return
    elseif event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
        self.isNotInterruptible = true
        MSUF_PlayerCastbar_UpdateColorForInterruptible(self)
        return
    end
end

---------------------------------------------------------------------------
-- _G exports
---------------------------------------------------------------------------
_G.MSUF_PlayerCastbar_UpdateLatencyZone           = MSUF_PlayerCastbar_UpdateLatencyZone
_G.MSUF_PlayerCastbar_UpdateColorForInterruptible = MSUF_PlayerCastbar_UpdateColorForInterruptible
_G.MSUF_GetInterruptFeedbackColor                 = MSUF_GetInterruptFeedbackColor
_G.MSUF_PlayerCastbar_HideIfNoLongerCasting       = MSUF_PlayerCastbar_HideIfNoLongerCasting
_G.MSUF_PlayerCastbar_ShowInterruptFeedback       = MSUF_PlayerCastbar_ShowInterruptFeedback
_G.MSUF_PlayerCastbar_GetEffectiveUnit            = MSUF_PlayerCastbar_GetEffectiveUnit
_G.MSUF_PlayerCastbar_UnhaltedUpdate              = MSUF_PlayerCastbar_UnhaltedUpdate
_G.MSUF_PlayerCastbar_Cast                        = MSUF_PlayerCastbar_Cast
_G.MSUF_PlayerCastbar_OnEvent                     = MSUF_PlayerCastbar_OnEvent
-- MSUF_UpdateCastbarLatencyIndicator is already defined directly on _G
