-- rOrbs: Target Castbar Ring  v26 (Release)
--
-- WoW 12.x: Alle Werte aus UnitCastingInfo/UnitChannelInfo sind tainted (secret values).
-- SpellID aus Event-Args ist ebenfalls tainted - C_Spell.GetSpellInfo nicht nutzbar.
-- Timing-Quelle: gelernte Dauer (learnedCastDur / learnedChannelDur).
--   - Wird nach jedem abgeschlossenen Cast per gleitendem Durchschnitt verbessert.
--   - Persistiert zwischen Sessions via rOrbsCastDB (SavedVariable).
--   - Startwert: 3.0s (DEFAULT_DURATION), verbessert sich nach 1-2 Casts.

local addon, ns = ...

local mediaPath = "Interface\\AddOns\\rOrbs\\media\\"
local GetTime   = GetTime
local math      = math

-- DB Keys
local DB_ENABLED         = "castRingEnabled"
local DB_CAST_R          = "castRingCastR"
local DB_CAST_G          = "castRingCastG"
local DB_CAST_B          = "castRingCastB"
local DB_CHAN_R           = "castRingChanR"
local DB_CHAN_G           = "castRingChanG"
local DB_CHAN_B           = "castRingChanB"
local DB_SPARK_CAST_R    = "castRingSparkCastR"
local DB_SPARK_CAST_G    = "castRingSparkCastG"
local DB_SPARK_CAST_B    = "castRingSparkCastB"
local DB_SPARK_CHAN_R     = "castRingSparkChanR"
local DB_SPARK_CHAN_G     = "castRingSparkChanG"
local DB_SPARK_CHAN_B     = "castRingSparkChanB"
local DB_BG_ALPHA        = "castRingBgAlpha"
local DB_BG_R            = "castRingBgR"
local DB_BG_G            = "castRingBgG"
local DB_BG_B            = "castRingBgB"
local DB_BG_MODE         = "castRingBgMode"
local DB_BG_CLASS_COLOR  = "castRingBgClassColor"
local DB_USE_CLASS_COLOR = "castRingUseClassColor"
local DB_FADE_TIME       = "castRingFadeTime"
local DB_RING_SCALE      = "castRingRingScale"

local DEF = {
    castR=1,   castG=0.15, castB=0.05,
    chanR=0.3, chanG=0.6,  chanB=1.0,
    sparkCastR=1,   sparkCastG=0.6,  sparkCastB=0.2,
    sparkChanR=0.5, sparkChanG=0.8,  sparkChanB=1.0,
    bgAlpha=0.8, bgR=1.0, bgG=1.0, bgB=1.0,
    bgMode="rose_bright", bgClassColor=false,
    useClassColor=false, fadeTime=0.4, ringScale=1.0,
}

local FIXED_OVERSIZE = 1.5
local RING_TEX_SIZE  = 512

local function db(key, default)
    if rOrbsDB and rOrbsDB[key] ~= nil then return rOrbsDB[key] end
    return default
end

local function GetTargetClassColor()
    local _, classToken = UnitClass("target")
    if not classToken then return nil end
    local c = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken]
    if c then return c.r, c.g, c.b end
    return nil
end

local castbarFrame = nil
local bgFrame      = nil
local bgFade = { active=false, fadeIn=false, current=0.0, target=0.0 }

local DEFAULT_DURATION  = 3.0
local learnedCastDur    = DEFAULT_DURATION  -- RAM-Fallback: lernt in dieser Session
local learnedChannelDur = DEFAULT_DURATION  -- RAM-Fallback: lernt in dieser Session
-- Kein currentSpellID / targetGUID mehr - CLEU-Ansatz entfernt (Context-Lock-Problem).

-- ── Persistente Lern-Dauern (SavedVariable rOrbsCastDB) ─────────────────────
-- SpellID-basierter Ansatz schlaegt in WoW 12.x fehl:
--   - CLEU-RegisterEvent verliert sicheren Kontext in verschachtelten C_Timer-Calls
--   - SpellID aus UnitCastingInfo ist secret value
-- Loesung: Zwei Werte pro Typ (cast/channel) persistieren.
-- Das deckt 95% der Praezision ab, da Targets in Kaempfen wenige Spells wiederholen.
-- Format: rOrbsCastDB = { castDur = N, channelDur = N }

