-- =============================================================
--  rCastbar -- Standalone Spell Castbar Addon
--  Author : Imithat (WoWInterface.com) - selias2k (curseforge.com)
--  Version: 1.0.6
--  WoW    : Retail 12.0 (Interface 120001)
--  Lua    : 5.1  (WoW embedded)
-- =============================================================

local ADDON_NAME = ...
local L          -- set after ADDON_LOADED from rCastbar_L

local MEDIA = "Interface\\AddOns\\rCastbar\\media\\"

local floor = math.floor
local max   = math.max
local wipe  = table.wipe

-- Artwork proportions
local ART_BG_RATIO   = 170 / 265
local ART_GLOW_RATIO =  35 / 265
local ART_BG_TOP     =  24.9
local ART_BG_BOT     = -24.2
local ART_GLOW_TOP   =  26.5
local ART_GLOW_BOT   = -24.2

-- Default configuration
local defaults = {
    player = {
        enabled        = true,
        width          = 265,
        height         = 15,
        point          = "BOTTOM",
        relPoint       = "BOTTOM",
        relFrame       = "UIParent",
        x              = 0,
        y              = 181,
        fontSize       = 11,
        latency        = true,
        timeToHold     = 0.5,
        -- Success Flash
        castSuccess    = true,
        successColor   = { r = 0,   g = 0.8, b = 0,   a = 1.0 },
        -- Fade
        fadeEnable     = true,
        fadeInTime     = 0.15,
        fadeOutTime    = 0.30,
        -- Dynamic Width
        dynamicWidth   = false,
        -- Icon / Text
        showIcon       = true,
        showSpellName  = true,
        showTime       = true,
        -- Colors
        classColor     = false,
        color          = { r = 0,   g = 0.5, b = 1,   a = 0.8 },
        bgColor        = { r = 0.1, g = 0.1, b = 0.1, a = 0.7 },
        failColor      = { r = 1,   g = 0,   b = 0,   a = 1.0 },
    },
    target = {
        enabled        = true,
        width          = 265,
        height         = 15,
        point          = "TOP",
        relPoint       = "TOP",
        relFrame       = "UIParent",
        x              = -10,
        y              = -125,
        fontSize       = 11,
        timeToHold     = 0.5,
        castSuccess    = true,
        successColor   = { r = 0,   g = 0.8, b = 0,   a = 1.0 },
        fadeEnable     = true,
        fadeInTime     = 0.15,
        fadeOutTime    = 0.30,
        dynamicWidth   = false,
        showIcon       = true,
        showSpellName  = true,
        showTime       = true,
        classColor     = false,
        color          = { r = 0,   g = 0.5, b = 1,   a = 0.7 },
        bgColor        = { r = 0.1, g = 0.1, b = 0.1, a = 1.0 },
        shieldColor    = { r = 0.5, g = 0.5, b = 0.5, a = 1.0 },
        shieldBgColor  = { r = 0.1, g = 0.1, b = 0.1, a = 0.7 },
        failColor      = { r = 1,   g = 0,   b = 0,   a = 1.0 },
    },
}

-- Helpers

local function deepMerge(target, source)
    for k, v in pairs(source) do
        if target[k] == nil then
            if type(v) == "table" then
                target[k] = {}
                deepMerge(target[k], v)
            else
                target[k] = v
            end
        elseif type(v) == "table" and type(target[k]) == "table" then
            deepMerge(target[k], v)
        end
    end
end

local function mkFont(parent, size)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    if not fs:SetFont(MEDIA .. "Expressway.ttf", size, "THINOUTLINE") then
        fs:SetFont("Fonts\\FRIZQT__.TTF", size, "THINOUTLINE")
    end
    fs:SetShadowColor(0, 0, 0, 1)
    fs:SetShadowOffset(1, -1)
    return fs
end

