-- ============================================================
-- FILE: CastBar.lua
-- PURPOSE: Full custom cast bar system for player, target, and
--   focus units, plus an air/breath bar for mirror timers.
--   Displays spell icon, name, cast timer, and interruptibility
--   status (coloured bar, shield icon, glow pulse).  Handles
--   all cast states: casting, channeling, empowering (staged),
--   crafting, succeeded (green flash), interrupted (red flash).
-- HOW IT WORKS:
--   M = _G.MidnightUI_CastBars module table.  Three cast bar
--   objects (player/target/focus) are created on
--   PLAYER_ENTERING_WORLD; each is a table holding all widget
--   references and state.  A state machine (IDLE/CASTING/
--   CHANNELING/EMPOWERING/SUCCEEDED/INTERRUPTED/FAILED/AIR/
--   CRAFTING) drives per-frame OnUpdate.  External frames
--   (PlayerFrame/TargetFrame/FocusFrame) call the Attach*
--   functions to own the bar.  Blizzard cast bars are fully
--   suppressed (UnregisterAllEvents + Hide).
-- DEPENDS ON:
--   MidnightUI_ResolveFontPath, MidnightUI_StyleOverlay,
--   MidnightUI_MoverRegistry, MidnightUI_AttachOverlaySettings
-- EXPORTS (in _G.MidnightUI_CastBars module):
--   EnsureCastBarSettings, CreateCastBar, LayoutCastBar,
--   HideCastBar, OnCastStart/Channel/Empower/Succeeded/
--   Interrupted/Failed/Stop/Delayed/ChannelUpdate/
--   InterruptibleChanged, SyncCastBarToUnit
-- GLOBAL EXPORTS:
--   MidnightUI_AttachPlayerCastBar / AttachTargetCastBar /
--   AttachFocusCastBar, MidnightUI_ApplyCastBarSettings
-- OWNED SAVESVARIABLES: none (reads MidnightUISettings.CastBars)
-- EVENTS REGISTERED:
--   ADDON_LOADED, PLAYER_ENTERING_WORLD,
--   PLAYER_TARGET_CHANGED, PLAYER_FOCUS_CHANGED,
--   MIRROR_TIMER_START/STOP/PAUSE,
--   UNIT_SPELLCAST_START/STOP/CHANNEL_START/STOP/UPDATE/
--   SUCCEEDED/INTERRUPTED/FAILED/FAILED_QUIET/DELAYED/
--   EMPOWER_START/STOP/UPDATE/INTERRUPTIBLE/NOT_INTERRUPTIBLE
-- FRAMES CREATED: 3 cast bar containers + air bar (all local)
-- GOTCHAS:
--   Empower bars check 0..MAX_EMPOWER_STAGES (4) stage durations.
--   Mirror timer handler only acts on "BREATH" timer name.
--   Glow uses C_CurveUtil.EvaluateColorValueFromBoolean for
--   smooth boolean color interpolation.
--   Bars attached to external owner frames override default
--   parent positioning; Attach* reassigns parent dynamically.
-- ============================================================

local ADDON_NAME = "MidnightUI"
local M = _G.MidnightUI_CastBars or {}
_G.MidnightUI_CastBars = M


local COLOR_INTERRUPTABLE     = { 0.85, 0.68, 0.20 }
local COLOR_NOT_INTERRUPTABLE = { 0.55, 0.58, 0.62 }
local COLOR_CHANNEL           = { 0.25, 0.78, 0.78 }
local COLOR_INTERRUPTED       = { 0.85, 0.15, 0.15 }
local COLOR_SUCCEEDED         = { 1.00, 1.00, 1.00 }
local COLOR_AIR               = { 0.20, 0.55, 0.85 }
local COLOR_CRAFTING          = { 0.80, 0.55, 0.25 }

local GLOW_COLOR              = { 0.10, 0.75, 0.10 }
local GLOW_COLOR_NI           = { 0.75, 0.05, 0.05 }
local GLOW_ALPHA_MIN          = 0.40
local GLOW_ALPHA_MAX          = 0.80
local GLOW_CYCLE              = 1.20

local BORDER_THICKNESS        = 1
local FONT_SPELL              = "Fonts\\FRIZQT__.TTF"
local FONT_TIMER              = "Fonts\\ARIALN.TTF"
local FONT_SPELL_SIZE         = 11
local FONT_TIMER_SIZE         = 10
local SPARK_WIDTH             = 3
local SPARK_OVERSHOOT         = 2

local ICON_BAR_GAP            = 1
local TEXT_PAD_LEFT            = 5
local TEXT_PAD_RIGHT           = 4
local TEXT_GAP                 = 6
local TIMER_MIN_WIDTH          = 36
local SHIELD_SIZE_RATIO        = 0.58
local SHIELD_PAD_RIGHT         = 3
local ICON_TEXCOORD_INSET      = 0.07

local SUCCESS_FAILED_QUIET_GRACE = 0.75

local STATE_IDLE        = 0
local STATE_CASTING     = 1
local STATE_CHANNELING  = 2
local STATE_EMPOWERING  = 3
local STATE_SUCCEEDED   = 4
local STATE_INTERRUPTED = 5
local STATE_FAILED      = 6
local STATE_AIR         = 7
local STATE_CRAFTING    = 8

local MAX_EMPOWER_STAGES = 4

local LAZY_TICK_INTERVAL = 0.05


local CastBarManager = CreateFrame("Frame")
local unitBars = {}
local airBar = nil
local lastLockedState = nil
local playerOwner, targetOwner, focusOwner

local math_max   = math.max
local math_min   = math.min
local math_floor = math.floor
local math_abs   = math.abs
local math_sin   = math.sin
local math_pi    = math.pi
local GetTime    = GetTime


local issecretvalue = issecretvalue or function() return false end

-- ============================================================
-- SECTION: Utility Functions + Cast Bar Creation
-- Safe type coercion helpers, settings accessor, and the
-- CreateCastBar factory that builds a full cast bar widget set:
-- icon frame, status bar, spell name text, timer text, spark,
-- glow frame, flash texture (success/fail), shield icon,
-- empower stage marker array.
--
-- READS:  MidnightUISettings.CastBars (via EnsureCastBarSettings)
-- WRITES: cast bar object table stored on container._muiCastBar
--
-- FUNCTIONS:
--   SafeBool/SafeNum/SafeStr — secret-value-safe coercions
--   EnsureCastBarSettings() — lazy init MidnightUISettings.CastBars
--   CreateCastBar(name,parent) — returns cast bar object with
--     all sub-widgets; bar is in STATE_IDLE
-- ============================================================
local function SafeBool(val, fallback)
    if val == nil then return fallback or false end
    if issecretvalue(val) then return fallback or false end
    return val and true or false
end

local function SetFontSafe(fs, fontPath, size, flags)
    local resolved = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath(fontPath)) or fontPath
    local ok = fs:SetFont(resolved, size, flags)
    if not ok then
        local fallback = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath()) or (GameFontNormal and GameFontNormal:GetFont())
        if fallback then fs:SetFont(fallback, size or 11, flags) end
    end
end

local function GetSettingsColors()
    local s = MidnightUISettings and MidnightUISettings.CastBars and MidnightUISettings.CastBars.colors
    if not s then return nil end
    return s
end

local function GetColor(key, default)
    local colors = GetSettingsColors()
    if colors and colors[key] then return colors[key] end
    return default
end


local function EnsureCastBarSettings()
    if not MidnightUISettings then return nil end
    if not MidnightUISettings.CastBars then MidnightUISettings.CastBars = {} end
    local cb = MidnightUISettings.CastBars

    local contextDefaults = {
        player    = { enabled = true, width = 360, height = 24, scale = 100 },
        target    = { enabled = true, width = 360, height = 24, scale = 100 },
        focus     = { enabled = true, width = 320, height = 20, scale = 100 },
        nameplate = { enabled = true },
        air       = { enabled = true, width = 360, height = 24, scale = 100 },
    }
    for ctx, defs in pairs(contextDefaults) do
        if not cb[ctx] then cb[ctx] = {} end
        for k, v in pairs(defs) do
            if cb[ctx][k] == nil then cb[ctx][k] = v end
        end
    end
    if cb.air and not cb.air._sizeV2 then
        cb.air.width = 360
        cb.air.height = 24
        cb.air._sizeV2 = true
    end

    if not cb.colors then cb.colors = {} end
    local colorDefaults = {
        interruptable    = COLOR_INTERRUPTABLE,
        notInterruptable = COLOR_NOT_INTERRUPTABLE,
        channel          = COLOR_CHANNEL,
        interrupted      = COLOR_INTERRUPTED,
        succeeded        = COLOR_SUCCEEDED,
        air              = COLOR_AIR,
        crafting         = COLOR_CRAFTING,
    }
    for k, v in pairs(colorDefaults) do
        if not cb.colors[k] then cb.colors[k] = { v[1], v[2], v[3] } end
    end

    return cb