local function InitCastDB()
    if not rOrbsCastDB then
        rOrbsCastDB = { castDur = DEFAULT_DURATION, channelDur = DEFAULT_DURATION }
    end
    -- Gespeicherte Werte in RAM-Variablen laden
    if rOrbsCastDB.castDur    and rOrbsCastDB.castDur > 0.3    then learnedCastDur    = rOrbsCastDB.castDur    end
    if rOrbsCastDB.channelDur and rOrbsCastDB.channelDur > 0.3 then learnedChannelDur = rOrbsCastDB.channelDur end
end

-- Beste verfuegbare Dauer ermitteln.
-- WoW 12.x: SpellID aus Event-Args ist tainted -> C_Spell.GetSpellInfo nicht nutzbar.
-- Duration-Objekte (UnitCastingDuration etc.): GetTotalDuration liefert ebenfalls
-- secret values -> Vergleiche (total > 0.1) und Arithmetik schlagen fehl (Taint-Error).
-- Einzig sichere Quelle: gelernte Dauer aus laufender Session + SavedVariable.
local function GetBestDuration(isChannel, isEmpower)
    -- WoW 12.x: alle Blizzard-Duration-APIs liefern tainted secret values.
    -- Sichere Quelle: gelernte Dauer (aus dieser Session / SavedVariable).
    return isChannel and learnedChannelDur or learnedCastDur
end

-- Gemessene Dauer speichern: RAM + SavedVariable (gleitender Durchschnitt).
local function LearnDuration(measured, isChannel)
    if measured < 0.3 or measured > 30 then return end

    if isChannel then
        -- Gleitender Durchschnitt (Gewicht 4 fuer alte Messungen)
        learnedChannelDur = (learnedChannelDur * 4 + measured) / 5
        if rOrbsCastDB then rOrbsCastDB.channelDur = learnedChannelDur end
    else
        learnedCastDur = (learnedCastDur * 4 + measured) / 5
        if rOrbsCastDB then rOrbsCastDB.castDur = learnedCastDur end
    end
end


-- Prozent-Berechnung via GetTime() - keine secret values
local function GetBarPct(self, isChannel)
    if not self.castStartTime then return nil end
    local elapsed = GetTime() - self.castStartTime
    local dur = self.castDuration or DEFAULT_DURATION
    if dur <= 0 then return nil end
    local pct = elapsed / dur
    if isChannel then pct = 1.0 - pct end
    return math.max(0.0, math.min(1.0, pct))
end

local function ApplyBgColor()
    if not bgFrame then return end
    if db(DB_BG_CLASS_COLOR, DEF.bgClassColor) then
        local cr,cg,cb = GetTargetClassColor()
        if cr then bgFrame.bgTexture:SetVertexColor(cr,cg,cb); return end
    end
    bgFrame.bgTexture:SetVertexColor(db(DB_BG_R,DEF.bgR), db(DB_BG_G,DEF.bgG), db(DB_BG_B,DEF.bgB))
end

local function StartFadeIn()
    if not bgFrame then return end
    local alpha = db(DB_BG_ALPHA, DEF.bgAlpha)
    bgFade.fadeIn=true; bgFade.active=true; bgFade.target=alpha
    bgFrame:SetAlpha(bgFade.current); bgFrame:Show()
end

local function StartFadeOut()
    if not bgFrame then return end
    bgFade.fadeIn=false; bgFade.active=true; bgFade.target=0.0
end

local function BgOnUpdate(self, elapsed)
    if not bgFade.active then return end
    local speed = 1.0 / math.max(db(DB_FADE_TIME, DEF.fadeTime), 0.05)
    if bgFade.fadeIn then
        bgFade.current = math.min(bgFade.current + elapsed*speed, bgFade.target)
        self:SetAlpha(bgFade.current)
        if bgFade.current >= bgFade.target then bgFade.active=false end
    else
        bgFade.current = math.max(bgFade.current - elapsed*speed, 0)
        self:SetAlpha(bgFade.current)
        if bgFade.current <= 0 then bgFade.active=false; self:Hide() end
    end
end