local function updateArtwork(bar, w)
    local bgOver   = w * ART_BG_RATIO
    local glowOver = w * ART_GLOW_RATIO

    bar.background:ClearAllPoints()
    bar.background:SetPoint("TOP",    bar, "TOP",    0,        ART_BG_TOP)
    bar.background:SetPoint("LEFT",   bar, "LEFT",  -bgOver,   0)
    bar.background:SetPoint("RIGHT",  bar, "RIGHT",  bgOver,   0)
    bar.background:SetPoint("BOTTOM", bar, "BOTTOM", 0,        ART_BG_BOT)

    bar.glow:ClearAllPoints()
    bar.glow:SetPoint("TOP",    bar, "TOP",    0,          ART_GLOW_TOP)
    bar.glow:SetPoint("LEFT",   bar, "LEFT",  -glowOver,   0)
    bar.glow:SetPoint("RIGHT",  bar, "RIGHT",  glowOver,   0)
    bar.glow:SetPoint("BOTTOM", bar, "BOTTOM", 0,          ART_GLOW_BOT)

    bar.highlight:ClearAllPoints()
    bar.highlight:SetPoint("TOP",    bar, "TOP",    0,          ART_GLOW_TOP)
    bar.highlight:SetPoint("LEFT",   bar, "LEFT",  -glowOver,   0)
    bar.highlight:SetPoint("RIGHT",  bar, "RIGHT",  glowOver,   0)
    bar.highlight:SetPoint("BOTTOM", bar, "BOTTOM", 0,          ART_GLOW_BOT)
end

local function suppressBlizzardCastbars()
    -- Only UnregisterAllEvents + Hide. Never overwrite .Show on secure Blizzard frames:
    -- assigning a plain Lua function to .Show taints the frame permanently, which
    -- propagates into Blizzard secure code (EditMode -> RefreshEncounterEvents) and
    -- causes "attempt to compare a secret number value (tainted by rCastbar)".
    if PlayerCastingBarFrame then
        PlayerCastingBarFrame:UnregisterAllEvents()
        PlayerCastingBarFrame:Hide()
    end
    if PetCastingBarFrame then
        PetCastingBarFrame:UnregisterAllEvents()
        PetCastingBarFrame:Hide()
    end
end

-- Bar state

local function resetBarState(bar)
    bar.casting          = false
    bar.channeling       = false
    bar.empowering       = false
    bar.castID           = nil
    bar.spellID          = nil
    bar.notInterruptible = false
    bar.holdTime         = 0
    bar.delay            = 0
    bar.max              = 0
    bar.numStages        = nil
    bar.curStage         = 0
    bar.successHold      = 0
    bar.failHold         = 0
    wipe(bar.stagePoints)
end

-- Fade helpers

local function fadeIn(bar)
    local cfg = bar.cfg
    if not cfg.fadeEnable then
        bar:SetAlpha(1)
        return
    end
    bar:SetAlpha(0)
    bar.fadeTime   = 0
    bar.fadingIn   = true
    bar.fadingOut  = false
    bar.fadeTarget = cfg.fadeInTime
end

local function fadeOut(bar)
    local cfg = bar.cfg
    if not cfg.fadeEnable then
        bar:Hide()
        return
    end
    bar.fadeTime   = 0
    bar.fadingIn   = false
    bar.fadingOut  = true
    bar.fadeTarget = cfg.fadeOutTime
end

-- Class color helper

local function getClassColor(unit)
    local _, classFile = UnitClass(unit)
    if not classFile then return nil end
    local cc = RAID_CLASS_COLORS[classFile]
    if not cc then return nil end
    return cc.r, cc.g, cc.b
end

-- Apply bar color

local function applyBarColor(bar)
    local cfg = bar.cfg
    local r, g, b = cfg.color.r, cfg.color.g, cfg.color.b
    if cfg.classColor then
        local cr, cg, cb = getClassColor(bar.unit)
        if cr then r, g, b = cr, cg, cb end
    end
    bar:SetStatusBarColor(r, g, b, cfg.color.a)
    if bar.bg then bar.bg:SetVertexColor(cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b, cfg.bgColor.a) end