end
M.EnsureCastBarSettings = EnsureCastBarSettings


local function CreateCastBar(name, parent)
    local container = CreateFrame("Frame", name, parent or UIParent)
    container:SetFrameStrata("MEDIUM")
    container:SetFrameLevel(60)
    container:SetMovable(true)
    container:SetClampedToScreen(true)
    container:EnableMouse(false)
    container:SetSize(200, 24)

    local iconFrame = CreateFrame("Frame", nil, container)
    iconFrame:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    iconFrame:SetPoint("BOTTOMLEFT", container, "BOTTOMLEFT", 0, 0)
    iconFrame:SetWidth(24)

    local iconBg = iconFrame:CreateTexture(nil, "BACKGROUND")
    iconBg:SetAllPoints()
    iconBg:SetColorTexture(0.05, 0.05, 0.05, 0.90)

    local icon = iconFrame:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", BORDER_THICKNESS, -BORDER_THICKNESS)
    icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -BORDER_THICKNESS, BORDER_THICKNESS)
    icon:SetTexCoord(ICON_TEXCOORD_INSET, 1 - ICON_TEXCOORD_INSET,
                     ICON_TEXCOORD_INSET, 1 - ICON_TEXCOORD_INSET)

    local iconBorderFrame = CreateFrame("Frame", nil, iconFrame)
    iconBorderFrame:SetAllPoints()
    iconBorderFrame:SetFrameLevel(iconFrame:GetFrameLevel() + 1)
    local function MakeBorderEdge(bParent, side, thickness)
        local t = bParent:CreateTexture(nil, "OVERLAY")
        t:SetColorTexture(0, 0, 0, 1)
        if side == "TOP" then
            t:SetHeight(thickness); t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT")
        elseif side == "BOTTOM" then
            t:SetHeight(thickness); t:SetPoint("BOTTOMLEFT"); t:SetPoint("BOTTOMRIGHT")
        elseif side == "LEFT" then
            t:SetWidth(thickness); t:SetPoint("TOPLEFT"); t:SetPoint("BOTTOMLEFT")
        elseif side == "RIGHT" then
            t:SetWidth(thickness); t:SetPoint("TOPRIGHT"); t:SetPoint("BOTTOMRIGHT")
        end
        return t
    end
    MakeBorderEdge(iconBorderFrame, "TOP",    BORDER_THICKNESS)
    MakeBorderEdge(iconBorderFrame, "BOTTOM", BORDER_THICKNESS)
    MakeBorderEdge(iconBorderFrame, "LEFT",   BORDER_THICKNESS)
    MakeBorderEdge(iconBorderFrame, "RIGHT",  BORDER_THICKNESS)

    local bar = CreateFrame("StatusBar", nil, container)
    bar:SetPoint("TOPLEFT", container, "TOPLEFT", 24 + ICON_BAR_GAP, 0)
    bar:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)
    bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    bar:SetStatusBarColor(unpack(COLOR_INTERRUPTABLE))
    bar:SetMinMaxValues(0, 1)
    bar:SetValue(0)
    local barTex = bar:GetStatusBarTexture()
    if barTex then
        barTex:SetHorizTile(false)
        barTex:SetVertTile(false)
    end

    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    bg:SetGradient("VERTICAL",
        CreateColor(0.03, 0.03, 0.03, 0.85),
        CreateColor(0.08, 0.08, 0.10, 0.75))

    local barBorderFrame = CreateFrame("Frame", nil, bar)
    barBorderFrame:SetAllPoints()
    barBorderFrame:SetFrameLevel(bar:GetFrameLevel() + 1)
    MakeBorderEdge(barBorderFrame, "TOP",    BORDER_THICKNESS)
    MakeBorderEdge(barBorderFrame, "BOTTOM", BORDER_THICKNESS)
    MakeBorderEdge(barBorderFrame, "RIGHT",  BORDER_THICKNESS)
    MakeBorderEdge(barBorderFrame, "LEFT",   BORDER_THICKNESS)

    local timerText = bar:CreateFontString(nil, "OVERLAY")
    timerText:SetPoint("RIGHT", bar, "RIGHT", -TEXT_PAD_RIGHT, 0)
    timerText:SetWidth(TIMER_MIN_WIDTH)
    timerText:SetJustifyH("RIGHT")
    timerText:SetJustifyV("MIDDLE")
    timerText:SetWordWrap(false)
    SetFontSafe(timerText, FONT_TIMER, FONT_TIMER_SIZE, "OUTLINE")
    timerText:SetShadowOffset(1, -1)
    timerText:SetShadowColor(0, 0, 0, 0.85)

    local spellText = bar:CreateFontString(nil, "OVERLAY")
    spellText:SetPoint("LEFT", bar, "LEFT", TEXT_PAD_LEFT, 0)
    spellText:SetPoint("RIGHT", timerText, "LEFT", -TEXT_GAP, 0)
    spellText:SetJustifyH("LEFT")
    spellText:SetJustifyV("MIDDLE")
    spellText:SetWordWrap(false)
    spellText:SetMaxLines(1)
    SetFontSafe(spellText, FONT_SPELL, FONT_SPELL_SIZE, "OUTLINE")
    spellText:SetShadowOffset(1, -1)
    spellText:SetShadowColor(0, 0, 0, 0.85)

    local shieldIcon = bar:CreateTexture(nil, "OVERLAY", nil, 4)
    shieldIcon:SetSize(14, 14)
    shieldIcon:SetPoint("LEFT", bar, "LEFT", TEXT_PAD_LEFT, 0)
    shieldIcon:SetTexture("Interface\\CastingBar\\UI-CastingBar-Small-Shield")
    shieldIcon:SetTexCoord(0.15, 0.85, 0.15, 0.85)
    shieldIcon:Hide()

    local spark = bar:CreateTexture(nil, "OVERLAY", nil, 3)
    spark:SetWidth(SPARK_WIDTH)
    spark:SetTexture("Interface\\Buttons\\WHITE8X8")
    spark:SetBlendMode("ADD")
    spark:SetVertexColor(1, 1, 1, 0.55)
    if barTex then
        spark:SetPoint("CENTER", barTex, "RIGHT", 0, 0)
    end
    spark:Hide()

    local glowFrame = CreateFrame("Frame", nil, container)
    glowFrame:SetPoint("TOPLEFT", container, "TOPLEFT", -3, 3)
    glowFrame:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 3, -3)
    glowFrame:SetFrameLevel(math_max(1, container:GetFrameLevel() - 1))
    local GLOW_EDGE_SIZE = 3
    local gR, gG, gB = unpack(GLOW_COLOR)
    local function MakeGlowEdge(gParent, side)
        local t = gParent:CreateTexture(nil, "BACKGROUND")
        t:SetTexture("Interface\\Buttons\\WHITE8X8")
        t:SetBlendMode("ADD")
        t:SetVertexColor(gR, gG, gB, 0.6)
        if side == "TOP" then
            t:SetHeight(GLOW_EDGE_SIZE); t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT")
        elseif side == "BOTTOM" then
            t:SetHeight(GLOW_EDGE_SIZE); t:SetPoint("BOTTOMLEFT"); t:SetPoint("BOTTOMRIGHT")
        elseif side == "LEFT" then
            t:SetWidth(GLOW_EDGE_SIZE); t:SetPoint("TOPLEFT"); t:SetPoint("BOTTOMLEFT")
        elseif side == "RIGHT" then
            t:SetWidth(GLOW_EDGE_SIZE); t:SetPoint("TOPRIGHT"); t:SetPoint("BOTTOMRIGHT")
        end
        return t
    end
    local glowEdges = {
        MakeGlowEdge(glowFrame, "TOP"),
        MakeGlowEdge(glowFrame, "BOTTOM"),
        MakeGlowEdge(glowFrame, "LEFT"),
        MakeGlowEdge(glowFrame, "RIGHT"),
    }
    glowFrame:Hide()

    local flashTex = bar:CreateTexture(nil, "OVERLAY", nil, 5)
    flashTex:SetAllPoints()
    flashTex:SetTexture("Interface\\Buttons\\WHITE8X8")
    flashTex:SetBlendMode("ADD")
    flashTex:SetAlpha(0)

    local stageMarkers = {}

    local innerHighlight = bar:CreateTexture(nil, "OVERLAY", nil, 1)
    innerHighlight:SetHeight(2)
    innerHighlight:SetPoint("TOPLEFT", bar, "TOPLEFT", BORDER_THICKNESS, -BORDER_THICKNESS)
    innerHighlight:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -BORDER_THICKNESS, -BORDER_THICKNESS)
    innerHighlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    innerHighlight:SetGradient("VERTICAL",
        CreateColor(1, 1, 1, 0),
        CreateColor(1, 1, 1, 0.08))

    container:Hide()

    local cb = {
        container    = container,
        bar          = bar,
        barTex       = barTex,
        bg           = bg,
        iconFrame    = iconFrame,
        icon         = icon,
        iconBg       = iconBg,
        spellText    = spellText,
        timerText    = timerText,
        spark        = spark,
        glowFrame    = glowFrame,
        glowEdges    = glowEdges,
        shieldIcon   = shieldIcon,
        flashTex     = flashTex,
        stageMarkers = stageMarkers,
        state            = STATE_IDLE,
        unit             = nil,
        durationObj      = nil,
        castID           = nil,
        spellID          = nil,
        isChannel        = false,
        isEmpowered      = false,
        numStages        = 0,
        stageDurations   = nil,
        isCrafting       = false,
        notInterruptible = false,
        finishAnimTime   = 0,
        lastSuccessAt    = 0,
        startTime        = 0,
        mirrorValue      = 0,
        mirrorMaxValue   = 0,
        mirrorScale      = 0,
        mirrorPaused     = false,
        lazyTickTimer    = 0,
    }

    return cb
