local _, ns = ...

local Castbar = {}
ns.Castbar = Castbar

Castbar.frames         = {}
Castbar.anchors        = {}
Castbar.selectedAnchor = nil

local SNAP_GRID     = 10
local snapDragGrid  = nil

local function SnapCoordToGrid(v, grid)
    grid = grid or SNAP_GRID
    return math.floor(v / grid + 0.5) * grid
end

local function ShowSnapDragGrid()
    if snapDragGrid then return end
    snapDragGrid = CreateFrame("Frame", nil, UIParent)
    snapDragGrid:SetAllPoints(UIParent)
    snapDragGrid:SetFrameStrata("BACKGROUND")
    local scale = 64
    local screenW = GetScreenWidth()
    local screenH = GetScreenHeight()
    local numV = scale
    local numH = math.floor(screenH / screenW * scale)
    local offV = screenW / numV
    local offH = screenH / numH
    for i = 1, numV - 1 do
        local line = snapDragGrid:CreateLine()
        line:SetThickness(1)
        line:SetColorTexture(1, 1, 1, 0.3)
        line:SetStartPoint("TOPLEFT", i * offV, 0)
        line:SetEndPoint("BOTTOMLEFT", i * offV, 0)
    end
    for i = 1, numH - 1 do
        local line = snapDragGrid:CreateLine()
        line:SetThickness(1)
        line:SetColorTexture(1, 1, 1, 0.3)
        line:SetStartPoint("BOTTOMLEFT", 0, i * offH)
        line:SetEndPoint("BOTTOMRIGHT", 0, i * offH)
    end
    snapDragGrid:Show()
end

local function HideSnapDragGrid()
    if snapDragGrid then
        snapDragGrid:Hide()
        snapDragGrid = nil
    end
end

-- ============================================================
-- Helpers
-- ============================================================
local function CreateBorder(frame, size)
    frame:CreateBeautyBorder(size or 11)
    frame:SetBeautyBorderPadding(2.66)
end

local function ResolveTexture(config)
    local texName = config and config.statusbarTexture
    if texName == "smooth_v2" then
        return "Interface\\AddOns\\nCastbar\\Media\\smooth_v2"
    end
    if texName then
        local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
        if LSM then
            local path = LSM:Fetch("statusbar", texName)
            if path then return path end
        end
    end
    return "Interface\\AddOns\\nCastbar\\Media\\smooth_v2"
end

-- ============================================================
-- Empower stage tick helpers
-- ============================================================
local hide_castbar -- Forward-Deklaration, damit onUpdate die lokale Funktion nutzt

local function hide_empower_stages(castbar)
    if castbar.empTickPool then
        for _, t in ipairs(castbar.empTickPool) do
            t:Hide()
        end
    end
    castbar.numActiveTicks = 0
end