end

-- Success flash

local SUCCESS_HOLD = 0.45

local function showSuccess(bar)
    local cfg = bar.cfg
    if not cfg.castSuccess then
        resetBarState(bar)
        bar:SetMinMaxValues(0, 1)
        bar:SetValue(0)
        fadeOut(bar)
        return
    end
    bar:SetMinMaxValues(0, 1)
    bar:SetValue(1)
    local sc = cfg.successColor
    bar:SetStatusBarColor(sc.r, sc.g, sc.b, sc.a)
    if bar.bg then
        bar.bg:SetVertexColor(sc.r * 0.25, sc.g * 0.25, sc.b * 0.25, cfg.bgColor.a)
    end
    if bar.Spark then bar.Spark:Hide() end
    if bar.Time  then bar.Time:SetText("") end
    resetBarState(bar)
    bar.successHold = SUCCESS_HOLD
end

-- Shield coloring

local function applyShieldColor(bar, shielded)
    local cfg = bar.cfg
    if not cfg.shieldColor then return end
    if shielded and UnitCanAttack("player", bar.unit) then
        bar:SetStatusBarColor(cfg.shieldColor.r, cfg.shieldColor.g, cfg.shieldColor.b, cfg.shieldColor.a)
        if bar.bg then bar.bg:SetVertexColor(cfg.shieldBgColor.r, cfg.shieldBgColor.g, cfg.shieldBgColor.b, cfg.shieldBgColor.a) end
        bar.Spark:SetVertexColor(0.8, 0.8, 0.8, 1)
        bar.background:SetDesaturated(true)
    else
        applyBarColor(bar)
        bar.Spark:SetVertexColor(0.8, 0.6, 0, 1)
        bar.background:SetDesaturated(false)
    end
end

-- Helper: adjust Text RIGHT anchor based on showTime setting
local function updateTextAnchor(bar)
    local rightOfs = (bar.cfg.showTime ~= false) and -55 or -5
    bar.Text:ClearAllPoints()
    bar.Text:SetPoint("LEFT",  bar, "LEFT",  5,        0)
    bar.Text:SetPoint("RIGHT", bar, "RIGHT", rightOfs, 0)
end

-- OnUpdate

local function onUpdate(self, elapsed)
    -- Fade-In
    if self.fadingIn then
        self.fadeTime = self.fadeTime + elapsed
        local pct = math.min(self.fadeTime / self.fadeTarget, 1)
        self:SetAlpha(pct)
        if pct >= 1 then self.fadingIn = false end
    end

    -- Success-Hold
    if self.successHold and self.successHold > 0 then
        self.successHold = self.successHold - elapsed
        if self.successHold <= 0 then
            self.successHold = 0
            fadeOut(self)
        end
        return
    end

    -- Failed-Hold -- checked BEFORE fadingOut so it can cancel a running fade
    if self.failHold and self.failHold > 0 then
        self.fadingOut = false
        self.fadingIn  = false
        self:SetAlpha(1)
        self.failHold = self.failHold - elapsed
        if self.failHold <= 0 then
            self.failHold = 0
            resetBarState(self)
            fadeOut(self)
        end
        return
    end

    -- Fade-Out
    if self.fadingOut then
        self.fadeTime = self.fadeTime + elapsed
        local pct = math.min(self.fadeTime / self.fadeTarget, 1)
        self:SetAlpha(1 - pct)
        if pct >= 1 then
            self.fadingOut = false
            self:SetAlpha(1)
            self:Hide()
        end
        return
    end

    -- Preview mode: static display, skip all cast logic
    if self.previewMode then return end

    -- Active cast / channel / empowered
    if self.casting or self.empowering then
        self.duration = self.duration + elapsed
        if self.duration >= self.max then
            self.duration = self.max
        end
    elseif self.channeling then
        self.duration = self.duration - elapsed
        if self.duration <= 0 then
            self.duration = 0
        end
    else
        return
    end

    -- Time text
    if self.Time and self.cfg.showTime ~= false then
        if self.delay and self.delay > 0 then
            local sign = (self.casting or self.empowering) and "+" or "-"
            self.Time:SetFormattedText("%.1f|cffff0000%s%.2f|r", self.duration, sign, self.delay)
        else
            self.Time:SetFormattedText("%.1f", self.duration)
        end
    end

    -- Spark
    if self.Spark then
        self.Spark:ClearAllPoints()
        self.Spark:SetPoint("CENTER", self:GetStatusBarTexture(), "RIGHT", 0, 0)
    end

    self:SetValue(self.duration)