end
M.CreateCastBar = CreateCastBar


-- ============================================================
-- SECTION: Layout + Glow + Interrupt Visuals
-- LayoutCastBar resizes/repositions all sub-elements and
-- scales font sizes to match height.  HideCastBar resets bar
-- to STATE_IDLE.  Glow pulse is a sine-wave alpha cycle driven
-- by OnUpdate.  Interrupt visuals change bar color, toggle
-- shield icon, and start/stop the glow.
--
-- READS:  cb.state, cb.interruptible, GLOW_* constants
-- WRITES: bar:SetStatusBarColor, glow:SetAlpha, shield:Show/Hide
--
-- FUNCTIONS:
--   LayoutCastBar(cb,width,height) — resize all sub-elements
--   HideCastBar(cb) — reset to IDLE, hide all elements
--   UpdateGlowPulse(cb,now) — advance glow sine cycle
--   UpdateInterruptVisuals(cb) — sync bar color + shield +
--     glow based on cb.interruptible flag
-- ============================================================
local function LayoutCastBar(cb, width, height)
    if not cb then return end

    local iconSize = height
    local totalW = iconSize + ICON_BAR_GAP + width
    cb.container:SetSize(totalW, height)
    cb.iconFrame:SetWidth(iconSize)

    cb.bar:ClearAllPoints()
    cb.bar:SetPoint("TOPLEFT", cb.container, "TOPLEFT", iconSize + ICON_BAR_GAP, 0)
    cb.bar:SetPoint("BOTTOMRIGHT", cb.container, "BOTTOMRIGHT", 0, 0)

    cb.spark:SetHeight(height + SPARK_OVERSHOOT * 2)

    local spellSize = math_max(8, math_min(14, math_floor(height * 0.48 + 0.5)))
    local timerSize = math_max(7, math_min(12, math_floor(height * 0.42 + 0.5)))
    SetFontSafe(cb.spellText, FONT_SPELL, spellSize, "OUTLINE")
    SetFontSafe(cb.timerText, FONT_TIMER, timerSize, "OUTLINE")

    local timerW = math_max(TIMER_MIN_WIDTH, math_floor(width * 0.12))
    cb.timerText:SetWidth(timerW)

    cb.spellText:ClearAllPoints()
    cb.spellText:SetPoint("LEFT", cb.bar, "LEFT", TEXT_PAD_LEFT, 0)
    cb.spellText:SetPoint("RIGHT", cb.timerText, "LEFT", -TEXT_GAP, 0)

    local shieldSize = math_max(10, math_floor(height * SHIELD_SIZE_RATIO + 0.5))
    cb.shieldIcon:SetSize(shieldSize, shieldSize)
    cb.shieldIcon:ClearAllPoints()
    cb.shieldIcon:SetPoint("LEFT", cb.bar, "LEFT", TEXT_PAD_LEFT, 0)
end
M.LayoutCastBar = LayoutCastBar


local function HideCastBar(cb)
    if not cb then return end
    cb.state = STATE_IDLE
    cb.durationObj = nil
    if lastLockedState ~= false then
        cb.container:Hide()
    end
    cb.container:SetAlpha(1)
    cb.bar:SetValue(0)
    cb.flashTex:SetAlpha(0)
    cb.spark:Hide()
    cb.glowFrame:Hide()
    cb.shieldIcon:Hide()
    cb.spellText:SetText("")
    cb.timerText:SetText("")
    cb.isChannel = false
    cb.isEmpowered = false
    cb.isCrafting = false
    cb.notInterruptible = false
    cb._rawNotInterruptible = nil
    cb._glowEnabled = false
    cb.finishAnimTime = 0
    for i = 1, #cb.stageMarkers do
        cb.stageMarkers[i]:Hide()
    end
    cb.container:SetScript("OnUpdate", nil)
end
M.HideCastBar = HideCastBar


local function UpdateGlowPulse(cb, now)
    local t = (now % GLOW_CYCLE) / GLOW_CYCLE
    local pulseAlpha = GLOW_ALPHA_MIN + (GLOW_ALPHA_MAX - GLOW_ALPHA_MIN) * (0.5 + 0.5 * math_sin(t * 2 * math_pi))
    cb.glowFrame:SetAlpha(pulseAlpha)

    local rawNI = cb._rawNotInterruptible
    local Eval = C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean
    if rawNI ~= nil and Eval and cb.glowEdges then
        local r = Eval(rawNI, GLOW_COLOR_NI[1], GLOW_COLOR[1])
        local g = Eval(rawNI, GLOW_COLOR_NI[2], GLOW_COLOR[2])
        local b = Eval(rawNI, GLOW_COLOR_NI[3], GLOW_COLOR[3])
        for i = 1, #cb.glowEdges do
            cb.glowEdges[i]:SetVertexColor(r, g, b, 1)
        end
    end
end


local function AnchorSpellText(cb, shieldVisible)
    cb.spellText:ClearAllPoints()
    if shieldVisible then
        cb.spellText:SetPoint("LEFT", cb.shieldIcon, "RIGHT", SHIELD_PAD_RIGHT, 0)
    else
        cb.spellText:SetPoint("LEFT", cb.bar, "LEFT", TEXT_PAD_LEFT, 0)
    end
    cb.spellText:SetPoint("RIGHT", cb.timerText, "LEFT", -TEXT_GAP, 0)
end

local function UpdateInterruptVisuals(cb)
    if not cb then return end

    if cb.unit == "player" then
        cb.glowFrame:Hide()
        cb.shieldIcon:Hide()
        cb._glowEnabled = false
        AnchorSpellText(cb, false)
        if cb.isCrafting then
            local c = GetColor("crafting", COLOR_CRAFTING)
            cb.bar:SetStatusBarColor(c[1], c[2], c[3])
        elseif cb.isChannel or cb.isEmpowered then
            local c = GetColor("channel", COLOR_CHANNEL)
            cb.bar:SetStatusBarColor(c[1], c[2], c[3])
        else
            local c = GetColor("interruptable", COLOR_INTERRUPTABLE)
            cb.bar:SetStatusBarColor(c[1], c[2], c[3])
        end
        return
    end

    local baseColor
    if cb.isCrafting then
        baseColor = GetColor("crafting", COLOR_CRAFTING)
    elseif cb.isChannel or cb.isEmpowered then
        baseColor = GetColor("channel", COLOR_CHANNEL)
    else
        baseColor = GetColor("interruptable", COLOR_INTERRUPTABLE)
    end
    local niColor = GetColor("notInterruptable", COLOR_NOT_INTERRUPTABLE)

    local rawNI = cb._rawNotInterruptible
    local Eval = C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean

    if rawNI ~= nil and Eval then
        local r = Eval(rawNI, niColor[1], baseColor[1])
        local g = Eval(rawNI, niColor[2], baseColor[2])
        local b = Eval(rawNI, niColor[3], baseColor[3])
        cb.bar:SetStatusBarColor(r, g, b)

        cb.shieldIcon:Show()
        cb.shieldIcon:SetAlphaFromBoolean(rawNI, 1, 0)

        if cb.isCrafting then
            cb.glowFrame:Hide()
            cb._glowEnabled = false
        else
            cb.glowFrame:Show()
            cb._glowEnabled = true
        end
        AnchorSpellText(cb, false)
    elseif cb.notInterruptible then
        cb.bar:SetStatusBarColor(niColor[1], niColor[2], niColor[3])
        cb.shieldIcon:Show()
        cb.shieldIcon:SetAlpha(1)
        if cb.isCrafting then
            cb.glowFrame:Hide()
            cb._glowEnabled = false
        else
            if cb.glowEdges then
                for i = 1, #cb.glowEdges do
                    cb.glowEdges[i]:SetVertexColor(GLOW_COLOR_NI[1], GLOW_COLOR_NI[2], GLOW_COLOR_NI[3], 1)
                end
            end
            cb.glowFrame:Show()
            cb._glowEnabled = true
        end
        AnchorSpellText(cb, true)
    else
        cb.bar:SetStatusBarColor(baseColor[1], baseColor[2], baseColor[3])
        cb.shieldIcon:Hide()
        if cb.isCrafting then
            cb.glowFrame:Hide()
            cb._glowEnabled = false
        else
            if cb.glowEdges then
                for i = 1, #cb.glowEdges do
                    cb.glowEdges[i]:SetVertexColor(GLOW_COLOR[1], GLOW_COLOR[2], GLOW_COLOR[3], 1)
                end
            end
            cb.glowFrame:Show()
            cb._glowEnabled = true
        end
        AnchorSpellText(cb, false)
    end