local function ApplyColors(f, isChannel)
    local rr, rg, rb, sr, sg, sb
    if isChannel then
        rr=db(DB_CHAN_R,DEF.chanR); rg=db(DB_CHAN_G,DEF.chanG); rb=db(DB_CHAN_B,DEF.chanB)
        sr=db(DB_SPARK_CHAN_R,DEF.sparkChanR); sg=db(DB_SPARK_CHAN_G,DEF.sparkChanG); sb=db(DB_SPARK_CHAN_B,DEF.sparkChanB)
    else
        if db(DB_USE_CLASS_COLOR, DEF.useClassColor) then
            local cr,cg,cb = GetTargetClassColor()
            if cr then rr,rg,rb=cr,cg,cb
            else rr=db(DB_CAST_R,DEF.castR); rg=db(DB_CAST_G,DEF.castG); rb=db(DB_CAST_B,DEF.castB) end
        else
            rr=db(DB_CAST_R,DEF.castR); rg=db(DB_CAST_G,DEF.castG); rb=db(DB_CAST_B,DEF.castB)
        end
        sr=db(DB_SPARK_CAST_R,DEF.sparkCastR); sg=db(DB_SPARK_CAST_G,DEF.sparkCastG); sb=db(DB_SPARK_CAST_B,DEF.sparkCastB)
    end
    if f.leftRingTexture  then f.leftRingTexture:SetVertexColor(rr,rg,rb)   end
    if f.rightRingTexture then f.rightRingTexture:SetVertexColor(rr,rg,rb)  end
    if f.leftRingSpark    then f.leftRingSpark:SetVertexColor(sr,sg,sb)     end
    if f.rightRingSpark   then f.rightRingSpark:SetVertexColor(sr,sg,sb)    end
end

local function Disable(self)
    self:SetScript("OnUpdate", nil)
    self:Hide()
    self.isCasting     = false
    self.isChanneling  = false
    self.isEmpowering  = false
    self.castStartTime = nil
    self.castDuration  = nil
    StartFadeOut()
end

local function OnUpdate(self, elapsed)
    local pct = GetBarPct(self, self.isChanneling)
    if pct == nil then return end
    if pct > 0.5 then
        self.leftRingTexture:SetRotation( math.rad(self.leftRingTexture.baseDeg  - 180*(pct*2-1)))
        self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg - 180))
    else
        self.leftRingTexture:SetRotation( math.rad(self.leftRingTexture.baseDeg))
        self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg - 180*(pct*2)))
    end
    if self.rightRingSpark then
        self.rightRingSpark:SetRotation(math.rad(self.rightRingSpark.baseDeg - 180*(pct*2)))
    end
    if self.leftRingSpark then
        self.leftRingSpark:SetRotation(math.rad(self.leftRingSpark.baseDeg - 180*(pct*2-1)))
    end
end

local function Enable(self, isChannel, isEmpower)
    self.castStartTime = GetTime()
    self.castDuration  = GetBestDuration(isChannel, isEmpower)
    self.isCasting     = not isChannel and not isEmpower
    self.isChanneling  = isChannel
    self.isEmpowering  = isEmpower
    ApplyColors(self, isChannel)
    ApplyBgColor()
    self:Show()
    self:SetScript("OnUpdate", OnUpdate)
    StartFadeIn()
end

local function CheckCasting(self)
    -- nCastbar-Pattern: immer aktuellen Zustand abfragen statt auf Event-Reihenfolge vertrauen.
    -- Verhindert Race-Condition: UNIT_SPELLCAST_STOP feuert manchmal NACH CHANNEL_START
    -- und wuerde den Ring sofort wieder deaktivieren.
    -- "if name then" (Wahrheitswert) ist erlaubt auch bei secret values.
    -- Nur Arithmetik / Table-Key / string-Ops auf secret values sind verboten.
    if not UnitExists("target") then Disable(self); return end

    local name = UnitCastingInfo("target")
    if name then
        -- Laufender normaler Cast oder Empower
        if not self.isCasting then
            Enable(self, false, false)
        end
        return
    end

    name = UnitChannelInfo("target")
    if name then
        -- Laufender Channel (Arcane Missiles, Rapid Fire, Tranquility, Evoker-Channels...)
        if not self.isChanneling then
            Enable(self, true, false)
        end
        return
    end

    -- Kein Cast aktiv
    Disable(self)
end

local function OnEvent(self, event, unitArg)
    if unitArg and unitArg ~= "target" then return end

    -- Target-Wechsel
    if event == "PLAYER_TARGET_CHANGED" then
        CheckCasting(self)
        return
    end

    -- Normaler Cast
    if event == "UNIT_SPELLCAST_START" then
        Enable(self, false, false)
        return
    end
    -- Kanalisierter Spell (Arcane Missiles, Rapid Fire, Tranquility, Mind Flay...)
    if event == "UNIT_SPELLCAST_CHANNEL_START" then
        Enable(self, true, false)
        return
    end
    -- Evoker Empower
    if event == "UNIT_SPELLCAST_EMPOWER_START" then
        Enable(self, false, true)
        return
    end

    -- Updates: ignorieren, Timing laeuft via GetTime()
    if event == "UNIT_SPELLCAST_DELAYED"
    or event == "UNIT_SPELLCAST_CHANNEL_UPDATE"
    or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
        return
    end

    -- STOP / FAILED / SUCCEEDED / INTERRUPTED: Dauer lernen, dann deaktivieren.
    if self.castStartTime then
        LearnDuration(GetTime() - self.castStartTime, self.isChanneling)
    end
    Disable(self)