end

-- Build castbar frame

local function buildCastbar(unit, cfg)
    local baseName = "rCastbar_" .. (unit:gsub("^%l", string.upper))

    local container = CreateFrame("Frame", baseName .. "_Container", UIParent)
    container:SetSize(cfg.width + 20, cfg.height + 50)
    container:SetPoint(cfg.point, _G[cfg.relFrame] or UIParent, cfg.relPoint, cfg.x, cfg.y)
    container:SetMovable(true)
    container:SetUserPlaced(true)
    container:SetClampedToScreen(true)
    container:EnableMouse(false)
    container.dragging = false
    container:RegisterForDrag("LeftButton")
    container:SetScript("OnDragStart", function(f)
        if IsShiftKeyDown() then
            f.dragging = true
            f:EnableMouse(true)  -- ensure mouse stays on during drag
            f:StartMoving()
        end
    end)
    container:SetScript("OnDragStop", function(f)
        f.dragging = false
        f:StopMovingOrSizing()
        local point, _, relPoint, x, y = f:GetPoint()
        if rCastbarDB and rCastbarDB[unit] then
            rCastbarDB[unit].point    = point    or rCastbarDB[unit].point
            rCastbarDB[unit].relPoint = relPoint or rCastbarDB[unit].relPoint
            rCastbarDB[unit].x        = floor((x or 0) + 0.5)
            rCastbarDB[unit].y        = floor((y or 0) + 0.5)
        end
        -- If bar is hidden and not in preview, release mouse
        local b = _G[baseName]
        if b and not b:IsShown() then
            f:EnableMouse(false)
        end
    end)

    local bar = CreateFrame("StatusBar", baseName, container)
    bar:SetSize(cfg.width, cfg.height)
    bar:SetPoint("CENTER", container, 0, 0)
    bar:SetFrameStrata("HIGH")
    bar:SetStatusBarTexture(MEDIA .. "statusbar3")
    bar:SetStatusBarColor(cfg.color.r, cfg.color.g, cfg.color.b, cfg.color.a)
    bar:SetMinMaxValues(0, 1)
    bar:SetValue(0)

    -- Artwork
    bar.background = bar:CreateTexture(nil, "OVERLAY", nil, -8)
    bar.background:SetTexture(MEDIA .. "castbar")

    bar.bg = bar:CreateTexture(nil, "BACKGROUND", nil, -6)
    bar.bg:SetTexture(MEDIA .. "statusbar3")
    bar.bg:SetAllPoints(bar)
    bar.bg:SetVertexColor(cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b, cfg.bgColor.a)

    bar.glow = bar:CreateTexture(nil, "OVERLAY", nil, -4)
    bar.glow:SetTexture(MEDIA .. "castbar_glow")
    bar.glow:SetVertexColor(0, 0, 0, 1)

    bar.highlight = bar:CreateTexture(nil, "OVERLAY", nil, -3)
    bar.highlight:SetTexture(MEDIA .. "castbar_highlight")

    updateArtwork(bar, cfg.width)
    bar.UpdateArtwork = function(self, w) updateArtwork(self, w) end

    -- Spark
    bar.Spark = bar:CreateTexture(nil, "OVERLAY", nil, -7)
    bar.Spark:SetBlendMode("ADD")
    bar.Spark:SetVertexColor(0.8, 0.6, 0, 1)
    bar.Spark:SetSize(20, 30)
    bar.Spark:Hide()

    -- Icon
    bar.Icon = bar:CreateTexture(nil, "OVERLAY", nil, -6)
    bar.Icon:SetSize(cfg.height + 4, cfg.height + 4)
    bar.Icon:SetPoint("RIGHT", bar, "LEFT", -3, 0)
    bar.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    if cfg.showIcon then bar.Icon:Show() else bar.Icon:Hide() end

    -- Spell Name
    bar.Text = mkFont(bar, cfg.fontSize)
    -- RIGHT anchor: leave room for time text, or extend if time is hidden
    bar.Text:SetPoint("LEFT",  bar, "LEFT",  5, 0)
    bar.Text:SetPoint("RIGHT", bar, "RIGHT", (cfg.showTime ~= false) and -55 or -5, 0)
    bar.Text:SetJustifyH("LEFT")
    bar.Text:SetWordWrap(false)
    if cfg.showSpellName == false then bar.Text:Hide() end

    -- Time
    bar.Time = mkFont(bar, cfg.fontSize)
    bar.Time:SetPoint("RIGHT", bar, "RIGHT", -2, 0)
    bar.Time:SetJustifyH("RIGHT")
    if cfg.showTime == false then bar.Time:Hide() end

    -- Shield (target only)
    if unit == "target" then
        bar.Shield = bar:CreateTexture(nil, "OVERLAY", nil, -5)
        bar.Shield:SetTexture([[Interface\CastingBar\UI-CastingBar-Small-Shield]])
        bar.Shield:SetSize(18, 18)
        bar.Shield:SetPoint("RIGHT", bar, "LEFT", -3, 0)
        bar.Shield:Hide()
    end

    -- Safezone (player only)
    if unit == "player" and cfg.latency then
        bar.SafeZone = bar:CreateTexture(nil, "OVERLAY")
        bar.SafeZone:SetTexture(MEDIA .. "statusbar3")
        bar.SafeZone:SetVertexColor(0.6, 0, 0, 0.6)
        bar.SafeZone:SetPoint("TOPRIGHT",    bar, "TOPRIGHT",    0, 0)
        bar.SafeZone:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 0, 0)
        bar.SafeZone:SetWidth(1)
    end

    -- State
    bar.unit         = unit
    bar.cfg          = cfg
    bar.casting      = false
    bar.channeling   = false
    bar.empowering   = false
    bar.holdTime     = 0
    bar.failHold     = 0
    bar.successHold  = 0
    bar.duration     = 0
    bar.max          = 0
    bar.delay        = 0
    bar.startTime    = 0
    bar.stagePoints  = {}
    bar.castID       = nil
    bar.spellID      = nil
    bar.fadingIn     = false
    bar.fadingOut    = false
    bar.fadeTime     = 0
    bar.fadeTarget   = 0

    bar:SetScript("OnUpdate", onUpdate)
    bar:SetScript("OnShow", function(self) self:GetParent():EnableMouse(true) end)
    bar:SetScript("OnHide", function(self)
        local cont = self:GetParent()
        -- Never disable mouse mid-drag: OnDragStop would never fire -> mouse locks up
        if not cont.dragging then
            cont:EnableMouse(false)
        end
    end)
    bar:Hide()
    return bar, container