end


-- ============================================================
-- SECTION: Empower Stages + Animation States + Update Loop
-- ShowStageMarkers places divider lines at stage duration
-- fractions for empowered casts.  Success/interrupt animations
-- are a time-based fade driven by OnUpdate.  The main
-- OnCastBarUpdate dispatcher routes per-frame elapsed time to
-- the active state handler.  ApplyDuration / ApplyDurationSmooth
-- initialise SetMinMaxValues + SetValue with optional easing.
--
-- READS:  cb.state, cb.duration, cb.finishAnimTime, GetTime()
-- WRITES: bar SetValue, bar:SetStatusBarColor, flash:SetAlpha
--
-- FUNCTIONS:
--   ShowStageMarkers(cb,numStages,durations,total) — position
--     divider lines on the bar; creates lazily
--   UpdateEmpowerStageShading(cb,progress) — grey out completed
--     stages as cast progresses
--   StartSuccessAnimation / RunSuccessAnimation — green flash
--   StartInterruptAnimation / RunInterruptAnimation — red flash
--   OnCastBarUpdate(container,elapsed) — main per-frame handler
--   StartOnUpdate(cb) — attach OnUpdate script
--   ApplyDuration / ApplyDurationSmooth(cb,durationObj,dir)
--     — set bar range + value; smooth variant eases from current
-- ============================================================
local function ShowStageMarkers(cb, numStages, stageDurations, totalDuration)
    if not totalDuration or totalDuration <= 0 then return end
    local barWidth = cb.bar:GetWidth()
    local elapsed = 0
    for i = 1, numStages - 1 do
        elapsed = elapsed + stageDurations[i]
        local fraction = elapsed / totalDuration
        local marker = cb.stageMarkers[i]
        if not marker then
            marker = cb.bar:CreateTexture(nil, "OVERLAY", nil, 2)
            marker:SetWidth(2)
            cb.stageMarkers[i] = marker
        end
        marker:SetColorTexture(1, 1, 1, 0.35)
        marker:ClearAllPoints()
        local xPos = barWidth * fraction
        marker:SetPoint("TOP", cb.bar, "TOPLEFT", xPos, 1)
        marker:SetPoint("BOTTOM", cb.bar, "BOTTOMLEFT", xPos, -1)
        marker:Show()
    end
    for i = numStages, MAX_EMPOWER_STAGES do
        if cb.stageMarkers[i] then cb.stageMarkers[i]:Hide() end
    end
end

local function UpdateEmpowerStageShading(cb, progress)
    if not cb.stageDurations or not cb.totalDuration or cb.totalDuration <= 0 then return end
    local elapsed = progress * cb.totalDuration
    local stageElapsed = 0
    for i = 1, (cb.numStages or 0) - 1 do
        stageElapsed = stageElapsed + cb.stageDurations[i]
        local marker = cb.stageMarkers[i]
        if marker then
            if elapsed >= stageElapsed then
                marker:SetColorTexture(1, 1, 1, 0.90)
            else
                marker:SetColorTexture(1, 1, 1, 0.35)
            end
        end
    end
end


local function StartSuccessAnimation(cb)
    cb.state = STATE_SUCCEEDED
    cb.finishAnimTime = 0
    cb.bar:SetMinMaxValues(0, 1)
    cb.bar:SetValue(1)
    local c = GetColor("succeeded", COLOR_SUCCEEDED)
    cb.flashTex:SetVertexColor(c[1], c[2], c[3])
    cb.flashTex:SetAlpha(0.6)
    cb.spark:Hide()
    cb.glowFrame:Hide()
    cb.shieldIcon:Hide()
    cb.timerText:SetText("")
end

local function RunSuccessAnimation(cb, elapsed)
    cb.finishAnimTime = cb.finishAnimTime + elapsed
    local t = cb.finishAnimTime
    if t < 0.3 then
        local alpha = 1 - (t / 0.3)
        cb.flashTex:SetAlpha(0.6 * alpha)
        cb.container:SetAlpha(alpha)
    else
        cb.flashTex:SetAlpha(0)
        cb.container:SetAlpha(1)
        HideCastBar(cb)
    end
end

local SHAKE_DURATION   = 0.30
local SHAKE_AMP_X      = 2.0
local SHAKE_AMP_Y      = 1.2
local SHAKE_FREQ_X     = 40
local SHAKE_FREQ_Y     = 28
local SHAKE_HOLD_TIME  = 0.35
local SHAKE_FADE_TIME  = 0.30

local function StartInterruptAnimation(cb, interrupterGUID)
    cb.state = STATE_INTERRUPTED
    cb.finishAnimTime = 0

    local bar = cb.bar

    -- Freeze the bar at its current position using the durationObj's
    -- raw values (no division/comparison — secret-safe).
    -- Plater uses this same approach: SetMinMaxValues(0, total), SetValue(elapsed).
    local unit = cb.unit or "player"
    local durationObj = cb.durationObj
    if durationObj then
        local okE, elapsed = pcall(durationObj.GetElapsedDuration, durationObj)
        local okT, total = pcall(durationObj.GetTotalDuration, durationObj)
        if okE and elapsed and okT and total then
            bar:SetMinMaxValues(0, total)
            bar:SetValue(elapsed)
        end
    end
    cb.durationObj = nil

    -- Shake only works on the player cast bar — nameplate/target/focus
    -- frames are restricted during combat (GetPoint triggers taint)
    cb._shakeAnchor = nil
    if unit == "player" then
        local point, relativeTo, relativePoint, baseX, baseY = cb.container:GetPoint(1)
        if point then
            cb._shakeAnchor = { point, relativeTo, relativePoint, baseX or 0, baseY or 0 }
        end
    end

    local c = GetColor("interrupted", COLOR_INTERRUPTED)
    cb.bar:SetStatusBarColor(c[1], c[2], c[3])

    -- Show interrupter name (class-colored) if GUID available.
    -- UnitNameFromGUID / UnitClassFromGUID are secret-safe sinks.
    local interruptLabel = "Interrupted"
    if interrupterGUID then
        local ok, name = pcall(UnitNameFromGUID, interrupterGUID)
        if ok and name then
            local ok2, class = pcall(UnitClassFromGUID, interrupterGUID)
            if ok2 and class then
                local color = C_ClassColor and C_ClassColor.GetClassColor(class)
                if color then
                    name = color:WrapTextInColorCode(name)
                end
            end
            interruptLabel = "Interrupted [" .. name .. "]"
        end
    end
    cb.spellText:SetText(interruptLabel)

    cb.flashTex:SetAlpha(0)
    cb.glowFrame:Hide()
    cb.shieldIcon:Hide()
    cb.timerText:SetText("")
end

local PULSE_DURATION   = 0.10
local PULSE_BRIGHTNESS = 1.8

