-- rOrbs: Target Castbar Ring  v26 (Release)
--
-- WoW 12.x: UnitCastingInfo/UnitChannelInfo values are tainted (secret values).
-- SpellID from event args is tainted too — do not use C_Spell.GetSpellInfo.
-- Timing source: learned duration (learnedCastDur / learnedChannelDur).
--   - Refined with a moving average after each finished cast.
--   - Persisted across sessions in rOrbsCastDB (saved variable).
--   - Starts at 3.0s (DEFAULT_DURATION); improves after 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; learns this session
local learnedChannelDur = DEFAULT_DURATION  -- RAM fallback; learns this session
-- No currentSpellID / targetGUID — CLEU path removed (context lock issues).

-- ── Persistent learned durations (saved variable rOrbsCastDB) ──────────────
-- SpellID-based approach fails in WoW 12.x:
--   - CLEU RegisterEvent loses secure context in nested C_Timer calls
--   - SpellID from UnitCastingInfo is a secret value
-- Fix: persist two values per type (cast/channel).
-- Good enough for most fights where targets repeat few spells.
-- Format: rOrbsCastDB = { castDur = N, channelDur = N }

local function InitCastDB()
    if not rOrbsCastDB then
        rOrbsCastDB = { castDur = DEFAULT_DURATION, channelDur = DEFAULT_DURATION }
    end
    -- Load saved values into RAM
    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

-- Pick best available duration.
-- WoW 12.x: spell ID from event args is tainted — avoid C_Spell.GetSpellInfo.
-- Duration objects (UnitCastingDuration etc.): GetTotalDuration is also secret —
-- comparisons (total > 0.1) and math taint-fail.
-- Only safe source: learned duration from this session + saved variable.
local function GetBestDuration(isChannel, isEmpower)
    -- WoW 12.x: Blizzard duration APIs return tainted secrets.
    -- Safe source: learned duration (session + saved variable).
    return isChannel and learnedChannelDur or learnedCastDur
end

-- Store measured duration: RAM + saved variable (moving average).
local function LearnDuration(measured, isChannel)
    if measured < 0.3 or measured > 30 then return end

    if isChannel then
        -- Moving average (weight 4 for old samples)
        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


-- Percent via GetTime() — no 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-style: poll current state instead of trusting event order.
    -- Avoid race: UNIT_SPELLCAST_STOP sometimes fires after CHANNEL_START
    -- and would disable the ring immediately.
    -- "if name then" (truth test) is allowed with secret values.
    -- Avoid math / table keys / string ops on secrets.
    if not UnitExists("target") then Disable(self); return end

    local name = UnitCastingInfo("target")
    if name then
        -- Active normal cast or empower
        if not self.isCasting then
            Enable(self, false, false)
        end
        return
    end

    name = UnitChannelInfo("target")
    if name then
        -- Active channel (Arcane Missiles, Rapid Fire, Tranquility, evoker channels…)
        if not self.isChanneling then
            Enable(self, true, false)
        end
        return
    end

    -- No cast active
    Disable(self)
end

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

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

    -- Normal cast
    if event == "UNIT_SPELLCAST_START" then
        Enable(self, false, false)
        return
    end
    -- Channeled 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: ignore; timing driven by 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: learn duration, then disable.
    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 from CLEU unknown at login check; CLEU provides it on next 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)