end


local function CreateBgFrame(parent)
    local w = RING_TEX_SIZE
    local f = CreateFrame("Frame", "rOrbsTargetCastBg", parent)
    f:SetSize(w,w); f:SetPoint("CENTER", parent, "CENTER", 0, 0)
    f:SetFrameStrata("BACKGROUND"); f:SetFrameLevel(1)
    f:SetAlpha(0); f:Hide()
    local tex = f:CreateTexture(nil, "BACKGROUND", nil, 0)
    tex:SetTexture(mediaPath.."compass-rose-bright")
    tex:SetSize(w,w); tex:SetPoint("CENTER")
    f.bgTexture = tex
    f:SetScript("OnUpdate", BgOnUpdate)
    return f
end

local function CreateRingFrame(parent)
    local w = RING_TEX_SIZE
    local f = CreateFrame("Frame", "rOrbsTargetCastRing", parent)
    f:SetSize(w,w); f:SetPoint("CENTER", parent, "CENTER", 0, 0)
    f:SetFrameStrata("HIGH")
    f:SetFrameLevel(parent:GetFrameLevel() + 20)
    f:Hide()

    local sf1 = CreateFrame("ScrollFrame", nil, f)
    sf1:SetSize(w/2,w); sf1:SetPoint("LEFT", f, "LEFT", 0, 0)
    local sc1 = CreateFrame("Frame"); sc1:SetSize(w,w); sf1:SetScrollChild(sc1)
    local rt1 = sc1:CreateTexture(nil,"ARTWORK",nil,1)
    rt1:SetTexture(mediaPath.."compass-rose-ring"); rt1:SetSize(w,w); rt1:SetPoint("CENTER")
    rt1.baseDeg=-180; f.leftRingTexture=rt1
    local rs1 = sc1:CreateTexture(nil,"ARTWORK",nil,2)
    rs1:SetTexture(mediaPath.."compass-rose-spark"); rs1:SetSize(w,w); rs1:SetPoint("CENTER")
    rs1:SetBlendMode("ADD"); rs1.baseDeg=-180; f.leftRingSpark=rs1

    local sf2 = CreateFrame("ScrollFrame", nil, f)
    sf2:SetSize(w/2,w); sf2:SetPoint("RIGHT", f, "RIGHT", 0, 0); sf2:SetHorizontalScroll(w/2)
    local sc2 = CreateFrame("Frame"); sc2:SetSize(w,w); sf2:SetScrollChild(sc2)
    local rt2 = sc2:CreateTexture(nil,"ARTWORK",nil,1)
    rt2:SetTexture(mediaPath.."compass-rose-ring"); rt2:SetSize(w,w); rt2:SetPoint("CENTER")
    rt2.baseDeg=0; f.rightRingTexture=rt2
    local rs2 = sc2:CreateTexture(nil,"ARTWORK",nil,2)
    rs2:SetTexture(mediaPath.."compass-rose-spark"); rs2:SetSize(w,w); rs2:SetPoint("CENTER")
    rs2:SetBlendMode("ADD"); rs2.baseDeg=0; f.rightRingSpark=rs2

    f:RegisterUnitEvent("UNIT_SPELLCAST_START",            "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_STOP",             "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_FAILED",           "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_FAILED_QUIET",     "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED",      "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED",        "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_DELAYED",          "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START",    "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE",   "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP",     "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_START",    "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_UPDATE",   "target")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_STOP",     "target")
    f:RegisterEvent("PLAYER_TARGET_CHANGED")
    f:SetScript("OnEvent", OnEvent)

    return f
end

local function UpdateRingScale()
    local orbFrame = _G["rOrbsTargetHealthOrb"]
    if not orbFrame then return end
    local baseScale = (orbFrame:GetWidth() * FIXED_OVERSIZE) / RING_TEX_SIZE
    local ringMult  = db(DB_RING_SCALE, DEF.ringScale)
    if castbarFrame then castbarFrame:SetScale(baseScale * ringMult) end
    if bgFrame      then bgFrame:SetScale(baseScale) end