local function RunInterruptAnimation(cb, elapsed)
    cb.finishAnimTime = cb.finishAnimTime + elapsed
    local t = cb.finishAnimTime
    local anchor = cb._shakeAnchor

    -- Phase timings differ for shake (player) vs pulse (nameplates/target/focus)
    local impactEnd = anchor and SHAKE_DURATION or PULSE_DURATION
    local totalHold = impactEnd + SHAKE_HOLD_TIME
    local totalFade = totalHold + SHAKE_FADE_TIME

    if t < impactEnd then
        if anchor then
            -- Player: shake
            local decay = (1 - (t / SHAKE_DURATION)) ^ 1.5
            local offsetX = SHAKE_AMP_X * decay * math.sin(t * SHAKE_FREQ_X)
            local offsetY = SHAKE_AMP_Y * decay * math.sin(t * SHAKE_FREQ_Y + 0.8)
            pcall(function()
                cb.container:ClearAllPoints()
                cb.container:SetPoint(anchor[1], anchor[2], anchor[3], anchor[4] + offsetX, anchor[5] + offsetY)
            end)
        else
            -- Nameplates/target/focus: quick brightness pulse
            local pulseT = t / PULSE_DURATION
            local brightness = 1 + (PULSE_BRIGHTNESS - 1) * (1 - pulseT)
            local c = GetColor("interrupted", COLOR_INTERRUPTED)
            pcall(cb.bar.SetStatusBarColor, cb.bar,
                math_min(c[1] * brightness, 1),
                math_min(c[2] * brightness, 1),
                math_min(c[3] * brightness, 1))
        end

    elseif t < totalHold then
        -- Restore position / color, hold
        if anchor then
            pcall(function()
                cb.container:ClearAllPoints()
                cb.container:SetPoint(anchor[1], anchor[2], anchor[3], anchor[4], anchor[5])
            end)
        else
            local c = GetColor("interrupted", COLOR_INTERRUPTED)
            pcall(cb.bar.SetStatusBarColor, cb.bar, c[1], c[2], c[3])
        end

    elseif t < totalFade then
        -- Fade out
        local fadeT = (t - totalHold) / SHAKE_FADE_TIME
        cb.container:SetAlpha(1 - fadeT)

    else
        -- Restore and hide
        if anchor then
            pcall(function()
                cb.container:ClearAllPoints()
                cb.container:SetPoint(anchor[1], anchor[2], anchor[3], anchor[4], anchor[5])
            end)
        end
        cb.container:SetAlpha(1)
        HideCastBar(cb)
    end
end


local function OnCastBarUpdate(container, elapsed)
    local cb = container._muiCastBar
    if not cb then return end

    local state = cb.state

    if state == STATE_CASTING or state == STATE_CRAFTING
       or state == STATE_CHANNELING or state == STATE_EMPOWERING then
        local durationObj = cb.durationObj
        if durationObj then
            cb.lazyTickTimer = cb.lazyTickTimer + elapsed
            if cb.lazyTickTimer >= LAZY_TICK_INTERVAL then
                cb.lazyTickTimer = 0
                local ok, remaining = pcall(function() return durationObj:GetRemainingDuration() end)
                if ok and remaining then
                    cb.timerText:SetFormattedText("%.1f", remaining)
                end
            end

            if state == STATE_EMPOWERING then
                local okE, elapsedTime = pcall(function() return durationObj:GetElapsedDuration() end)
                local okT, totalTime = pcall(function() return durationObj:GetTotalDuration() end)
                if okE and okT and elapsedTime and totalTime and totalTime > 0 then
                    local progress = elapsedTime / totalTime
                    UpdateEmpowerStageShading(cb, progress)
                end
            end
        end

    elseif state == STATE_SUCCEEDED then
        RunSuccessAnimation(cb, elapsed)
        return

    elseif state == STATE_INTERRUPTED or state == STATE_FAILED then
        RunInterruptAnimation(cb, elapsed)
        return

    elseif state == STATE_AIR then
        if not cb.mirrorPaused then
            local now = GetTime()
            local elapsedSinceStart = now - cb.startTime
            -- Blizzard's mirror timer scale is in value-units per millisecond;
            -- elapsed is in seconds so multiply by 1000.
            local currentValue = cb.mirrorValue + cb.mirrorScale * elapsedSinceStart * 1000
            local progress = math_max(0, math_min(1, currentValue / cb.mirrorMaxValue))
            cb.bar:SetValue(progress)
            cb.lazyTickTimer = cb.lazyTickTimer + elapsed
            if cb.lazyTickTimer >= LAZY_TICK_INTERVAL then
                cb.lazyTickTimer = 0
                cb.timerText:SetFormattedText("%.0f", math_max(0, currentValue / 1000))
            end
        end
        return

    else
        return
    end

    if cb._glowEnabled
       and (state == STATE_CASTING or state == STATE_CHANNELING or state == STATE_EMPOWERING) then
        UpdateGlowPulse(cb, GetTime())
    end
end

local function StartOnUpdate(cb)
    cb.container._muiCastBar = cb
    cb.lazyTickTimer = 0
    cb.container:SetScript("OnUpdate", OnCastBarUpdate)
end


local function ApplyDuration(cb, durationObj, direction)
    cb.durationObj = durationObj
    if durationObj and cb.bar.SetTimerDuration and Enum and Enum.StatusBarInterpolation then
        cb.bar:SetTimerDuration(
            durationObj,
            Enum.StatusBarInterpolation.Immediate,
            direction or Enum.StatusBarTimerDirection.ElapsedTime
        )
    end
end

local function ApplyDurationSmooth(cb, durationObj, direction)
    cb.durationObj = durationObj
    if durationObj and cb.bar.SetTimerDuration and Enum and Enum.StatusBarInterpolation then
        cb.bar:SetTimerDuration(
            durationObj,
            Enum.StatusBarInterpolation.ExponentialEaseOut,
            direction or Enum.StatusBarTimerDirection.ElapsedTime
        )
    end
end


-- ============================================================
-- SECTION: Cast State Handlers + Blizzard Suppression +
--   Unit Bar Management
-- State machine entry points for every UNIT_SPELLCAST_* event.
-- SyncCastBarToUnit re-syncs a bar to the unit's current cast
-- on login or owner change.  SuppressBlizzardCastBars
-- unregisters events and hides all Blizzard cast bars and
-- MirrorTimers.  InitUnitCastBars creates the three bars on
-- PLAYER_ENTERING_WORLD.  ApplyUnitCastBarSettings applies
-- width/height from settings and re-attaches bars to owners.
--
-- READS:  UnitCastingInfo / UnitChannelInfo / UnitCastingEmpowerInfo,
--         MidnightUISettings.CastBars.* (per kind: player/target/focus)
-- WRITES: cb.state, cb.castID, cb.durationObj, bar SetValue/color
--
-- FUNCTIONS:
--   OnCastStart / OnChannelStart / OnEmpowerStart(cb,unit)
--   OnCastSucceeded / OnCastInterrupted / OnCastFailed(cb,unit)
--   OnCastStop / OnCastDelayed / OnChannelUpdate(cb,unit)
--   OnInterruptibleChanged(cb,notInterruptible)
--   SyncCastBarToUnit(cb,unit) — call after attach or login
--   SuppressBlizzardCastBars() — one-shot suppression
--   InitUnitCastBars() — creates player/target/focus bar objects
--   ApplyUnitCastBarSettings(kind) — layout + visibility
-- ============================================================
local function OnCastStart(cb, unit)
    if not cb then return end

    local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellID
        = UnitCastingInfo(unit)
    if not name then
        HideCastBar(cb)
        return
    end

    local durationObj = UnitCastingDuration and UnitCastingDuration(unit) or nil

    cb.unit = unit
    cb.isChannel = false
    cb.isEmpowered = false
    cb.isCrafting = SafeBool(isTradeSkill)
    cb.notInterruptible = SafeBool(notInterruptible)
    cb._rawNotInterruptible = notInterruptible
    cb.spellID = spellID
    cb.castID = castID
    cb.finishAnimTime = 0
    cb.totalDuration = nil

    if cb.isCrafting then
        cb.state = STATE_CRAFTING
        cb.spark:Hide()
    else
        cb.state = STATE_CASTING
        cb.spark:Show()
    end

    cb.icon:SetTexture(texture)
    cb.spellText:SetText(name or "")
    cb.container:Show()
    cb.container:SetAlpha(1)
    cb.flashTex:SetAlpha(0)

    for i = 1, #cb.stageMarkers do cb.stageMarkers[i]:Hide() end

    ApplyDuration(cb, durationObj, Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.ElapsedTime)

    UpdateInterruptVisuals(cb)
    StartOnUpdate(cb)
end
M.OnCastStart = OnCastStart

local function OnChannelStart(cb, unit)
    if not cb then return end

    local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellID
        = UnitChannelInfo(unit)
    if not name then
        HideCastBar(cb)
        return
    end

    local durationObj = UnitChannelDuration and UnitChannelDuration(unit) or nil

    cb.unit = unit
    cb.isChannel = true
    cb.isEmpowered = false
    cb.isCrafting = SafeBool(isTradeSkill)
    cb.notInterruptible = SafeBool(notInterruptible)
    cb._rawNotInterruptible = notInterruptible
    cb.spellID = spellID
    cb.finishAnimTime = 0
    cb.totalDuration = nil

    if cb.isCrafting then
        cb.state = STATE_CRAFTING
        cb.spark:Hide()
    else
        cb.state = STATE_CHANNELING
        cb.spark:Show()
    end

    cb.icon:SetTexture(texture)
    cb.spellText:SetText(name or "")
    cb.container:Show()
    cb.container:SetAlpha(1)
    cb.flashTex:SetAlpha(0)

    for i = 1, #cb.stageMarkers do cb.stageMarkers[i]:Hide() end

    ApplyDuration(cb, durationObj, Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.RemainingTime)

    UpdateInterruptVisuals(cb)
    StartOnUpdate(cb)
end
M.OnChannelStart = OnChannelStart

