-- =============================================================
--  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
    bar.duration_obj     = nil
    bar._useTimerDuration = false
    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
        bar._useTimerDuration = false
        bar.duration_obj = nil
        resetBarState(bar)
        bar:SetMinMaxValues(0, 1)
        bar:SetValue(0)
        fadeOut(bar)
        return
    end
    bar:SetReverseFill(false)
    bar._useTimerDuration = false
    bar.duration_obj = nil
    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

    -- Bar fill: SetTimerDuration aktualisiert intern; v5.9 braucht SetValue(nowMs)
    if self._useTimerDuration then
        -- Bar wird von StatusBar intern aktualisiert, nur Duration fuer Time-Text
        if self.duration_obj then
            local ok_e, elapsed = pcall(self.duration_obj.GetElapsedDuration, self.duration_obj, 0)
            if ok_e and elapsed and not (issecretvalue and issecretvalue(elapsed)) then
                self.duration = elapsed
            end
        end
    else
        -- v5.9: SetValue(nowMs) mit MinMax(startMs,endMs)
        local nowMs = GetTime() * 1000
        if Enum.StatusBarInterpolation then
            self:SetValue(nowMs, Enum.StatusBarInterpolation.ExponentialEaseOut)
        else
            self:SetValue(nowMs)
        end
        if self.duration_obj then
            local ok_e, elapsed = pcall(self.duration_obj.GetElapsedDuration, self.duration_obj, 0)
            if ok_e and elapsed and not (issecretvalue and issecretvalue(elapsed)) then
                self.duration = elapsed
            end
        end
    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

    -- Safety hide: Cast abgebrochen (z.B. durch Bewegung) – UnitCastingInfo nil
    -- Nur bei normalen Casts, nicht Channel/Empowered (nil zwischen Ticks moeglich)
    self._safetyAccum = (self._safetyAccum or 0) + elapsed
    if self._safetyAccum >= 0.1 then
        self._safetyAccum = 0
        if self.casting and not self.empowering and not self.channeling then
            if not UnitCastingInfo(self.unit) then
                resetBarState(self)
                fadeOut(self)
            end
        end
    end
end

-- Event dispatch (vor buildCastbar, damit Closure es findet)
local eventDispatch