end

ns.CastRing = {
    ApplyColors  = function()
        if castbarFrame then ApplyColors(castbarFrame, castbarFrame.isChanneling) end
    end,
    ApplyScale   = UpdateRingScale,
    ApplyBgAlpha = function(v)
        if bgFrame and bgFade.current > 0 then
            bgFade.target=v; bgFade.current=v; bgFrame:SetAlpha(v)
        end
    end,
    ApplyBgColor    = ApplyBgColor,
    ApplyBackground = function()
        if db(DB_BG_MODE, DEF.bgMode) ~= "rose_bright" then
            if bgFrame then bgFrame:SetAlpha(0); bgFrame:Hide() end
            bgFade.current=0; bgFade.active=false
        end
    end,
    Rebuild = function()
        if castbarFrame then
            castbarFrame:SetScript("OnUpdate", nil)
            castbarFrame:UnregisterAllEvents()
            castbarFrame:Hide(); castbarFrame=nil
        end
        if bgFrame then bgFrame:SetScript("OnUpdate",nil); bgFrame:Hide(); bgFrame=nil end
        bgFade.current=0; bgFade.active=false
        local orb = _G["rOrbsTargetHealthOrb"]
        if orb then
            bgFrame      = CreateBgFrame(orb)
            castbarFrame = CreateRingFrame(orb)
            UpdateRingScale()
        end
    end,
}

local function Init()
    InitCastDB()  -- SavedVariable rOrbsCastDB initialisieren


    if rOrbsDB then
        local defs = {
            [DB_ENABLED]=true,
            [DB_CAST_R]=DEF.castR,   [DB_CAST_G]=DEF.castG,   [DB_CAST_B]=DEF.castB,
            [DB_CHAN_R]=DEF.chanR,   [DB_CHAN_G]=DEF.chanG,   [DB_CHAN_B]=DEF.chanB,
            [DB_SPARK_CAST_R]=DEF.sparkCastR, [DB_SPARK_CAST_G]=DEF.sparkCastG, [DB_SPARK_CAST_B]=DEF.sparkCastB,
            [DB_SPARK_CHAN_R]=DEF.sparkChanR, [DB_SPARK_CHAN_G]=DEF.sparkChanG, [DB_SPARK_CHAN_B]=DEF.sparkChanB,
            [DB_BG_ALPHA]=DEF.bgAlpha, [DB_BG_R]=DEF.bgR, [DB_BG_G]=DEF.bgG, [DB_BG_B]=DEF.bgB,
            [DB_BG_MODE]=DEF.bgMode, [DB_BG_CLASS_COLOR]=DEF.bgClassColor,
            [DB_USE_CLASS_COLOR]=DEF.useClassColor, [DB_FADE_TIME]=DEF.fadeTime, [DB_RING_SCALE]=DEF.ringScale,
        }
        for k,v in pairs(defs) do if rOrbsDB[k]==nil then rOrbsDB[k]=v end end
    end

    local orbFrame = _G["rOrbsTargetHealthOrb"]
    if not orbFrame then C_Timer.After(0.5, Init); return end
    if not db(DB_ENABLED, true) then return end

    if castbarFrame then
        castbarFrame:SetScript("OnUpdate", nil)
        castbarFrame:UnregisterAllEvents()
        castbarFrame:Hide(); castbarFrame=nil
    end
    if bgFrame then bgFrame:SetScript("OnUpdate",nil); bgFrame:Hide(); bgFrame=nil end
    bgFade.current=0; bgFade.active=false

    bgFrame      = CreateBgFrame(orbFrame)
    castbarFrame = CreateRingFrame(orbFrame)
    UpdateRingScale()

    local sf = CreateFrame("Frame")
    sf:RegisterEvent("UI_SCALE_CHANGED"); sf:RegisterEvent("PLAYER_ENTERING_WORLD")
    sf:SetScript("OnEvent", UpdateRingScale)

    C_Timer.After(1.2, function()
        if not castbarFrame or not UnitExists("target") then return end
        -- SpellID via CLEU unbekannt beim Login-Check; CLEU liefert sie beim naechsten Cast.
        local name = UnitCastingInfo("target")
        if name then Enable(castbarFrame, false); return end
        name = UnitChannelInfo("target")
        if name then Enable(castbarFrame, true) end
    end)
end

ns:RegisterInit(function() C_Timer.After(0.2, Init) end)