end

-- Event handlers

local function onCastStart(bar, event, unit)
    if bar.unit ~= unit then return end

    local name, text, texture, startTime, endTime
    local isTradeSkill, castID, notInterruptible, spellID, numStages

    name, text, texture, startTime, endTime,
        isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(unit)

    if name then
        bar.casting = true; bar.channeling = false; bar.empowering = false
    else
        name, text, texture, startTime, endTime,
            isTradeSkill, notInterruptible, spellID, _, numStages = UnitChannelInfo(unit)
        if name then
            if numStages and numStages > 0 then
                bar.empowering = true; bar.channeling = false; bar.casting = false
                endTime = endTime + (GetUnitEmpowerHoldAtMaxTime(unit) or 0)
            else
                bar.channeling = true; bar.empowering = false; bar.casting = false
            end
        end
    end

    if not name then bar:Hide(); return end

    endTime   = endTime   / 1000
    startTime = startTime / 1000

    bar.max          = endTime - startTime
    bar.startTime    = startTime
    bar.delay        = 0
    bar.holdTime     = 0
    bar.failHold     = 0
    bar.successHold  = 0
    bar.castID       = castID
    bar.spellID      = spellID
    bar.notInterruptible = notInterruptible and true or false
    wipe(bar.stagePoints)

    bar.duration = bar.channeling
        and (endTime - GetTime()) or (GetTime() - startTime)
    if bar.duration < 0 then bar.duration = 0 end

    bar:SetMinMaxValues(0, bar.max)
    bar:SetValue(bar.duration)

    -- Spell name
    if bar.Text then
        if bar.cfg.showSpellName ~= false then
            bar.Text:SetText(text or name)
            bar.Text:Show()
        else
            bar.Text:SetText("")
            bar.Text:Hide()
        end
    end

    -- Time
    if bar.Time then
        if bar.cfg.showTime ~= false then
            bar.Time:SetText("")
            bar.Time:Show()
        else
            bar.Time:Hide()
        end
    end

    if bar.Spark then bar.Spark:Show() end

    -- Icon
    if bar.Icon then
        if texture then bar.Icon:SetTexture(texture) end
        if bar.cfg.showIcon then bar.Icon:Show() else bar.Icon:Hide() end
    end

    -- Dynamic Width (only when spell name is visible)
    if bar.cfg.dynamicWidth and bar.Text and bar.cfg.showSpellName ~= false then
        C_Timer.After(0, function()
            if not bar.Text then return end
            local tw = bar.Text:GetStringWidth()
            if tw and tw > 0 then
                local pad  = bar.cfg.showTime ~= false and 70 or 20
                local newW = math.max(bar.cfg.width, math.min(tw + pad, 500))
                bar:SetWidth(newW)
                bar:UpdateArtwork(newW)
                local cont = _G["rCastbar_" .. (bar.unit:sub(1,1):upper()..bar.unit:sub(2)) .. "_Container"]
                if cont then cont:SetWidth(newW + 20) end
            end
        end)
    end

    -- Shield
    if bar.Shield then bar.Shield:SetShown(notInterruptible and true or false) end

    -- Colors
    applyBarColor(bar)
    if bar.cfg.shieldColor then applyShieldColor(bar, notInterruptible) end

    -- Safezone
    if bar.SafeZone and bar.max > 0 then
        local _, _, _, latencyMS = GetNetStats()
        latencyMS = latencyMS or 0
        local ratio = (latencyMS / 1000) / bar.max
        if ratio > 1 then ratio = 1 end
        bar.SafeZone:SetWidth(max(1, bar:GetWidth() * ratio))
    end

    bar:Show()
    fadeIn(bar)