-- 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.duration_obj = nil
    bar._useTimerDuration = false
    bar._safetyAccum = 0

    -- Unit-Events direkt auf der Bar registrieren (wie nCastbar) – funktioniert zuverlaessig fuer NPC-Target
    local events = {
        "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_FAILED",
        "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_INTERRUPTED",
        "UNIT_SPELLCAST_DELAYED", "UNIT_SPELLCAST_CHANNEL_START", "UNIT_SPELLCAST_CHANNEL_STOP",
        "UNIT_SPELLCAST_CHANNEL_UPDATE", "UNIT_SPELLCAST_EMPOWER_START",
        "UNIT_SPELLCAST_EMPOWER_UPDATE", "UNIT_SPELLCAST_EMPOWER_STOP",
        "UNIT_SPELLCAST_INTERRUPTIBLE", "UNIT_SPELLCAST_NOT_INTERRUPTIBLE",
    }
    for _, ev in ipairs(events) do
        bar:RegisterUnitEvent(ev, unit)
    end
    bar:SetScript("OnEvent", function(self, event, ...)
        local handler = eventDispatch[event]
        if handler then handler(self, event, unit, ...) end
    end)

    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, startMs, endMs
    local isTradeSkill, castID, notInterruptible, spellID, numStages

    name, text, texture, startMs, endMs,
        isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(unit)

    if name then
        bar.casting = true; bar.channeling = false; bar.empowering = false
    else
        name, text, texture, startMs, endMs,
            isTradeSkill, notInterruptible, spellID, _, numStages = UnitChannelInfo(unit)
        if name then
            if numStages and numStages > 0 then
                bar.empowering = true; bar.channeling = false; bar.casting = false
                -- UnitEmpoweredChannelDuration inkl. hold-at-max per Default
            else
                bar.channeling = true; bar.empowering = false; bar.casting = false
            end
        end
    end

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

    -- Retail: startMs/endMs sind "secret values" – keine Arithmetik in Lua.
    -- Hybrid: SetTimerDuration wenn duration_obj vorhanden, sonst v5.9 (direkt an C uebergeben).
    local durationObj
    if bar.empowering and UnitEmpoweredChannelDuration then
        durationObj = UnitEmpoweredChannelDuration(unit)
    elseif bar.channeling and UnitChannelDuration then
        durationObj = UnitChannelDuration(unit)
    elseif UnitCastingDuration then
        durationObj = UnitCastingDuration(unit)
    end
    bar.duration_obj = durationObj

    bar.delay        = 0
    bar.holdTime     = 0
    bar.failHold     = 0
    bar.successHold  = 0
    bar.castID       = (issecretvalue and issecretvalue(castID)) and nil or castID
    bar.spellID      = spellID
    if issecretvalue and issecretvalue(notInterruptible) then
        bar.notInterruptible = false
    else
        bar.notInterruptible = notInterruptible and true or false
    end
    wipe(bar.stagePoints)

    bar:SetReverseFill(bar.channeling)

    if durationObj and bar.SetTimerDuration then
        bar._useTimerDuration = true
        local direction = bar.channeling and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime
        bar:SetTimerDuration(durationObj, Enum.StatusBarInterpolation.Immediate, direction)
        local ok, total = pcall(durationObj.GetTotalDuration, durationObj, 0)
        bar.max = (ok and total and not (issecretvalue and issecretvalue(total)) and total > 0) and total or 1
        bar.duration = 0
    else
        bar._useTimerDuration = false
        bar:SetMinMaxValues(startMs, endMs)
        bar:SetValue(GetTime() * 1000)
        bar.max = 1
        bar.duration = 0
    end

    -- 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 and not (issecretvalue and issecretvalue(tw)) 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 (notInterruptible kann secret value sein)
    if bar.Shield then
        local showShield = bar.notInterruptible
        bar.Shield:SetShown(showShield)
    end

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

    -- Safezone (player only, braucht echte Cast-Dauer von duration_obj)
    if bar.SafeZone and bar.duration_obj then
        local ok, total = pcall(bar.duration_obj.GetTotalDuration, bar.duration_obj, 0)
        if ok and total and not (issecretvalue and issecretvalue(total)) and total > 0 then
            local _, _, _, latencyMS = GetNetStats()
            latencyMS = latencyMS or 0
            local ratio = (latencyMS / 1000) / total
            if ratio > 1 then ratio = 1 end
            bar.SafeZone:SetWidth(max(1, bar:GetWidth() * ratio))
        end
    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 and bar.castID and not (issecretvalue and (issecretvalue(castID) or issecretvalue(bar.castID))) 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 and bar.castID and not (issecretvalue and (issecretvalue(castID) or issecretvalue(bar.castID))) and bar.castID ~= castID then return end

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

    local durationObj
    if bar.empowering and UnitEmpoweredChannelDuration then
        durationObj = UnitEmpoweredChannelDuration(unit)
    elseif bar.channeling and UnitChannelDuration then
        durationObj = UnitChannelDuration(unit)
    else
        durationObj = UnitCastingDuration and UnitCastingDuration(unit)
    end
    bar.duration_obj = durationObj

    if durationObj and bar.SetTimerDuration then
        bar._useTimerDuration = true
        local direction = bar.channeling and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime
        bar:SetTimerDuration(durationObj, Enum.StatusBarInterpolation.Immediate, direction)
        local ok, total = pcall(durationObj.GetTotalDuration, durationObj, 0)
        bar.max = (ok and total and not (issecretvalue and issecretvalue(total)) and total > 0) and total or 1
    else
        bar._useTimerDuration = false
        bar:SetMinMaxValues(startMs, endMs)
        bar:SetValue(GetTime() * 1000)
    end
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 and bar.castID and not (issecretvalue and (issecretvalue(castID) or issecretvalue(bar.castID))) 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

    bar._useTimerDuration = false
    bar.duration_obj = nil
    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 and bar.castID and not (issecretvalue and (issecretvalue(castID) or issecretvalue(bar.castID))) 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._useTimerDuration = false
    bar.duration_obj = nil
    bar:SetReverseFill(false)
    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

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:RegisterEvent("PLAYER_TARGET_CHANGED")

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()
        registered = true
        for unit, entry in pairs(castbars) do
            onCastStart(entry.bar, "ForceUpdate", unit)
        end
        return
    end

    if event == "PLAYER_TARGET_CHANGED" then
        local entry = castbars.target
        if entry then
            resetBarState(entry.bar)
            entry.bar:Hide()
            if UnitExists("target") then
                -- Deferred: Target-Daten sind evtl. erst im naechsten Frame verfuegbar (NPC mid-cast)
                C_Timer.After(0, function()
                    if entry and entry.bar and UnitExists("target") then
                        onCastStart(entry.bar, "ForceUpdate", "target")
                    end
                end)
            end
        end
        return
    end

    -- Spell-Events werden von jeder Bar direkt empfangen (RegisterUnitEvent auf Bar)
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