local function OnEmpowerStart(cb, unit)
    if not cb then return end

    local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellID
        = UnitChannelInfo(unit)
    if not name then
        HideCastBar(cb)
        return
    end

    local durationObj = UnitEmpoweredChannelDuration and UnitEmpoweredChannelDuration(unit, true) or nil

    cb.unit = unit
    cb.isChannel = true
    cb.isEmpowered = true
    cb.isCrafting = false
    cb.notInterruptible = SafeBool(notInterruptible)
    cb._rawNotInterruptible = notInterruptible
    cb.spellID = spellID
    cb.finishAnimTime = 0

    local stageDurations = {}
    local totalDuration = 0
    local numStages = 0
    for stage = 0, MAX_EMPOWER_STAGES do
        local dur = GetUnitEmpowerStageDuration(unit, stage)
        if issecretvalue(dur) then break end
        if not dur or dur == 0 then break end
        numStages = numStages + 1
        stageDurations[numStages] = dur / 1000
        totalDuration = totalDuration + dur / 1000
    end

    local holdTime = GetUnitEmpowerHoldAtMaxTime and GetUnitEmpowerHoldAtMaxTime(unit) or 0
    if not issecretvalue(holdTime) then
        holdTime = (holdTime or 0) / 1000
        totalDuration = totalDuration + holdTime
    end

    cb.numStages = numStages
    cb.stageDurations = stageDurations
    cb.totalDuration = totalDuration
    cb.state = STATE_EMPOWERING

    cb.icon:SetTexture(texture)
    cb.spellText:SetText(name or "")
    cb.container:Show()
    cb.container:SetAlpha(1)
    cb.flashTex:SetAlpha(0)
    cb.spark:Show()

    ShowStageMarkers(cb, numStages, stageDurations, totalDuration)

    ApplyDuration(cb, durationObj, Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.ElapsedTime)

    UpdateInterruptVisuals(cb)
    StartOnUpdate(cb)
end
M.OnEmpowerStart = OnEmpowerStart

local function OnCastSucceeded(cb, unit)
    if not cb then return end
    if cb.state ~= STATE_CASTING and cb.state ~= STATE_CRAFTING then return end
    if UnitCastingInfo(unit) then return end
    cb.lastSuccessAt = GetTime()
    StartSuccessAnimation(cb)
    StartOnUpdate(cb)
end
M.OnCastSucceeded = OnCastSucceeded

local function OnCastInterrupted(cb, unit, interrupterGUID)
    if not cb then return end
    if cb.state == STATE_IDLE or cb.state == STATE_SUCCEEDED
       or cb.state == STATE_INTERRUPTED or cb.state == STATE_FAILED
       or cb.state == STATE_CHANNELING or cb.state == STATE_EMPOWERING then
        return
    end
    StartInterruptAnimation(cb, interrupterGUID)
    StartOnUpdate(cb)
end
M.OnCastInterrupted = OnCastInterrupted

local function OnCastFailed(cb, unit, isQuiet)
    if not cb then return end
    if isQuiet and cb.lastSuccessAt and (GetTime() - cb.lastSuccessAt) < SUCCESS_FAILED_QUIET_GRACE then
        return
    end
    if cb.state == STATE_IDLE or cb.state == STATE_SUCCEEDED
       or cb.state == STATE_INTERRUPTED or cb.state == STATE_FAILED
       or cb.state == STATE_CHANNELING or cb.state == STATE_EMPOWERING then
        return
    end
    cb.state = STATE_FAILED
    StartInterruptAnimation(cb)
    StartOnUpdate(cb)
end
M.OnCastFailed = OnCastFailed

local function OnCastStop(cb, unit)
    if not cb then return end
    if cb.state == STATE_SUCCEEDED or cb.state == STATE_INTERRUPTED or cb.state == STATE_FAILED then
        return
    end
    if UnitCastingInfo(unit) or UnitChannelInfo(unit) then return end
    HideCastBar(cb)
end
M.OnCastStop = OnCastStop

local function OnCastDelayed(cb, unit)
    if not cb then return end
    if cb.state ~= STATE_CASTING then return end
    local durationObj = UnitCastingDuration and UnitCastingDuration(unit) or nil
    if durationObj then
        ApplyDurationSmooth(cb, durationObj, Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.ElapsedTime)
    end
end
M.OnCastDelayed = OnCastDelayed

local function OnChannelUpdate(cb, unit)
    if not cb then return end
    if cb.state ~= STATE_CHANNELING and cb.state ~= STATE_EMPOWERING then return end
    local durationObj = UnitChannelDuration and UnitChannelDuration(unit) or nil
    if durationObj then
        local dir = cb.isEmpowered
            and (Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.ElapsedTime)
            or  (Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.RemainingTime)
        ApplyDuration(cb, durationObj, dir)
    end
end
M.OnChannelUpdate = OnChannelUpdate

local function OnInterruptibleChanged(cb, notInterruptible)
    if not cb then return end
    if cb.state == STATE_IDLE then return end
    cb.notInterruptible = notInterruptible and true or false
    cb._rawNotInterruptible = cb.notInterruptible
    UpdateInterruptVisuals(cb)
end
M.OnInterruptibleChanged = OnInterruptibleChanged

local function SyncCastBarToUnit(cb, unit)
    if not cb or not unit then return end

    local s = MidnightUISettings and MidnightUISettings.CastBars and MidnightUISettings.CastBars[unit]
    if s and s.enabled == false then
        if cb.container and cb.container:IsShown() then HideCastBar(cb) end
        return
    end

    local name = UnitCastingInfo(unit)
    if name then
        OnCastStart(cb, unit)
        return
    end

    name = UnitChannelInfo(unit)
    if name then
        local dur = GetUnitEmpowerStageDuration(unit, 0)
        if not issecretvalue(dur) and dur and dur > 0 then
            OnEmpowerStart(cb, unit)
        else
            OnChannelStart(cb, unit)
        end
        return
    end

    HideCastBar(cb)
end
M.SyncCastBarToUnit = SyncCastBarToUnit


local function ShouldSuppressKind(kind)
    local s = MidnightUISettings and MidnightUISettings.CastBars and MidnightUISettings.CastBars[kind]
    return not (s and s.enabled == false)
end

local function SuppressBlizzardCastBars()
    local map = {
        { kind = "player", frame = _G.PlayerCastingBarFrame },
        { kind = "target", frame = _G.TargetFrameSpellBar },
        { kind = "focus",  frame = _G.FocusFrameSpellBar },
    }
    for _, e in ipairs(map) do
        local f, kind = e.frame, e.kind
        if f and not (f.IsForbidden and f:IsForbidden()) then
            f._muiKind = kind
            if not f._muiHooked then
                f._muiHooked = true
                pcall(function()
                    f:HookScript("OnShow", function(self)
                        if ShouldSuppressKind(self._muiKind) then self:Hide() end
                    end)
                end)
            end
            if ShouldSuppressKind(kind) then
                pcall(function() f:SetAlpha(0); f:Hide() end)
            else
                pcall(function() f:SetAlpha(1) end)
            end
        end
    end

    local suppressAir = ShouldSuppressKind("air")
    for i = 1, MIRRORTIMER_NUMTIMERS or 3 do
        local frame = _G["MirrorTimer" .. i]
        if frame then
            if not frame._muiHooked then
                frame._muiHooked = true
                pcall(function()
                    frame:HookScript("OnShow", function(self)
                        if ShouldSuppressKind("air") then self:Hide() end
                    end)
                end)
            end
            if suppressAir then pcall(function() frame:Hide() end) end
        end
    end

    local mtc = _G.MirrorTimerContainer
    if mtc then
        if not mtc._muiHooked then
            mtc._muiHooked = true
            pcall(function()
                mtc:HookScript("OnShow", function(self)
                    if ShouldSuppressKind("air") then self:Hide() end
                end)
            end)
            if mtc.SetupTimer then
                pcall(function()
                    hooksecurefunc(mtc, "SetupTimer", function(self)
                        if ShouldSuppressKind("air") then self:Hide() end
                    end)
                end)
            end
        end
        if suppressAir then pcall(function() mtc:Hide() end) end
    end
end


local function GetOwner(kind)
    if kind == "player" then return playerOwner or _G.MidnightUI_PlayerFrame end
    if kind == "target" then return targetOwner or _G.MidnightUI_TargetFrame end
    if kind == "focus" then return focusOwner or _G.MidnightUI_FocusFrame end
    return nil
end

local function RefreshOverlayByKey(key)
    if _G.MidnightUI_GetOverlayHandle then
        local o = _G.MidnightUI_GetOverlayHandle(key)
        if o and o.dragOverlay and o.dragOverlay.SetAllPoints then
            o.dragOverlay:SetAllPoints(o)
        end
    end
end

local EnsureCastBarOverlay