end

local function onCastSucceeded(bar, event, unit, castID, spellID)
    if bar.unit ~= unit then return end
    if not bar:IsShown() then return end
    if castID ~= nil and bar.castID ~= nil and bar.castID ~= castID then return end
    if bar.channeling or bar.empowering then return end
    showSuccess(bar)
end

local function onCastUpdate(bar, event, unit, castID, spellID)
    if bar.unit ~= unit then return end
    if not bar:IsShown() then return end
    if castID ~= nil and bar.castID ~= nil and bar.castID ~= castID then return end

    local name, _, _, startTime, endTime
    if event == "UNIT_SPELLCAST_DELAYED" then
        name, _, _, startTime, endTime = UnitCastingInfo(unit)
    else
        name, _, _, startTime, endTime = UnitChannelInfo(unit)
    end
    if not name then return end

    if bar.empowering then
        endTime = endTime + (GetUnitEmpowerHoldAtMaxTime(unit) or 0)
    end

    endTime   = endTime   / 1000
    startTime = startTime / 1000

    local delta
    if bar.channeling or bar.empowering then
        delta = bar.startTime - startTime; bar.duration = endTime - GetTime()
    else
        delta = startTime - bar.startTime; bar.duration = GetTime() - startTime
    end
    if delta < 0 then delta = 0 end
    if bar.duration < 0 then bar.duration = 0 end

    bar.max       = endTime - startTime
    bar.startTime = startTime
    bar.delay     = (bar.delay or 0) + delta

    bar:SetMinMaxValues(0, bar.max)
    bar:SetValue(bar.duration)