local function to_roman(num)
    local map = {
        {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
        {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
        {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
    }
    local n = math.max(1, math.floor(num or 1))
    local out = {}
    for i = 1, #map do
        local v, s = map[i][1], map[i][2]
        while n >= v do
            n = n - v
            out[#out + 1] = s
        end
    end
    return table.concat(out)
end

local function get_empower_progress(castbar)
    if castbar.duration_obj then
        local ok_e, elapsed = pcall(castbar.duration_obj.GetElapsedDuration, castbar.duration_obj, 0)
        local ok_t, total   = pcall(castbar.duration_obj.GetTotalDuration, castbar.duration_obj, 0)
        if ok_e and ok_t and elapsed and total and total > 0 then
            return math.min(math.max(elapsed / total, 0), 1)
        end
    end
    local minV, maxV = castbar:GetMinMaxValues()
    local val = castbar:GetValue()
    if type(minV) == "number" and type(maxV) == "number" and type(val) == "number" and maxV > minV then
        return math.min(math.max((val - minV) / (maxV - minV), 0), 1)
    end
    return 0
end

local function update_empower_stage_visuals(castbar)
    if not castbar.isEmpowered or not castbar.empStageThresholds or not castbar.empTickPool then return end
    local progress = get_empower_progress(castbar)
    for i, fs in ipairs(castbar.empTickPool) do
        local reached = progress >= (castbar.empStageThresholds[i] or 1)
        fs:SetTextColor(1, 1, 1, reached and 1 or 0) -- 0% until stage reached
        fs:Show()
    end
end

local function setup_empower_stages(castbar)
    local unit = castbar.unit
    if not UnitEmpoweredStagePercentages then return end

    local rawStages = UnitEmpoweredStagePercentages(unit, true)
    if not rawStages or #rawStages == 0 then return end

    local thresholds = {}
    local cumSum = 0
    for i = 1, #rawStages do
        cumSum = cumSum + rawStages[i]
        thresholds[i] = math.min(cumSum, 1.0)
    end

    local numStages = #thresholds
    if numStages < 1 then return end

    if not castbar.empTickPool then
        castbar.empTickPool = {}
    end
    castbar.empStageThresholds = castbar.empStageThresholds or {}

    hide_empower_stages(castbar)

    local barWidth  = castbar:GetWidth()

    for i = 1, numStages do
        local fs = castbar.empTickPool[i]
        if not fs then
            fs = castbar:CreateFontString(nil, "OVERLAY")
            fs:SetFont("Fonts\\FRIZQT__.TTF", math.max(12, math.floor((castbar:GetHeight() or 15) * 0.9)), "OUTLINE")
            fs:SetShadowColor(0, 0, 0, 1)
            fs:SetShadowOffset(1, -1)
            castbar.empTickPool[i] = fs
        end
        local prev = (i == 1) and 0 or (thresholds[i - 1] or 0)
        local cur  = thresholds[i] or 1
        local center = ((prev + cur) * 0.5) * barWidth
        fs:ClearAllPoints()
        fs:SetPoint("CENTER", castbar, "LEFT", center, 0)
        fs:SetText(to_roman(i))
        fs:SetTextColor(1, 1, 1, 0)
        castbar.empStageThresholds[i] = cur
    end

    for i = numStages + 1, #castbar.empTickPool do
        castbar.empTickPool[i]:Hide()
        castbar.empStageThresholds[i] = nil
    end

    castbar.numActiveTicks = numStages
    update_empower_stage_visuals(castbar)
end

-- ============================================================
-- Anchor (draggable positioning frame)
-- ============================================================
local function SetAnchorSelected(self, selected)
    self._selected = selected
    if selected then
        Castbar.selectedAnchor = self
    elseif Castbar.selectedAnchor == self then
        Castbar.selectedAnchor = nil
    end
    if not self.text then return end
    if selected then
        -- Deutliche Hervorhebung: grüner Text
        self.text:SetTextColor(0, 1, 0)
    else
        -- Standard UI-Gelb
        self.text:SetTextColor(1, 0.82, 0)
    end
end

local function UpdateAnchorPositionDisplay(self, point, relPoint, x, y)
    if not self.xText or not self.yText then return end
    self._point    = point
    self._relPoint = relPoint
    self._x        = x
    self._y        = y
    local xVal = string.format("%.0f", x or 0)
    local yVal = string.format("%.0f", y or 0)
    self.xText:SetFormattedText("X = %s", xVal)
    self.yText:SetFormattedText("Y = %s", yVal)
end

function Castbar:NudgeSelected(dx, dy)
    local a = self.selectedAnchor
    if not a or not a:IsShown() then return end
    local point, _, relPoint, x, y = a:GetPoint()
    x, y = (x or 0) + dx, (y or 0) + dy
    local prof = nCastbarDB and nCastbarDB.profile
    if prof and prof.snapToGrid then
        x = SnapCoordToGrid(x)
        y = SnapCoordToGrid(y)
    end
    a:ClearAllPoints()
    a:SetPoint(point or "CENTER", UIParent, relPoint or "CENTER", x, y)
    UpdateAnchorPositionDisplay(a, point or "CENTER", relPoint or "CENTER", x, y)
    if a.config then
        a.config.position = {point or "CENTER", "UIParent", relPoint or "CENTER", x, y}
    end
end

function Castbar:CreateAnchor(unit, label)
    local config = nCastbarDB.profile[unit]
    if not config then return end
    local anchor = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    anchor:SetSize((config.width + config.height) * config.scale, config.height * config.scale)
    anchor:SetScale(1.193)
    anchor:SetPoint(unpack(config.position or {"CENTER"}))
    anchor:SetFrameStrata("HIGH")
    anchor:SetMovable(true)
    anchor:SetClampedToScreen(true)
    anchor:SetUserPlaced(true)
    anchor:SetBackdrop({ bgFile = "Interface\\MINIMAP\\TooltipBackdrop-Background" })
    CreateBorder(anchor, 11)
    anchor:EnableMouse(true)
    anchor:RegisterForDrag("LeftButton")
    anchor:Hide()
    anchor.text = anchor:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    -- Name der Castbar oberhalb des Rahmens, mittig
    anchor.text:SetPoint("BOTTOM", anchor, "TOP", 0, 4)
    anchor.text:SetText(label .. " Castbar")
    anchor.text:SetTextColor(1, 0.82, 0)

    -- Koordinaten-Anzeige (X/Y) – gut lesbar, unter der Castbar, zentriert
    local xText = anchor:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    xText:SetPoint("TOP", anchor, "BOTTOM", 0, -2)
    xText:SetTextColor(0.2, 0.9, 1) -- helles Cyan für gute Lesbarkeit

    local yText = anchor:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    yText:SetPoint("TOP", xText, "BOTTOM", 0, -2)
    yText:SetTextColor(0.2, 0.9, 1)

    anchor.xText  = xText
    anchor.yText  = yText
    anchor.config = config
    anchor.unit   = unit
    anchor._snapDragActive = false

    anchor:SetScript("OnUpdate", function(self)
        if not self._snapDragActive then return end
        local prof = nCastbarDB and nCastbarDB.profile
        if not (prof and prof.snapToGrid and self._movePoint and self._moveRel) then return end
        local uisc = UIParent:GetEffectiveScale()
        local cx, cy = GetCursorPosition()
        cx, cy = cx / uisc, cy / uisc
        local nx = SnapCoordToGrid(self._moveStartAX + (cx - self._moveStartCX))
        local ny = SnapCoordToGrid(self._moveStartAY + (cy - self._moveStartCY))
        self:ClearAllPoints()
        self:SetPoint(self._movePoint, self._moveRel, self._moveRelPoint, nx, ny)
    end)

    anchor:SetScript("OnDragStart", function(self)
        local prof = nCastbarDB and nCastbarDB.profile
        if prof and prof.snapToGrid then
            local p, rt, rp, ax, ay = self:GetPoint(1)
            if p and ax ~= nil and ay ~= nil then
                self._snapDragActive = true
                self._moveRel = rt or UIParent
                self._movePoint = p
                self._moveRelPoint = rp
                self._moveStartAX, self._moveStartAY = ax, ay
                local uisc = UIParent:GetEffectiveScale()
                local cx, cy = GetCursorPosition()
                self._moveStartCX = cx / uisc
                self._moveStartCY = cy / uisc
                ShowSnapDragGrid()
                return
            end
        end
        self._snapDragActive = false
        if prof and prof.snapToGrid then ShowSnapDragGrid() end
        self:StartMoving()
    end)
    anchor:SetScript("OnDragStop", function(self)
        local wasSnap = self._snapDragActive
        self._snapDragActive = false
        self:StopMovingOrSizing()
        if wasSnap then
            HideSnapDragGrid()
            local point, _, relPoint, x, y = self:GetPoint()
            UpdateAnchorPositionDisplay(self, point, relPoint, x or 0, y or 0)
            if self.config then
                self.config.position = {point, "UIParent", relPoint, x or 0, y or 0}
            end
        elseif nCastbarDB and nCastbarDB.profile and nCastbarDB.profile.snapToGrid then
            HideSnapDragGrid()
            C_Timer.After(0, function()
                if not self or not self.GetPoint then return end
                local p, rt, rp, x, y = self:GetPoint(1)
                if not (p and x ~= nil and y ~= nil) then return end
                local rel = rt or UIParent
                local nx, ny = SnapCoordToGrid(x), SnapCoordToGrid(y)
                if nx ~= x or ny ~= y then
                    self:ClearAllPoints()
                    self:SetPoint(p, rel, rp, nx, ny)
                end
                local point, _, relPoint, ox, oy = self:GetPoint()
                UpdateAnchorPositionDisplay(self, point, relPoint, ox or 0, oy or 0)
                if self.config then
                    self.config.position = {point, "UIParent", relPoint, ox or 0, oy or 0}
                end
            end)
        else
            HideSnapDragGrid()
            local point, _, relPoint, x, y = self:GetPoint()
            UpdateAnchorPositionDisplay(self, point, relPoint, x or 0, y or 0)
            if self.config then
                self.config.position = {point, "UIParent", relPoint, x or 0, y or 0}
            end
        end
    end)

    -- Tastatursteuerung: Pfeiltasten zum feinen Verschieben im Anchor-Modus
    -- Auswahl & Tooltip (DE/EN) beim Anklicken/Berühren
    anchor:SetScript("OnMouseDown", function(self)
        for _, a in pairs(Castbar.anchors) do
            SetAnchorSelected(a, a == self)
        end
    end)
    anchor:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText(
            "Pfeiltasten: 1px, Shift+Pfeiltasten: 5px\n" ..
            "Arrow keys: 1px, Shift+arrows: 5px",
            1, 1, 1
        )
        GameTooltip:AddLine("X/Y: Koordinaten relativ zu UIParent", 0.8, 0.8, 0.8)
        GameTooltip:AddLine("X/Y: coordinates relative to UIParent", 0.8, 0.8, 0.8)
        GameTooltip:Show()
    end)
    anchor:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    self.anchors[unit] = anchor

    local point, _, relPoint, x, y = anchor:GetPoint()
    UpdateAnchorPositionDisplay(anchor, point, relPoint, x or 0, y or 0)

    return anchor
end

-- ============================================================
-- onUpdate – runs every rendered frame (replaces the 50fps ticker)
--
-- Bar position (SetValue) is updated every frame for silky-smooth
-- movement, identical to rcastbar's OnUpdate approach.
-- Time text and the safety-hide check are throttled (pcall + API
-- overhead is wasted at 144fps) but still feel instantaneous.
-- ============================================================
local function onUpdate(self, elapsed)
    local now   = GetTime()
    local nowMs = now * 1000

    -- Fail-hold: bar stays visible after interrupt/fail
    if self.failstart then
        if now - self.failstart > 1 then
            hide_castbar(self)
        end
        return
    end

    -- Success-hold: green flash after successful cast
    if self.successstart then
        if now - self.successstart > 0.35 then
            self.successstart = nil
            hide_castbar(self)
        end
        return
    end

    -- Fade-out animation running: don't touch SetValue
    if self.fadeOut and self.fadeOut:IsPlaying() then
        return
    end

    -- Bar position: Player nutzt SetTimerDuration (aktualisiert sich intern).
    -- Target/Focus nutzt v5.9: SetValue(nowMs) mit MinMax(startMs,endMs).
    if self.casting and not self._useTimerDuration then
        if Enum.StatusBarInterpolation then
            self:SetValue(nowMs, Enum.StatusBarInterpolation.ExponentialEaseOut)
        else
            self:SetValue(nowMs)
        end
    end

    -- ── Time text: throttled to ~20fps (pcall overhead) ──
    self._timeAccum = (self._timeAccum or 0) + elapsed
    if self._timeAccum >= 0.05 then
        self._timeAccum = 0
        if self.config.showTime ~= false and self.duration_obj then
            local ok_r, remaining = pcall(self.duration_obj.GetRemainingDuration, self.duration_obj, 0)
            local ok_t, total     = pcall(self.duration_obj.GetTotalDuration,     self.duration_obj, 0)
            if ok_r and ok_t and remaining and total then
                self.time:SetText(string.format("%.1f / %.1f", remaining, total))
            end
        end
    end

    if self.isEmpowered then
        update_empower_stage_visuals(self)
    end

    -- ── Safety hide: throttled to ~10fps (UnitCastingInfo API overhead) ──
    -- Skip for channeled/empowered casts; UnitChannelInfo can briefly return
    -- nil between ticks and would cause false hides.
    self._safetyAccum = (self._safetyAccum or 0) + elapsed
    if self._safetyAccum >= 0.1 then
        self._safetyAccum = 0
        if self.casting and not self.isEmpowered and not self.isChannel then
            if not UnitCastingInfo(self.unit) then
                hide_castbar(self)
            end
        end
    end
end

-- ============================================================
-- Create
-- ============================================================
function Castbar:Create(unit)
    local config = nCastbarDB.profile[unit]
    if not config or not config.enabled then return end

    -- Alten Frame aufraumen falls vorhanden (z.B. nach Reload)
    if self.frames[unit] then
        local old = self.frames[unit]
        old:UnregisterAllEvents()
        old:SetScript("OnEvent", nil)
        old:SetScript("OnUpdate", nil)
        old:Hide()
        if old.fadeOut then old.fadeOut:Stop() end
        self.frames[unit] = nil
    end

    if not self.anchors[unit] then
        self:CreateAnchor(unit, unit:gsub("^%l", string.upper))
    end

    local texturePath = ResolveTexture(config)

    local frameName = "nCastbar_Bar_" .. unit
    local isNew = (_G[frameName] == nil)
    local castbar = _G[frameName] or CreateFrame("StatusBar", frameName, UIParent)

    castbar:UnregisterAllEvents()
    castbar:SetScript("OnEvent", nil)
    castbar:SetScript("OnUpdate", nil)

    castbar:SetStatusBarTexture(texturePath)
    local tex = castbar:GetStatusBarTexture()
    if tex then
        tex:SetHorizTile(false)
        -- Verhindert "Jitter" bei fractional scaling (Pixel-Snapping)
        if tex.SetSnapToPixelGrid then tex:SetSnapToPixelGrid(false) end
        if tex.SetTexelSnappingBias then tex:SetTexelSnappingBias(0) end
    end
    castbar:SetSize(config.width, config.height)
    castbar:SetScale(config.scale)
    castbar:SetMinMaxValues(0, 1)
    castbar:SetValue(0)
    castbar:ClearAllPoints()
    if self.anchors[unit] then
        castbar:SetPoint("CENTER", self.anchors[unit])
    elseif config.position then
        castbar:SetPoint(unpack(config.position))
    end

    if isNew then
        castbar.bg = castbar:CreateTexture(nil, "BACKGROUND")
        castbar.bg:SetAllPoints(castbar)
        if castbar.bg.SetSnapToPixelGrid then castbar.bg:SetSnapToPixelGrid(false) end
        if castbar.bg.SetTexelSnappingBias then castbar.bg:SetTexelSnappingBias(0) end
        CreateBorder(castbar, 11)

        castbar.shieldOverlay = castbar:CreateTexture(nil, "ARTWORK")
        castbar.shieldOverlay:SetAllPoints(castbar)
        if castbar.shieldOverlay.SetSnapToPixelGrid then castbar.shieldOverlay:SetSnapToPixelGrid(false) end
        if castbar.shieldOverlay.SetTexelSnappingBias then castbar.shieldOverlay:SetTexelSnappingBias(0) end
        castbar.shieldOverlay:SetColorTexture(unpack(config.colors.nonInterruptible or {0.7, 0.7, 0.7}))

        local fontObjName = "nCastbar_Font_" .. unit
        if not _G[fontObjName] then _G[fontObjName] = CreateFont(fontObjName) end
        local spellFont = _G[fontObjName]
        castbar.text = castbar:CreateFontString(nil, "OVERLAY")
        castbar.text:SetFontObject(spellFont)
        castbar.text:SetWordWrap(false)
        castbar.time = castbar:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        castbar.time:SetJustifyH("RIGHT")

        if config.icon and config.icon.show then
            castbar.iconFrame = CreateFrame("Frame", nil, castbar)
            castbar.iconFrame:SetSize(config.height, config.height)
            castbar.icon = castbar.iconFrame:CreateTexture(nil, "ARTWORK")
            castbar.icon:SetAllPoints(castbar.iconFrame)
            castbar.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            if config.icon.positionOutside then
                castbar.iconFrame:CreateBeautyBorder(10)
                castbar.iconFrame:SetBeautyBorderPadding(2)
            end
        end

        if unit == "player" then
            if config.showSafezone then
                castbar.safezone = castbar:CreateTexture(nil, "OVERLAY")
                castbar.safezone:SetColorTexture(unpack(config.colors.safezone or {1,0,1,1}))
                castbar.safezone:SetPoint("TOPRIGHT")
                castbar.safezone:SetPoint("BOTTOMRIGHT")
            end
            if config.showLatency then
                castbar.latency = castbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                castbar.latency:SetPoint("RIGHT", castbar, "BOTTOMRIGHT", -2, -2)
                castbar.latency:SetTextColor(0.6, 0.6, 0.6, 1)
            end
        end

        castbar.fadeOut = castbar:CreateAnimationGroup()
        local anim = castbar.fadeOut:CreateAnimation("Alpha")
        anim:SetFromAlpha(1); anim:SetToAlpha(0); anim:SetDuration(0.3); anim:SetSmoothing("IN")
        castbar.fadeOut:SetScript("OnFinished", function(ag)
            local f = ag:GetParent()
            f:Hide()
            f:SetAlpha(1)
            f:SetReverseFill(false)
            f:SetMinMaxValues(0, 1)
            f:SetValue(0)
            if f.shieldOverlay then f.shieldOverlay:SetAlpha(0) end
            hide_empower_stages(f)
        end)
    end

    do
        local fontObjName = "nCastbar_Font_" .. unit
        if not _G[fontObjName] then _G[fontObjName] = CreateFont(fontObjName) end
        local spellFont = _G[fontObjName]
        local LSM      = LibStub and LibStub("LibSharedMedia-3.0", true)
        local fontSize = config.spellNameFontSize or 12
        local outline  = config.spellNameOutline or "OUTLINE"
        if outline == "NONE" then outline = "" end
        local fontName = config.spellNameFont or "Friz Quadrata TT"
        local fontPath = (LSM and LSM:Fetch("font", fontName))
                      or "Interface\\AddOns\\nCastbar\\Media\\FRIZQT__.ttf"
        spellFont:SetFont(fontPath, fontSize, outline)
        spellFont:SetTextColor(1, 1, 1, 1)
        if castbar.text then castbar.text:SetFontObject(spellFont) end
    end

    if castbar.iconFrame then
        castbar.iconFrame:ClearAllPoints()
        if config.icon and config.icon.show then
            castbar.iconFrame:Show()
            if config.icon.position == "LEFT" then
                if config.icon.positionOutside then
                    castbar.iconFrame:SetPoint("RIGHT", castbar, "LEFT", -8, 0)
                else
                    castbar.iconFrame:SetPoint("LEFT", castbar, "LEFT", 2, 0)
                end
            else
                if config.icon.positionOutside then
                    castbar.iconFrame:SetPoint("LEFT", castbar, "RIGHT", 8, 0)
                else
                    castbar.iconFrame:SetPoint("RIGHT", castbar, "RIGHT", -2, 0)
                end
            end
        else
            castbar.iconFrame:Hide()
        end
    end

    if castbar.text then
        self:UpdateTextPosition(castbar, config)
    end

    if castbar.bg then
        castbar.bg:SetColorTexture(0, 0, 0, 0.5)
    end
    if castbar.shieldOverlay then
        castbar.shieldOverlay:SetAlpha(0)
    end
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
    end

    -- State
    castbar.unit           = unit
    castbar.config         = config
    castbar.casting         = false
    castbar.isChannel       = false
    castbar.isEmpowered     = false
    castbar.failstart       = nil
    castbar.successstart    = nil
    castbar.duration_obj    = nil
    castbar.currentCastGUID = nil
    castbar.numActiveTicks  = 0
    castbar._timeAccum      = 0
    castbar._safetyAccum    = 0
    castbar:Hide()

    self.frames[unit] = castbar
    self:RegisterEvents(castbar)
    return castbar
end

-- ============================================================
-- UpdateTextPosition
-- ============================================================
function Castbar:UpdateTextPosition(castbar, config)
    castbar.text:ClearAllPoints()
    castbar.time:ClearAllPoints()
    local pos        = config.textPosition or "inside"
    local showTime   = config.showTime ~= false
    local iconInside = castbar.iconFrame
                   and config.icon
                   and config.icon.show
                   and not config.icon.positionOutside

    if pos == "above" then
        castbar.text:SetPoint("BOTTOMLEFT",  castbar, "TOPLEFT",  0, 2)
        castbar.text:SetPoint("BOTTOMRIGHT", castbar, "TOPRIGHT", 0, 2)
        castbar.text:SetJustifyH("CENTER")
        if showTime then
            castbar.time:SetPoint("LEFT", castbar.text, "RIGHT", 4, 0)
            castbar.time:Show()
        else castbar.time:Hide() end

    elseif pos == "below" then
        castbar.text:SetPoint("TOPLEFT",  castbar, "BOTTOMLEFT",  0, -2)
        castbar.text:SetPoint("TOPRIGHT", castbar, "BOTTOMRIGHT", 0, -2)
        castbar.text:SetJustifyH("CENTER")
        if showTime then
            castbar.time:SetPoint("LEFT", castbar.text, "RIGHT", 4, 0)
            castbar.time:Show()
        else castbar.time:Hide() end

    else -- inside
        if config.textAlignCenter then
            castbar.text:SetPoint("LEFT",  castbar, "LEFT",  4, 0)
            castbar.text:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
            castbar.text:SetJustifyH("CENTER")
            if showTime then
                castbar.time:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
                castbar.time:Show()
            else castbar.time:Hide() end
        else
            castbar.text:SetJustifyH("LEFT")
            if iconInside and config.icon.position == "LEFT" then
                castbar.text:SetPoint("LEFT", castbar.iconFrame, "RIGHT", 4, 0)
            else
                castbar.text:SetPoint("LEFT", castbar, "LEFT", 4, 0)
            end
            if showTime then
                if iconInside and config.icon.position == "RIGHT" then
                    castbar.text:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                    castbar.time:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                else
                    castbar.text:SetPoint("RIGHT", castbar, "CENTER", -10, 0)
                    castbar.time:SetPoint("RIGHT", castbar, "RIGHT",   -4, 0)
                end
                castbar.time:Show()
            else
                if iconInside and config.icon.position == "RIGHT" then
                    castbar.text:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                else
                    castbar.text:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
                end
                castbar.time:Hide()
            end
        end
    end
end

-- ============================================================
-- show_success
-- ============================================================
local function show_success(castbar)
    local config = castbar.config

    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
    end

    hide_empower_stages(castbar)

    if not config.castSuccess then
        castbar.casting      = false
        castbar.isEmpowered  = false
        castbar.duration_obj = nil
        castbar.failstart    = nil
        castbar.successstart = nil
        -- SetTimerDuration(nil) wird nicht unterstuetzt (bad argument) – nur Flag zuruecksetzen.
        castbar._useTimerDuration = nil
        if castbar.fadeOut then
            castbar.fadeOut:Play()
        else
            castbar:SetReverseFill(false)
            castbar:SetMinMaxValues(0, 1)
            castbar:SetValue(0)
            if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
            castbar:Hide()
        end
        return
    end

    castbar:SetReverseFill(false)
    castbar._useTimerDuration = nil
    castbar:SetMinMaxValues(0, 100)
    castbar:SetValue(100)

    local c = config.colors.success or {0.0, 0.8, 0.0}
    castbar:SetStatusBarColor(unpack(c))
    castbar.bg:SetColorTexture(c[1]*0.3, c[2]*0.3, c[3]*0.3, 0.5)

    if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end

    castbar.casting      = false
    castbar.isEmpowered  = false
    castbar.duration_obj = nil
    castbar.successstart = GetTime()

    castbar:Show()
end

-- ============================================================
-- hide_castbar
-- ============================================================
function hide_castbar(castbar)
    castbar.casting         = false
    castbar.isEmpowered     = false
    castbar.isChannel       = false
    castbar.failstart       = nil
    castbar.successstart    = nil
    castbar.duration_obj    = nil
    castbar.currentCastGUID = nil
    hide_empower_stages(castbar)
    castbar._useTimerDuration = nil

    if castbar.fadeOut then
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
        if castbar:IsShown() then
            castbar.fadeOut:Play()
        else
            castbar:SetReverseFill(false)
            castbar:SetMinMaxValues(0, 1)
            castbar:SetValue(0)
            if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        end
    else
        castbar:SetReverseFill(false)
        castbar:SetMinMaxValues(0, 1)
        castbar:SetValue(0)
        if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        castbar:Hide()
    end
end

-- ============================================================
-- UpdateCastColor
-- ============================================================
local function UpdateCastColor(castbar, channeling)
    local config = castbar.config
    local color
    if config.classColorCast then
        local ok, _, class = pcall(UnitClass, castbar.unit)
        if ok and class then
            if not (issecretvalue and issecretvalue(class)) then
                if RAID_CLASS_COLORS[class] then
                    local c = RAID_CLASS_COLORS[class]
                    color = {c.r, c.g, c.b}
                end
            end
        end
    end
    if not color then
        color = channeling and config.colors.channel or config.colors.cast
    end
    castbar:SetStatusBarColor(unpack(color))
    castbar.bg:SetColorTexture(0, 0, 0, 0.5)
end

-- ============================================================
-- check_casting
-- ============================================================
local function check_casting(castbar, event, interruptedBy, isEmpowered)
    local unit   = castbar.unit
    local config = castbar.config

    if not UnitExists(unit) then hide_castbar(castbar); return end

    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        hide_empower_stages(castbar)
        castbar:SetReverseFill(false)
        -- SetTimerDuration(nil) wird nicht unterstuetzt (bad argument) – nur Flag zuruecksetzen.
        castbar._useTimerDuration = nil
        castbar:SetMinMaxValues(0, 100)
        castbar:SetValue(100)
        castbar:SetStatusBarColor(unpack(config.colors.failed))
        castbar.bg:SetColorTexture(
            config.colors.failed[1]*0.3,
            config.colors.failed[2]*0.3,
            config.colors.failed[3]*0.3, 0.5)
        castbar.time:SetText(ns.L["INTERRUPTED"] or "Interrupted")
        castbar.text:SetText("")
        castbar.failstart    = GetTime()
        castbar.casting      = false
        castbar.isEmpowered  = false
        castbar.duration_obj = nil
        castbar.shieldOverlay:SetAlpha(0)
        if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
            castbar.fadeOut:Stop(); castbar:SetAlpha(1)
        end
        castbar:Show()
        return
    end

    local isChannel = false
    local name, _, texture, startMs, endMs, isTradeSkill, castID, notInterruptible, spellId
    name, _, texture, startMs, endMs, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(unit)
    if name then
        isChannel = not isEmpowered
    else
        name, _, texture, startMs, endMs, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(unit)
        isChannel = false
    end

    if not name then
        if castbar.isChannel then
            ns.Debug("check_casting: nil name (isChannel=true) – ignoriert (event=%s)", event)
            return
        end
        if not castbar.failstart and not castbar.successstart then
            hide_castbar(castbar)
        end
        return
    end

    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
        castbar:SetReverseFill(false)
        -- SetTimerDuration(nil) wird nicht unterstuetzt (bad argument) – nur Flag zuruecksetzen.
        castbar._useTimerDuration = nil
        castbar:SetMinMaxValues(0, 1)
        castbar:SetValue(0)
        if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        hide_empower_stages(castbar)
    end
    castbar.successstart = nil

    local durationObj
    if isEmpowered then
        if UnitEmpoweredChannelDuration then durationObj = UnitEmpoweredChannelDuration(unit) end
    elseif isChannel then
        if UnitChannelDuration then durationObj = UnitChannelDuration(unit) end
    else
        if UnitCastingDuration then durationObj = UnitCastingDuration(unit) end
    end
    castbar.duration_obj = durationObj
    castbar.isEmpowered  = isEmpowered or false

    castbar:SetReverseFill(isChannel)

    -- Hybrid: SetTimerDuration wenn duration_obj vorhanden (smooth, kein Springen).
    -- v5.9-Fallback fuer Target/Focus wenn duration_obj nil (z.B. aeltere API).
    if durationObj and castbar.SetTimerDuration then
        -- v6.2: Smooth fill, keine Float-Quantisierung
        local direction = isChannel and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime
        castbar:SetTimerDuration(durationObj, Enum.StatusBarInterpolation.Immediate, direction)
        castbar._useTimerDuration = true
    else
        -- v5.9-Fallback: Target/Focus oder wenn duration_obj nil
        castbar._useTimerDuration = false
        if isEmpowered then
            local holdMs = 0
            if GetUnitEmpowerHoldAtMaxTime then
                holdMs = GetUnitEmpowerHoldAtMaxTime(unit) or 0
            end
            castbar:SetMinMaxValues(startMs, endMs + holdMs)
            castbar:SetValue(GetTime() * 1000)
        else
            castbar:SetMinMaxValues(startMs, endMs)
            castbar:SetValue(GetTime() * 1000)
        end
    end

    castbar.casting     = true
    castbar.isChannel   = isChannel
    castbar.failstart   = nil
    -- Reset OnUpdate throttle accumulators for fresh cast
    castbar._timeAccum   = 0
    castbar._safetyAccum = 0

    ns.Debug("  check_casting START: name=%s isChannel=%s isEmp=%s",
        name, tostring(isChannel), tostring(isEmpowered))

    castbar.text:SetText(name)
    if castbar.icon then castbar.icon:SetTexture(texture) end

    if config.dynamicWidth then
        local tw = castbar.text:GetStringWidth()
        if not (issecretvalue and issecretvalue(tw)) then
            local pad = 20
            if config.showTime ~= false then pad = pad + 55 end
            if castbar.iconFrame and config.icon.show
               and not config.icon.positionOutside then
                pad = pad + config.height + 8
            end
            castbar:SetWidth(math.max(config.width, math.min(tw + pad, 500)))
        end
    end

    UpdateCastColor(castbar, isChannel)

    do
        local shieldAlpha = 0
        if not (issecretvalue and issecretvalue(notInterruptible)) then
            shieldAlpha = notInterruptible and 0.55 or 0
        end
        castbar.shieldOverlay:SetAlpha(shieldAlpha)
    end

    if unit == "player" and not isEmpowered and durationObj then
        local ok, totalDur = pcall(durationObj.GetTotalDuration, durationObj, 0)
        if ok and totalDur and totalDur > 0 then
            if castbar.safezone then
                local _, _, lagHome = GetNetStats()
                local w = castbar:GetWidth()
                castbar.safezone:SetWidth(math.min((w / totalDur) * (lagHome / 1000), w))
            end
        end
        if castbar.latency then
            local _, _, lH, lW = GetNetStats()
            castbar.latency:SetText(string.format("%.0fms", (lH + lW) / 2))
        end
    end

    if isEmpowered then
        setup_empower_stages(castbar)
    else
        hide_empower_stages(castbar)
    end

    castbar:Show()
end

-- ============================================================
-- on_unit_event
-- ============================================================
local function on_unit_event(castbar, event, ...)
    do
        local arg1, arg2, arg3, arg4 = ...
        ns.Debug("[%s] %s  casting=%s isChannel=%s isEmp=%s",
            castbar.unit, event,
            tostring(castbar.casting),
            tostring(castbar.isChannel),
            tostring(castbar.isEmpowered))
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        if castbar.isEmpowered then
            ns.Debug("  >> SUCCEEDED ignoriert (empowered)"); return
        end
        if castbar.isChannel then
            ns.Debug("  >> SUCCEEDED ignoriert (isChannel=true, warte auf CHANNEL_STOP)"); return
        end
        if castbar.casting then
            ns.Debug("  >> show_success")
            show_success(castbar)
        end
        return
    end

    if event == "UNIT_SPELLCAST_STOP" then
        if castbar.isChannel then
            ns.Debug("  >> STOP ignoriert (isChannel)")
            return
        end
        local newCast    = UnitCastingInfo(castbar.unit)
        local newChannel = UnitChannelInfo(castbar.unit)
        if newCast or newChannel then
            ns.Debug("  >> STOP ignoriert (neuer Cast/Channel: %s/%s)",
                tostring(newCast), tostring(newChannel))
            return
        end
        ns.Debug("  >> STOP -> hide_castbar")
        hide_castbar(castbar)
        return
    end

    if event == "UNIT_SPELLCAST_FAILED" then
        if castbar.isChannel then
            ns.Debug("  >> FAILED ignoriert (isChannel)")
            return
        end
        local newCast    = UnitCastingInfo(castbar.unit)
        local newChannel = UnitChannelInfo(castbar.unit)
        if newCast or newChannel then
            ns.Debug("  >> FAILED ignoriert (neuer Cast/Channel aktiv)")
            return
        end
        ns.Debug("  >> FAILED -> hide_castbar")
        hide_castbar(castbar)
        return
    end

    local isEmpowered = (event == "UNIT_SPELLCAST_EMPOWER_START")
                     or (event == "UNIT_SPELLCAST_EMPOWER_UPDATE")
                     or (castbar.isEmpowered and event == "UNIT_SPELLCAST_EMPOWER_STOP")

    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        local interruptedBy = select(4, ...)
        ns.Debug("  >> INTERRUPTED by=%s", tostring(interruptedBy))
        check_casting(castbar, event, interruptedBy, isEmpowered)
        return

    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
        local stopGUID = select(2, ...)
        if castbar.currentCastGUID and stopGUID ~= castbar.currentCastGUID then
            ns.Debug("  >> CHANNEL_STOP stale (stop=%s current=%s) – ignored",
                tostring(stopGUID), tostring(castbar.currentCastGUID))
            return
        end
        local arg4 = select(4, ...)
        ns.Debug("  >> CHANNEL_STOP arg4=%s", tostring(arg4))
        if arg4 ~= nil then
            if not (issecretvalue and issecretvalue(arg4)) then
                check_casting(castbar, "UNIT_SPELLCAST_INTERRUPTED", arg4, false)
            end
        else
            ns.Debug("  >> CHANNEL_STOP -> show_success")
            show_success(castbar)
        end
        return

    elseif event == "UNIT_SPELLCAST_EMPOWER_STOP" then
        ns.Debug("  >> EMPOWER_STOP -> show_success")
        show_success(castbar)
        return
    end

    if event == "UNIT_SPELLCAST_CHANNEL_START" then
        castbar.currentCastGUID = select(2, ...)
        ns.Debug("  >> CHANNEL_START castGUID=%s", tostring(castbar.currentCastGUID))
    end

    ns.Debug("  >> check_casting (isEmp=%s)", tostring(isEmpowered))
    check_casting(castbar, event, nil, isEmpowered)
end

-- ============================================================
-- RegisterEvents
-- ============================================================
function Castbar:RegisterEvents(castbar)
    local unit = castbar.unit
    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
        castbar:RegisterUnitEvent(ev, unit)
    end
    castbar:SetScript("OnEvent", on_unit_event)
    -- OnUpdate fires every rendered frame – identical to rcastbar.
    -- WoW only calls OnUpdate on visible frames, so there is zero
    -- overhead while the bar is hidden between casts.
    castbar:SetScript("OnUpdate", onUpdate)
end

-- ============================================================
-- check_unit – called on target/focus change
-- ============================================================
local function check_unit(unit)
    local castbar = Castbar.frames[unit]
    if not castbar then return end

    castbar.casting      = false
    castbar.isEmpowered  = false
    castbar.failstart    = nil
    castbar.successstart = nil
    castbar.duration_obj = nil
    hide_empower_stages(castbar)
    castbar:Hide()
    castbar:SetAlpha(1)
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then castbar.fadeOut:Stop() end

    if not UnitExists(unit) then return end
    check_casting(castbar, "PLAYER_TARGET_CHANGED")
end

-- ============================================================
-- ToggleAnchors
-- ============================================================
function Castbar:ToggleAnchors()
    if InCombatLockdown() then return end
    for _, anchor in pairs(self.anchors) do
        if anchor:IsShown() then anchor:Hide() else anchor:Show() end
    end
end

-- ============================================================
-- Initialize
-- ============================================================
function Castbar:Initialize()
    if self._initialized then return end
    self._initialized = true

    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then
        LSM:Register("statusbar", "smooth_v2", "Interface\\AddOns\\nCastbar\\Media\\smooth_v2")
        LSM:Register("statusbar", "nCastbar Default", "Interface\\AddOns\\nCastbar\\Media\\statusbarTexture")
    end

    self:HideBlizzardCastbars()

    if not self.mainFrame then
        self.mainFrame = CreateFrame("Frame")
        self.mainFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
        self.mainFrame:RegisterEvent("PLAYER_FOCUS_CHANGED")
        self.mainFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        self.mainFrame:SetScript("OnEvent", function(_, event)
            if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
                check_unit("target")
            end
            if event == "PLAYER_FOCUS_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
                check_unit("focus")
            end
        end)
    end

    C_Timer.After(0.5, function()
        for _, unit in ipairs({"player", "target", "focus", "pet"}) do
            Castbar:Create(unit)
        end
        check_unit("target")
        check_unit("focus")
        -- No ticker needed: each castbar drives itself via OnUpdate.
    end)
end

function Castbar:HideBlizzardCastbars()
    if PlayerCastingBarFrame then
        PlayerCastingBarFrame:UnregisterAllEvents()
        PlayerCastingBarFrame:Hide()
        PlayerCastingBarFrame:SetAlpha(0)
    end
    if PetCastingBarFrame then
        PetCastingBarFrame:UnregisterAllEvents()
        PetCastingBarFrame:Hide()
        PetCastingBarFrame:SetAlpha(0)
    end
    if TargetFrameSpellBar then
        TargetFrameSpellBar:UnregisterAllEvents()
        TargetFrameSpellBar:Hide()
        TargetFrameSpellBar:SetAlpha(0)
        if TargetFrameSpellBar.ShowCastBar then TargetFrameSpellBar.ShowCastBar = nop end
    end
    if TargetFrame and TargetFrame.SpellBar then
        TargetFrame.SpellBar:UnregisterAllEvents()
        TargetFrame.SpellBar:Hide()
        TargetFrame.SpellBar:SetAlpha(0)
        if TargetFrame.SpellBar.ShowCastBar then TargetFrame.SpellBar.ShowCastBar = nop end
    end
    if FocusFrameSpellBar then
        FocusFrameSpellBar:UnregisterAllEvents()
        FocusFrameSpellBar:Hide()
        FocusFrameSpellBar:SetAlpha(0)
        if FocusFrameSpellBar.ShowCastBar then FocusFrameSpellBar.ShowCastBar = nop end
    end
    if FocusFrame and FocusFrame.SpellBar then
        FocusFrame.SpellBar:UnregisterAllEvents()
        FocusFrame.SpellBar:Hide()
        FocusFrame.SpellBar:SetAlpha(0)
        if FocusFrame.SpellBar.ShowCastBar then FocusFrame.SpellBar.ShowCastBar = nop end
    end
end

function Castbar:Cleanup()
    for _, frame in pairs(self.frames) do
        frame:UnregisterAllEvents()
        frame:SetScript("OnEvent", nil)
        frame:SetScript("OnUpdate", nil)
        frame:Hide()
    end
    for _, anchor in pairs(self.anchors) do anchor:Hide() end
end

function Castbar:Reload()
    self:Cleanup()
    self.frames       = {}
    self.anchors      = {}
    self._initialized = false
    self:Initialize()
end