local function InitUnitCastBars()
    local settings = EnsureCastBarSettings()
    if not settings then return end

    for _, kind in ipairs({"player", "target", "focus"}) do
        local s = settings[kind]
        local frameName = "MidnightUI_CastBar_" .. kind
        local cb = CreateCastBar(frameName, UIParent)
        cb.unit = kind
        unitBars[kind] = cb

        local w = s.width or 360
        local h = s.height or 24
        LayoutCastBar(cb, w, h)

        if s.enabled == false then
            cb.container:Hide()
        end
    end
end

local function ApplyUnitCastBarSettings(kind)
    local settings = EnsureCastBarSettings()
    if not settings then return end
    local s = settings[kind]
    local cb = unitBars[kind]
    if not cb or not s then return end

    local w = s.width or 360
    local h = s.height or 24
    LayoutCastBar(cb, w, h)

    local scalePct = tonumber(s.scale) or 100
    if scalePct < 25 then scalePct = 25 elseif scalePct > 300 then scalePct = 300 end
    if cb.container and cb.container.SetScale then
        pcall(cb.container.SetScale, cb.container, scalePct / 100)
    end

    if s.enabled == false then
        HideCastBar(cb)
        if cb.container.dragOverlay then cb.container.dragOverlay:Hide() end
    else
        if lastLockedState == false then
            EnsureCastBarOverlay(kind, cb)
            cb.container:Show()
            if cb.container.dragOverlay then cb.container.dragOverlay:Show() end
        else
            if cb.container.dragOverlay then cb.container.dragOverlay:Hide() end
            if cb.state == STATE_IDLE then
                cb.container:Hide()
            end
        end
    end

    local MR = _G.MidnightUI_MoverRegistry
    local moverKey = "CastBar_" .. kind
    if MR and MR.ApplyPosition then
        MR.ApplyPosition(moverKey)
    end
end


EnsureCastBarOverlay = function(kind, cb)
    if not cb then return end
    local container = cb.container
    if container.dragOverlay then return end

    container:SetMovable(true)
    container:SetClampedToScreen(true)
    container:EnableMouse(true)

    local overlay = CreateFrame("Frame", nil, container, "BackdropTemplate")
    overlay:SetAllPoints()
    overlay:SetFrameStrata("MEDIUM")
    overlay:SetFrameLevel((container:GetFrameLevel() or 0) + 10)
    if _G.MidnightUI_StyleOverlay then
        _G.MidnightUI_StyleOverlay(overlay, nil, nil, "cast")
    else
        overlay:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 }
        })
        overlay:SetBackdropColor(0.05, 0.08, 0.11, 0.30)
        overlay:SetBackdropBorderColor(0.30, 0.46, 0.58, 0.78)
    end

    local label = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    label:SetPoint("CENTER")
    local labels = { player = "PLAYER CAST BAR", target = "TARGET CAST BAR", focus = "FOCUS CAST BAR" }
    label:SetText(labels[kind] or "CAST BAR")
    label:SetTextColor(1, 1, 1)

    overlay:EnableMouse(true)
    overlay:RegisterForDrag("LeftButton")
    overlay:SetScript("OnDragStart", function()
        if InCombatLockdown and InCombatLockdown() then return end
        container._muiIsDragging = true
        container:StartMoving()
    end)
    overlay:SetScript("OnDragStop", function()
        if not container._muiIsDragging then return end
        container:StopMovingOrSizing()
        container._muiIsDragging = false
        local MR = _G.MidnightUI_MoverRegistry
        if MR and MR.SavePosition then MR.SavePosition("CastBar_" .. kind) end
    end)
    overlay:SetScript("OnHide", function()
        if container._muiIsDragging then
            container:StopMovingOrSizing()
            container._muiIsDragging = false
        end
    end)

    if _G.MidnightUI_AttachOverlaySettings then
        _G.MidnightUI_AttachOverlaySettings(overlay, "CastBar_" .. kind)
    end
    container.dragOverlay = overlay
end


local function ApplyAllCastBars()
    for _, kind in ipairs({"player", "target", "focus"}) do
        ApplyUnitCastBarSettings(kind)
    end
end


-- ============================================================
-- SECTION: Air/Breath Bar + Public Attachment API +
--   Settings UI + Mover Registry + Event Handler
-- Air bar handles the BREATH mirror timer only — other mirror
-- timers are ignored.  Public Attach* functions let external
-- frames adopt a cast bar as their child.
-- BuildCastBarOverlaySettings creates per-kind enable/size
-- sliders.  MoverRegistry registers all bars as draggable
-- overlays.  The OnEvent handler routes every
-- UNIT_SPELLCAST_* event to the appropriate bar via CAST_EVENTS
-- unit-list dispatch.
--
-- READS:  MirrorTimer API, MidnightUISettings.CastBars,
--         MidnightUI_MoverRegistry
-- WRITES: airBar state, bar parent reparented on Attach
--
-- FUNCTIONS:
--   GetOrCreateAirBar() — lazy create air bar
--   OnMirrorTimerStart/Stop/Pause — BREATH mirror timer handlers
--   MidnightUI_AttachPlayerCastBar / AttachTargetCastBar /
--     AttachFocusCastBar(ownerFrame) — reparent and relayout
--   MidnightUI_ApplyCastBarSettings() — public re-apply all
--   BuildCastBarOverlaySettings(content,key,kind) — settings panel
--   Mover do-block — registers PlayerCastBar/TargetCastBar/
--     FocusCastBar/AirBar with MoverRegistry
--   EVENT HANDLER: routes ADDON_LOADED, PEW, target/focus
--     changed, mirror timers, and all UNIT_SPELLCAST_* events
-- ============================================================
local function GetOrCreateAirBar()
    if airBar then return airBar end
    airBar = CreateCastBar("MidnightUI_AirBar", UIParent)
    airBar.unit = "player"
    local settings = EnsureCastBarSettings()
    local s = settings and settings.air
    local w = (s and s.width) or 200
    local h = (s and s.height) or 16
    LayoutCastBar(airBar, w, h)
    airBar.container:SetFrameStrata("MEDIUM")
    airBar.container:SetFrameLevel(55)

    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.DefineMover then
        MR.DefineMover("AirBar", {
            resolveFrame = function() return airBar and airBar.container end,
            defaultPosition = "TOP,UIParent,TOP,0,-100",
            category = "cast", label = "Air Bar",
            externalOverlay = true,
            onLock = function() lastLockedState = true; ApplyAllCastBars() end,
            onUnlock = function() lastLockedState = false; ApplyAllCastBars() end,
        })
        MR.ApplyPosition("AirBar")
    else
        airBar.container:SetPoint("TOP", UIParent, "TOP", 0, -100)
    end

    return airBar
end

local function OnMirrorTimerStart(timerName, value, maxValue, scale, paused, label)
    if timerName ~= "BREATH" then return end
    local settings = EnsureCastBarSettings()
    if settings and settings.air and settings.air.enabled == false then return end

    local ab = GetOrCreateAirBar()
    ab.state = STATE_AIR
    local c = GetColor("air", COLOR_AIR)
    ab.bar:SetStatusBarColor(c[1], c[2], c[3])
    ab.bar:SetMinMaxValues(0, 1)
    ab.bar:SetValue(value / maxValue)
    ab.spellText:SetText(label or "Breath")
    ab.icon:SetTexture("Interface\\Icons\\Spell_Shadow_DemonBreath")
    ab.startTime = GetTime()
    ab.mirrorValue = value
    ab.mirrorMaxValue = maxValue
    ab.mirrorScale = scale
    ab.mirrorPaused = (paused == true) or (type(paused) == "number" and paused ~= 0)
    ab.glowFrame:Hide()
    ab.shieldIcon:Hide()
    ab.spark:Hide()
    ab.container:Show()
    ab.container:SetAlpha(1)
    ab.flashTex:SetAlpha(0)
    StartOnUpdate(ab)
end

local function OnMirrorTimerStop(timerName)
    if timerName ~= "BREATH" then return end
    if airBar then HideCastBar(airBar) end
end

local function OnMirrorTimerPause(duration)
    if not airBar or airBar.state ~= STATE_AIR then return end
    if duration and duration > 0 then
        airBar.mirrorPaused = true
    else
        airBar.mirrorPaused = false
        airBar.startTime = GetTime()
        local progress = airBar.bar:GetValue()
        airBar.mirrorValue = progress * airBar.mirrorMaxValue
    end
end


function _G.MidnightUI_AttachPlayerCastBar(ownerFrame)
    playerOwner = ownerFrame
    ApplyUnitCastBarSettings("player")
end

function _G.MidnightUI_AttachTargetCastBar(ownerFrame)
    targetOwner = ownerFrame
    ApplyUnitCastBarSettings("target")
end

function _G.MidnightUI_AttachFocusCastBar(ownerFrame)
    focusOwner = ownerFrame
    ApplyUnitCastBarSettings("focus")