end

local function onCastStop(bar, event, unit, castID, spellID)
    if bar.unit ~= unit then return end
    if not bar:IsShown() then return end
    if castID ~= nil and bar.castID ~= nil and bar.castID ~= castID then return end

    if event == "UNIT_SPELLCAST_CHANNEL_STOP" then showSuccess(bar); return end
    if event == "UNIT_SPELLCAST_EMPOWER_STOP"  then showSuccess(bar); return end

    -- Ignore STOP if success-flash or fail-hold is already running
    if (bar.successHold and bar.successHold > 0) or (bar.failHold and bar.failHold > 0) then return end

    resetBarState(bar)
    bar:SetMinMaxValues(0, 1)
    bar:SetValue(0)
    fadeOut(bar)
end

local function onCastFail(bar, event, unit, castID, spellID)
    if bar.unit ~= unit then return end
    if not bar:IsShown() then return end
    if castID ~= nil and bar.castID ~= nil and bar.castID ~= castID then return end

    local fc = bar.cfg.failColor or { r=1, g=0, b=0, a=1 }
    bar:SetStatusBarColor(fc.r, fc.g, fc.b, fc.a)
    if bar.bg then bar.bg:SetVertexColor(fc.r*0.2, fc.g*0.2, fc.b*0.2, bar.cfg.bgColor.a) end

    if bar.Text and bar.cfg.showSpellName ~= false then
        bar.Text:SetText(
            event == "UNIT_SPELLCAST_FAILED"
            and (FAILED or "Failed") or (INTERRUPTED or "Interrupted"))
        bar.Text:Show()
    end
    if bar.Spark then bar.Spark:Hide() end

    local savedMax = bar.max
    bar.casting    = false
    bar.channeling = false
    bar.empowering = false
    bar.castID     = nil
    bar.successHold = 0
    bar:SetMinMaxValues(0, savedMax > 0 and savedMax or 1)
    bar:SetValue(savedMax > 0 and savedMax or 1)

    bar.failHold = bar.cfg.timeToHold or 0.5
end

local function onInterruptible(bar, event, unit)
    if bar.unit ~= unit then return end
    if not bar:IsShown() then return end
    bar.notInterruptible = (event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE")
    if bar.Shield then bar.Shield:SetShown(bar.notInterruptible) end
    if bar.cfg.shieldColor then applyShieldColor(bar, bar.notInterruptible) end
end

-- Event dispatch

local eventDispatch = {
    UNIT_SPELLCAST_START             = onCastStart,
    UNIT_SPELLCAST_CHANNEL_START     = onCastStart,
    UNIT_SPELLCAST_EMPOWER_START     = onCastStart,
    UNIT_SPELLCAST_SUCCEEDED         = onCastSucceeded,
    UNIT_SPELLCAST_STOP              = onCastStop,
    UNIT_SPELLCAST_CHANNEL_STOP      = onCastStop,
    UNIT_SPELLCAST_EMPOWER_STOP      = onCastStop,
    UNIT_SPELLCAST_DELAYED           = onCastUpdate,
    UNIT_SPELLCAST_CHANNEL_UPDATE    = onCastUpdate,
    UNIT_SPELLCAST_EMPOWER_UPDATE    = onCastUpdate,
    UNIT_SPELLCAST_FAILED            = onCastFail,
    UNIT_SPELLCAST_INTERRUPTED       = onCastFail,
    UNIT_SPELLCAST_INTERRUPTIBLE     = onInterruptible,
    UNIT_SPELLCAST_NOT_INTERRUPTIBLE = onInterruptible,
}

local castbars   = {}
local registered = false

local eventFrame = CreateFrame("Frame", "rCastbar_EventFrame", UIParent)
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")

eventFrame:SetScript("OnEvent", function(self, event, arg1, arg2, arg3)
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        L = rCastbar_L or {}

        if not rCastbarDB        then rCastbarDB = {}        end
        if not rCastbarDB.player then rCastbarDB.player = {} end
        if not rCastbarDB.target then rCastbarDB.target = {} end
        deepMerge(rCastbarDB.player, defaults.player)
        deepMerge(rCastbarDB.target, defaults.target)

        if rCastbarDB.player.enabled then
            local bar, cont = buildCastbar("player", rCastbarDB.player)
            castbars.player = { bar = bar, container = cont }
        end
        if rCastbarDB.target.enabled then
            local bar, cont = buildCastbar("target", rCastbarDB.target)
            castbars.target = { bar = bar, container = cont }
        end
        return
    end

    if event == "PLAYER_ENTERING_WORLD" then
        suppressBlizzardCastbars()
        if not registered then
            registered = true
            local unitList = {}
            for u in pairs(castbars) do unitList[#unitList + 1] = u end
            if #unitList > 0 then
                for ev in pairs(eventDispatch) do
                    if #unitList == 1 then self:RegisterUnitEvent(ev, unitList[1])
                    else self:RegisterUnitEvent(ev, unitList[1], unitList[2]) end
                end
            end
        end
        for unit, entry in pairs(castbars) do
            onCastStart(entry.bar, "ForceUpdate", unit)
        end
        return
    end

    local handler = eventDispatch[event]
    if not handler then return end
    local unit = arg1
    for _, entry in pairs(castbars) do
        handler(entry.bar, event, unit, arg2, arg3)
    end
end)

-- Preview API (used by GUI edit mode)
function rCastbar_ShowPreview(show)
    for unit, entry in pairs(castbars) do
        local bar  = entry.bar
        local cont = entry.container
        if show then
            -- Show bar in a static preview state
            bar.previewMode = true
            applyBarColor(bar)
            bar:SetMinMaxValues(0, 1)
            bar:SetValue(0.65)
            if bar.Text then
                if bar.cfg.showSpellName ~= false then
                    bar.Text:SetText("Preview")
                    bar.Text:Show()
                end
            end
            if bar.Time then
                if bar.cfg.showTime ~= false then
                    bar.Time:SetText("1.5")
                    bar.Time:Show()
                end
            end
            if bar.Icon and bar.cfg.showIcon then
                bar.Icon:SetTexture(237017)
                bar.Icon:Show()
            end
            if bar.Spark then
                bar.Spark:ClearAllPoints()
                bar.Spark:SetPoint("CENTER", bar:GetStatusBarTexture(), "RIGHT", 0, 0)
                bar.Spark:Show()
            end
            bar:SetAlpha(1)
            bar:Show()  -- triggers OnShow -> container:EnableMouse(true)
        else
            bar.previewMode = false
            -- Only hide if no real cast is running
            if not bar.casting and not bar.channeling and not bar.empowering
               and not (bar.successHold and bar.successHold > 0)
               and not (bar.failHold   and bar.failHold   > 0)
               and not bar.fadingOut then
                bar:Hide()  -- triggers OnHide -> container:EnableMouse(false) if not dragging
            end
        end
    end
end