end

function _G.MidnightUI_ApplyCastBarSettings()
    local locked = true
    if MidnightUISettings and MidnightUISettings.Messenger and MidnightUISettings.Messenger.locked ~= nil then
        locked = MidnightUISettings.Messenger.locked
    end
    lastLockedState = locked
    ApplyAllCastBars()
    SuppressBlizzardCastBars()
end


local function BuildCastBarOverlaySettings(content, key, kind)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    local s = EnsureCastBarSettings()
    if not s or not s[kind] then return end
    local cfg = s[kind]
    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Cast Bar")
    b:Checkbox("Enable", cfg.enabled ~= false, function(v)
        cfg.enabled = v
        _G.MidnightUI_ApplyCastBarSettings()
        RefreshOverlayByKey(key)
    end)
    b:Slider("Width", 200, 700, 5, cfg.width or 360, function(v)
        cfg.width = math_floor(v)
        _G.MidnightUI_ApplyCastBarSettings()
        RefreshOverlayByKey(key)
    end)
    b:Slider("Height", 8, 60, 1, cfg.height or 20, function(v)
        cfg.height = math_floor(v)
        _G.MidnightUI_ApplyCastBarSettings()
        RefreshOverlayByKey(key)
    end)
    return b:Height()
end

if _G.MidnightUI_RegisterOverlaySettings then
    _G.MidnightUI_RegisterOverlaySettings("CastBar_player", {
        title = "Player Cast Bar",
        build = function(content, key) return BuildCastBarOverlaySettings(content, key, "player") end
    })
    _G.MidnightUI_RegisterOverlaySettings("CastBar_target", {
        title = "Target Cast Bar",
        build = function(content, key) return BuildCastBarOverlaySettings(content, key, "target") end
    })
    _G.MidnightUI_RegisterOverlaySettings("CastBar_focus", {
        title = "Focus Cast Bar",
        build = function(content, key) return BuildCastBarOverlaySettings(content, key, "focus") end
    })
end


do
    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.DefineMover then
        MR.DefineMover("CastBar_player", {
            resolveFrame = function() return unitBars.player and unitBars.player.container end,
            defaultPosition = "BOTTOM,UIParent,BOTTOM,-280.56,212.89",
            category = "cast", label = "Player Cast Bar",
            externalOverlay = true,
            onLock = function() lastLockedState = true; ApplyAllCastBars() end,
            onUnlock = function() lastLockedState = false; ApplyAllCastBars() end,
        })
        MR.DefineMover("CastBar_target", {
            resolveFrame = function() return unitBars.target and unitBars.target.container end,
            defaultPosition = "BOTTOM,UIParent,BOTTOM,293.89,215.00",
            category = "cast", label = "Target Cast Bar",
            externalOverlay = true,
            onLock = function() lastLockedState = true; ApplyAllCastBars() end,
            onUnlock = function() lastLockedState = false; ApplyAllCastBars() end,
        })
        MR.DefineMover("CastBar_focus", {
            resolveFrame = function() return unitBars.focus and unitBars.focus.container end,
            defaultPosition = "CENTER,UIParent,CENTER,-298.47,-321.63",
            category = "cast", label = "Focus Cast Bar",
            externalOverlay = true,
            onLock = function() lastLockedState = true; ApplyAllCastBars() end,
            onUnlock = function() lastLockedState = false; ApplyAllCastBars() end,
        })
    end
end


CastBarManager:RegisterEvent("ADDON_LOADED")
CastBarManager:RegisterEvent("PLAYER_ENTERING_WORLD")
CastBarManager:RegisterEvent("PLAYER_TARGET_CHANGED")
CastBarManager:RegisterEvent("PLAYER_FOCUS_CHANGED")
CastBarManager:RegisterEvent("MIRROR_TIMER_START")
CastBarManager:RegisterEvent("MIRROR_TIMER_STOP")
CastBarManager:RegisterEvent("MIRROR_TIMER_PAUSE")

local CAST_EVENTS = {
    "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
    "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_FAILED_QUIET",
    "UNIT_SPELLCAST_INTERRUPTED", "UNIT_SPELLCAST_SUCCEEDED",
    "UNIT_SPELLCAST_CHANNEL_START", "UNIT_SPELLCAST_CHANNEL_STOP",
    "UNIT_SPELLCAST_CHANNEL_UPDATE", "UNIT_SPELLCAST_DELAYED",
    "UNIT_SPELLCAST_INTERRUPTIBLE", "UNIT_SPELLCAST_NOT_INTERRUPTIBLE",
    "UNIT_SPELLCAST_EMPOWER_START", "UNIT_SPELLCAST_EMPOWER_STOP",
    "UNIT_SPELLCAST_EMPOWER_UPDATE",
}
for _, ev in ipairs(CAST_EVENTS) do
    CastBarManager:RegisterUnitEvent(ev, "player", "target", "focus")
end


CastBarManager:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName ~= ADDON_NAME then return end
        EnsureCastBarSettings()
        SuppressBlizzardCastBars()
        InitUnitCastBars()
        _G.MidnightUI_ApplyCastBarSettings()
        return
    end

    if event == "PLAYER_ENTERING_WORLD" then
        SuppressBlizzardCastBars()
        for _, kind in ipairs({"player", "target", "focus"}) do
            local cb = unitBars[kind]
            if cb then SyncCastBarToUnit(cb, kind) end
        end
        return
    end

    if event == "PLAYER_TARGET_CHANGED" then
        local cb = unitBars.target
        if cb then SyncCastBarToUnit(cb, "target") end
        return
    end

    if event == "PLAYER_FOCUS_CHANGED" then
        local cb = unitBars.focus
        if cb then SyncCastBarToUnit(cb, "focus") end
        return
    end

    if event == "MIRROR_TIMER_START" then
        OnMirrorTimerStart(...)
        return
    end
    if event == "MIRROR_TIMER_STOP" then
        OnMirrorTimerStop(...)
        return
    end
    if event == "MIRROR_TIMER_PAUSE" then
        OnMirrorTimerPause(...)
        return
    end

    local unit = ...
    if not unit then return end
    local cb = unitBars[unit]
    if not cb then return end

    local s = MidnightUISettings and MidnightUISettings.CastBars and MidnightUISettings.CastBars[unit]
    if s and s.enabled == false then
        if cb.container and cb.container:IsShown() then HideCastBar(cb) end
        return
    end

    if event == "UNIT_SPELLCAST_START" then
        OnCastStart(cb, unit)
    elseif event == "UNIT_SPELLCAST_CHANNEL_START" then
        OnChannelStart(cb, unit)
    elseif event == "UNIT_SPELLCAST_EMPOWER_START" then
        OnEmpowerStart(cb, unit)
    elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
        OnCastSucceeded(cb, unit)
    elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
        local eventCastGUID = select(2, ...)
        local interrupterGUID = select(4, ...)
        if cb.castID and eventCastGUID and not issecretvalue(eventCastGUID) and not issecretvalue(cb.castID) and eventCastGUID ~= cb.castID then
            return
        end
        OnCastInterrupted(cb, unit, interrupterGUID)
    elseif event == "UNIT_SPELLCAST_FAILED" then
        local eventCastGUID = select(2, ...)
        if cb.castID and eventCastGUID and not issecretvalue(eventCastGUID) and not issecretvalue(cb.castID) and eventCastGUID ~= cb.castID then
            return
        end
        OnCastFailed(cb, unit, false)
    elseif event == "UNIT_SPELLCAST_FAILED_QUIET" then
        local eventCastGUID = select(2, ...)
        if cb.castID and eventCastGUID and not issecretvalue(eventCastGUID) and not issecretvalue(cb.castID) and eventCastGUID ~= cb.castID then
            return
        end
        OnCastFailed(cb, unit, true)
    elseif event == "UNIT_SPELLCAST_STOP" then
        OnCastStop(cb, unit)
    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" or event == "UNIT_SPELLCAST_EMPOWER_STOP" then
        OnCastStop(cb, unit)
    elseif event == "UNIT_SPELLCAST_DELAYED" then
        OnCastDelayed(cb, unit)
    elseif event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
        OnChannelUpdate(cb, unit)
    elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" then
        OnInterruptibleChanged(cb, false)
    elseif event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
        OnInterruptibleChanged(cb, true)
    end

end)


M.STATE_IDLE        = STATE_IDLE
M.STATE_CASTING     = STATE_CASTING
M.STATE_CHANNELING  = STATE_CHANNELING
M.STATE_EMPOWERING  = STATE_EMPOWERING
M.STATE_SUCCEEDED   = STATE_SUCCEEDED
M.STATE_INTERRUPTED = STATE_INTERRUPTED
M.STATE_FAILED      = STATE_FAILED
M.STATE_AIR         = STATE_AIR
M.STATE_CRAFTING    = STATE_CRAFTING
