-- rCompassCastbar
-- zork, 2018 — Retail: UnitCastingInfo/UnitChannelInfo (ms, texture=fileID), C_Spell.GetSpellCooldown

-----------------------------
-- Variables
-----------------------------

--addon tables
local A = ...
local L = rCompassCastbarL or {}

local mediaPath = "Interface\\AddOns\\"..A.."\\media\\"
local channelSparkTexture = mediaPath.."compass-rose-spark-channel.tga"
local iconBorderTexture = mediaPath.."icon_border.tga"
local assistedArrowTexture = mediaPath.."assist_arrow.tga"
local assistedArrowVerifiedTexture = 6992986
local assistedArrowPreferredTextureA = 4615764
local assistedArrowPreferredTextureB = 4613342
local GetUnitEmpowerHoldAtMaxTime = GetUnitEmpowerHoldAtMaxTime
local UnitCastingInfo, UnitChannelInfo = UnitCastingInfo, UnitChannelInfo
local GetTime, GetCursorPosition, math = GetTime, GetCursorPosition, math

-----------------------------
-- Config
-----------------------------

-- true = progress ring(s) use class color (per bar: player/target/focus/pet; gcd = player)
-- false = colors from ringCfg[..].ring.color
local useClassColor = false
-- true = empower casts tint ring per stage (last stage = full color)
-- false = no per-stage empower tinting
local useEmpowerStageColoring = true
-- true = show spell icon top-right with icon_border.tga
-- false = hide spell icon entirely
local showCurrentCastIcon = true
local castIconSize = 160
local castIconOffsetX = -170
local castIconOffsetY = 0
-- true = assisted-highlight icon (Blizzard suggested spell) right of cursor
-- false = hide assisted-highlight icon
local showAssistedHighlightIcon = true
local assistedHighlightIconSize = 160
local assistedHighlightIconScale = 0.2
local assistedHighlightIconOffsetX = 320
local assistedHighlightIconOffsetY = 0
local assistedArrowScaleMultiplier = 1.15
local assistedArrowOffsetXFactor = -0.15
local assistedArrowOffsetYFactor = 0
-- Fade in/out duration (seconds)
local castbarFadeInDuration = 0.12
local castbarFadeOutDuration = 0.12

--cfg
local ringCfg = {}

--overall scale
local overallScale = 0.80

--player settings
ringCfg["player"] = {
  enabled         = true,
  size            = {512,512},
  scale           = 0.2 * overallScale,
  --comment this in if you want the position to be fixed, otherwise position at cursor
  --point           = {"CENTER",0,0},
  background = {
    enabled = true, -- "compass-rose" background (only with .tga/.blp in media/, not PNG)
    color = {0.4,0.3,0,1}, --red,green,blue,alpha
    blendmode = "ADD", --ADD or BLEND
    texture = mediaPath.."compass-rose",
  },
  ring = {
    color = {1,0.8,0,1},
    blendmode = "BLEND",
    texture = mediaPath.."compass-rose-ring",
  },
  spark = {
    enabled = true,
    color = {1,1,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-spark",
  },
}

--gcd settings
ringCfg["gcd"] = {
  enabled         = false,
  size            = {512,512},
  scale           = 0.15 * overallScale,
  background = {
    enabled = false,
    color = {0.5,0.4,0,1}, --red,green,blue,alpha
    blendmode = "ADD", --ADD or BLEND
    texture = mediaPath.."compass-rose",
  },
  ring = {
    color = {0.5,0.5,0.5,1},
    blendmode = "BLEND",
    texture = mediaPath.."compass-rose-ring",
  },
  spark = {
    enabled = true,
    color = {1,1,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-spark",
  },
}

--target settings
--[[
ringCfg["target"] = {
  enabled         = false,
  size            = {512,512},
  scale           = 0.11 * overallScale,
  --comment this in if you want the position to be fixed, otherwise position at cursor
  --point           = {"CENTER",0,0},
  background = {
    enabled = false,
    color = {0.4,0.3,0,0.6}, --red,green,blue,alpha
    blendmode = "ADD", --ADD or BLEND
    texture = mediaPath.."compass-rose-bright",
  },
  ring = {
    color = {1,0,0,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-ring-bright",
  },
  spark = {
    enabled = true,
    color = {1,1,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-spark",
  },
}
]]

--focus settings
--[[
ringCfg["focus"] = {
  enabled         = false,
  size            = {512,512},
  scale           = 0.08 * overallScale,
  --comment this in if you want the position to be fixed, otherwise position at cursor
  --point           = {"CENTER",0,0},
  background = {
    enabled = false,
    color = {0.4,0.3,0,0.6}, --red,green,blue,alpha
    blendmode = "ADD", --ADD or BLEND
    texture = mediaPath.."compass-rose-bright",
  },
  ring = {
    color = {0,0.5,1,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-ring-bright",
  },
  spark = {
    enabled = true,
    color = {1,1,1},
    blendmode = "ADD",
    texture = mediaPath.."compass-rose-spark",
  },
}
]]

-----------------------------
-- Global Live-Control Table
-- GUI uses this table to drive frames at runtime without /reload.
-----------------------------
rCompassCastbarFrames = {
  castbarFrames        = {},   -- all created castbar frames (indexed)
  playerFrame          = nil,  -- player castbar frame
  gcdFrame             = nil,  -- GCD frame
  assistedHighlight    = nil,  -- assisted-highlight frame
}

-- Live setters: called from GUI
function rCompassCastbar_SetShowCastIcon(val)
  showCurrentCastIcon = val
  -- update all existing castIcon frames immediately
  for _, f in ipairs(rCompassCastbarFrames.castbarFrames) do
    if f.castIcon then
      if val then
        -- show only while a cast is active
        if f.isEnabled and f.spellTexture then
          f.castIcon.texture:SetTexture(f.spellTexture)
          f.castIcon:Show()
        end
      else
        f.castIcon:Hide()
      end
    end
  end
end

function rCompassCastbar_SetShowAssistedIcon(val)
  showAssistedHighlightIcon = val
  local af = rCompassCastbarFrames.assistedHighlight
  if af then
    if not val then
      af:Hide()
    end
    -- when val=true, UpdateAssistedHighlightIcon() shows it on next cast
  end
end

function rCompassCastbar_SetRingColor(unit, r, g, b, a)
  -- Set ring color live on matching castbar frame
  for _, f in ipairs(rCompassCastbarFrames.castbarFrames) do
    if f.unit == unit then
      f.cfgRing.color = {r, g, b, a or 1}
      if f.leftRingTexture  then f.leftRingTexture:SetVertexColor(r, g, b, a or 1) end
      if f.rightRingTexture then f.rightRingTexture:SetVertexColor(r, g, b, a or 1) end
    end
  end
end

function rCompassCastbar_SetBgColor(unit, r, g, b, a)
  for _, f in ipairs(rCompassCastbarFrames.castbarFrames) do
    if f.unit == unit then
      if f.bg then f.bg:SetVertexColor(r, g, b, a or 1) end
    end
  end
end

-- Preview cast duration (seconds)
local PREVIEW_CAST_DURATION = 10

-- Assigned after ApplyRingVertexColors: player frame mirrors GUI/SavedVars
local SyncPlayerFrameFromSavedCfg

-- Draw preview above config UI (same DIALOG strata would hide it underneath)
local function PreviewPushTopLayer(frame)
  frame._rCCBPreviewStrata = frame:GetFrameStrata()
  frame._rCCBPreviewLevel  = frame:GetFrameLevel()
  frame:SetFrameStrata("TOOLTIP")
  frame:SetFrameLevel(5000)
  local a = frame.cursorAnchor
  if a then
    a._rCCBPreviewStrata = a:GetFrameStrata()
    a._rCCBPreviewLevel  = a:GetFrameLevel()
    a:SetFrameStrata("TOOLTIP")
    a:SetFrameLevel(4999)
  end
end

local function PreviewPopTopLayer(frame)
  if frame._rCCBPreviewStrata then
    frame:SetFrameStrata(frame._rCCBPreviewStrata)
    frame:SetFrameLevel(frame._rCCBPreviewLevel or 0)
    frame._rCCBPreviewStrata, frame._rCCBPreviewLevel = nil, nil
  end
  local a = frame.cursorAnchor
  if a and a._rCCBPreviewStrata then
    a:SetFrameStrata(a._rCCBPreviewStrata)
    a:SetFrameLevel(a._rCCBPreviewLevel or 0)
    a._rCCBPreviewStrata, a._rCCBPreviewLevel = nil, nil
  end
end

-- Preview: same frame scale as real cast (no artificial minimum scale)
function rCompassCastbar_Preview()
  local f = rCompassCastbarFrames.playerFrame
  if not f then return end
  if not f.leftRingTexture or not f.rightRingTexture then return end
  if SyncPlayerFrameFromSavedCfg then
    SyncPlayerFrameFromSavedCfg(f)
  end
  -- Set preview state
  f.rCCBPreview  = true
  f.spellName    = L["Preview"]
  f.spellTexture = 135887  -- spell icon fileID for preview texture
  f.startTime    = GetTime() * 1000
  f.endTime      = (GetTime() + PREVIEW_CAST_DURATION) * 1000
  f.duration     = PREVIEW_CAST_DURATION
  f.current      = 0
  f.elapsed      = 0
  f.percent      = 0
  f.isChanneling = false
  f.isEmpowering = false
  f.update       = false
  f.isEnabled    = true
  f.fadeOut      = false
  f.fadeIn       = false
  f.fadeInElapsed= 0
  -- castIcon: layout/visibility from Sync (SavedVars); here only preview texture
  if f.castIcon and f.castIcon:IsShown() then
    f.castIcon.texture:SetTexture(135887)
  end
  -- position cursorAnchor
  if f.cursorAnchor then
    f.cursorAnchor:Show()
    local scale = UIParent:GetEffectiveScale()
    local x, y = GetCursorPosition()
    f.cursorAnchor:ClearAllPoints()
    f.cursorAnchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale)
    -- Disable(true) sets cursorAnchor:SetAlpha(0) via ApplyFrameAlpha — without this the chain stays invisible
    f.cursorAnchor:SetAlpha(1)
  end
  f:SetAlpha(1)
  PreviewPushTopLayer(f)
  f:Show()
  -- Assisted highlight like config (fake cast = no real UnitCast → preview mode)
  do
    local t = _G.rCompassCastbarCfg
    local showAssist = showAssistedHighlightIcon
    if t and t.showAssistedHighlightIcon ~= nil then
      local v = t.showAssistedHighlightIcon
      if v == true or v == 1 then showAssist = true elseif v == false or v == 0 then showAssist = false end
    end
    local af = rCompassCastbarFrames.assistedHighlight
    if af and showAssist then
      assistedHighlightPreviewMode = true
      if rCompassCastbarFrames.SyncAssistedHighlightFromSavedCfg then
        rCompassCastbarFrames.SyncAssistedHighlightFromSavedCfg(af)
      end
      PreviewPushTopLayer(af)
      if af.cursorAnchor then
        local sc = UIParent:GetEffectiveScale()
        local ax, ay = GetCursorPosition()
        af.cursorAnchor:ClearAllPoints()
        af.cursorAnchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", ax / sc, ay / sc)
        af.cursorAnchor:Show()
        af.cursorAnchor:SetAlpha(1)
      end
      if rCompassCastbarFrames.UpdateAssistedHighlightIcon then
        rCompassCastbarFrames.UpdateAssistedHighlightIcon()
      end
    end
  end
  -- OnUpdate-Skript manuell starten
  local previewElapsed = 0
  local previewDuration = PREVIEW_CAST_DURATION
  f:SetScript("OnUpdate", function(self, elapsed)
    previewElapsed = previewElapsed + elapsed
    local pct = math.min(previewElapsed / previewDuration, 1)
    self.percent = pct
    -- Cursor verfolgen
    if self.cursorAnchor then
      local scale = UIParent:GetEffectiveScale()
      local cx, cy = GetCursorPosition()
      self.cursorAnchor:ClearAllPoints()
      self.cursorAnchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx / scale, cy / scale)
    end
    local ok, err = pcall(function()
      -- Ring-Rotation (Cast, nicht Channel)
      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 - 0))
        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)
    if not ok then
      self.rCCBPreview = false
      PreviewPopTopLayer(self)
      if assistedHighlightPreviewMode then
        assistedHighlightPreviewMode = false
        local a = rCompassCastbarFrames.assistedHighlight
        if a then PreviewPopTopLayer(a); a:Hide(); if a.cursorAnchor then a.cursorAnchor:Hide() end end
        if rCompassCastbarFrames.UpdateAssistedHighlightIcon then rCompassCastbarFrames.UpdateAssistedHighlightIcon() end
      end
      self:SetScript("OnUpdate", nil)
      return
    end
    if pct >= 1 then
      -- Preview beenden
      self.rCCBPreview = false
      self:SetScript("OnUpdate", nil)
      PreviewPopTopLayer(self)
      if assistedHighlightPreviewMode then
        assistedHighlightPreviewMode = false
        local a = rCompassCastbarFrames.assistedHighlight
        if a then PreviewPopTopLayer(a); a:Hide(); if a.cursorAnchor then a.cursorAnchor:Hide() end end
        if rCompassCastbarFrames.UpdateAssistedHighlightIcon then rCompassCastbarFrames.UpdateAssistedHighlightIcon() end
      end
      self:Hide()
      if self.cursorAnchor then self.cursorAnchor:Hide() end
      if self.castIcon then self.castIcon:Hide() end
      self.isEnabled = false
    end
  end)
end

-----------------------------
-- Functions
-----------------------------

local function UnitTokenForClassColor(self)
  if self.unit == "gcd" then
    return "player"
  end
  return self.unit
end

local function GetRingBaseColor(self)
  local rc = self.cfgRing.color
  local r, g, b, a = rc[1], rc[2], rc[3], rc[4]
  if useClassColor then
    local token = UnitTokenForClassColor(self)
    local _, class = UnitClass(token)
    if not class and token == "pet" then
      _, class = UnitClass("player")
    end
    if class then
      local c = C_ClassColor.GetClassColor(class)
      if c then
        r, g, b = c.r, c.g, c.b
      end
    end
  end
  return r, g, b, a
end

local function ApplyRingVertexColors(self,brightness)
  local r, g, b, a = GetRingBaseColor(self)
  if brightness and brightness < 1 then
    r = r * brightness
    g = g * brightness
    b = b * brightness
  end
  self.leftRingTexture:SetVertexColor(r, g, b, a)
  self.rightRingTexture:SetVertexColor(r, g, b, a)
end

-- Align player ring to rCompassCastbarCfg (same keys as GUI) so preview matches current settings
SyncPlayerFrameFromSavedCfg = function(f)
  if not f or f.unit ~= "player" then return end
  local t = _G.rCompassCastbarCfg
  local function num(key, def)
    local v = t and t[key]
    if type(v) == "number" then return v end
    return def
  end
  local function bool(key, def)
    if not t or t[key] == nil then return def end
    local v = t[key]
    if v == true or v == 1 then return true end
    if v == false or v == 0 then return false end
    return def
  end
  f:SetScale(num("playerScale", 0.16))
  local prc = t and type(t.playerRingColor) == "table" and t.playerRingColor
  local dRing = {1, 0.8, 0, 1}
  for i = 1, 3 do
    f.cfgRing.color[i] = (prc and prc[i]) or dRing[i]
  end
  f.cfgRing.color[4] = (prc and prc[4] ~= nil) and prc[4] or 1
  local oldUcc = useClassColor
  useClassColor = bool("useClassColor", oldUcc)
  ApplyRingVertexColors(f)
  local r, g, b, a = GetRingBaseColor(f)
  local spOn = bool("playerSparkEnabled", true)
  if f.leftRingSpark then
    f.leftRingSpark:SetVertexColor(r, g, b, a)
    if spOn then f.leftRingSpark:Show() else f.leftRingSpark:Hide() end
  end
  if f.rightRingSpark then
    f.rightRingSpark:SetVertexColor(r, g, b, a)
    if spOn then f.rightRingSpark:Show() else f.rightRingSpark:Hide() end
  end
  useClassColor = oldUcc
  local pbg = t and type(t.playerBgColor) == "table" and t.playerBgColor
  if f.bg then
    if pbg and pbg[1] ~= nil then
      f.bg:SetVertexColor(pbg[1], pbg[2], pbg[3], pbg[4] ~= nil and pbg[4] or 1)
    end
    if bool("playerBgEnabled", true) then f.bg:Show() else f.bg:Hide() end
  end
  if f.castIcon then
    f.castIcon:SetSize(num("castIconSize", 160), num("castIconSize", 160))
    f.castIcon:ClearAllPoints()
    f.castIcon:SetPoint("TOPRIGHT", f, "TOPRIGHT", num("castIconOffsetX", -170), num("castIconOffsetY", 0))
    if bool("showCurrentCastIcon", true) then f.castIcon:Show() else f.castIcon:Hide() end
  end
end

local function ApplyEmpowerStageRingColor(self)
  if not useEmpowerStageColoring or not self.isEmpowering or not UnitEmpoweredStagePercentages then
    ApplyRingVertexColors(self)
    return
  end
  local rawStages = UnitEmpoweredStagePercentages(self.unit, true)
  if not rawStages or #rawStages == 0 then
    rawStages = UnitEmpoweredStagePercentages(self.unit)
  end
  if not rawStages or #rawStages == 0 then
    ApplyRingVertexColors(self)
    return
  end
  local stageCount = #rawStages
  local expectedStages = self.empowerNumStages
  local thresholds = {}
  local useRawAsThresholds = false

  if expectedStages and expectedStages > 0 then
    if stageCount == expectedStages - 1 then
      useRawAsThresholds = true
    elseif stageCount == expectedStages then
      useRawAsThresholds = type(rawStages[stageCount]) == "number" and rawStages[stageCount] >= 0.95
    end
  end

  if not useRawAsThresholds then
    local sum, monotonic = 0, true
    for i = 1, stageCount do
      local v = rawStages[i]
      if type(v) ~= "number" then
        ApplyRingVertexColors(self)
        return
      end
      sum = sum + v
      if i > 1 and rawStages[i] < rawStages[i - 1] then
        monotonic = false
      end
    end
    if monotonic and sum > 1.05 then
      useRawAsThresholds = true
    end
  end

  if useRawAsThresholds then
    for i = 1, stageCount do
      thresholds[i] = math.min(math.max(rawStages[i], 0), 1)
    end
  else
    local cumulative = 0
    for i = 1, stageCount do
      cumulative = cumulative + rawStages[i]
      thresholds[i] = math.min(math.max(cumulative, 0), 1)
    end
  end

  if expectedStages and expectedStages > stageCount then
    for i = stageCount + 1, expectedStages do
      thresholds[i] = 1
    end
    stageCount = expectedStages
  elseif thresholds[stageCount] and thresholds[stageCount] < 0.999 then
    stageCount = stageCount + 1
    thresholds[stageCount] = 1
  end

  local currentStage = stageCount
  for i = 1, stageCount do
    local threshold = thresholds[i]
    if type(threshold) == "number" and self.percent <= threshold then
      currentStage = i
      break
    end
  end
  if currentStage < 1 then currentStage = 1 end
  if currentStage > stageCount then currentStage = stageCount end
  local brightness
  if stageCount <= 1 then
    brightness = 1
  else
    brightness = 0.4 + 0.6 * ((currentStage - 1) / (stageCount - 1))
  end
  ApplyRingVertexColors(self,brightness)
end

local function ApplyFrameAlpha(self, alpha)
  self:SetAlpha(alpha)
  if self.cursorAnchor then
    self.cursorAnchor:SetAlpha(alpha)
  end
end

local OnUpdate
local assistedHighlightFrame
local assistedHighlightPreviewMode = false
local assistedHighlightLastShownSpellID
local assistedHighlightLastCastActive = false
local assistedHighlightCastActive = false

local function IsAssistedHighlightCVarEnabled()
  if GetCVarBool then
    return GetCVarBool("assistedCombatHighlight") == true
  end
  if C_CVar and C_CVar.GetCVar then
    return C_CVar.GetCVar("assistedCombatHighlight") == "1"
  end
  if GetCVar then
    return GetCVar("assistedCombatHighlight") == "1"
  end
  return false
end

local function GetAssistedSuggestedSpellID()
  if not (C_AssistedCombat and C_AssistedCombat.GetNextCastSpell) then
    return nil
  end
  local spellID = tonumber(C_AssistedCombat.GetNextCastSpell())
  if spellID and spellID > 0 then
    return spellID
  end
  return nil
end

local function GetPlayerCastSpellTexture()
  local _, _, castTexture = UnitCastingInfo("player")
  if castTexture then
    return castTexture
  end
  local _, _, channelTexture = UnitChannelInfo("player")
  if channelTexture then
    return channelTexture
  end
  return nil
end

local function IsPlayerCastingOrChanneling()
  local castName = UnitCastingInfo("player")
  if castName then return true end
  local channelName = UnitChannelInfo("player")
  if channelName then return true end
  return false
end

local function CopyTextureVisual(dst, src)
  if not (dst and src) then return false end
  local atlas = src.GetAtlas and src:GetAtlas()
  if atlas and atlas ~= "" and dst.SetAtlas then
    dst:SetAtlas(atlas, true)
    return true
  end
  local texture = src:GetTexture()
  if texture then
    dst:SetTexture(texture)
    local l, r, t, b = src:GetTexCoord()
    if l and r and t and b then dst:SetTexCoord(l, r, t, b) end
    return true
  end
  return false
end

local function ApplyAssistedArrowTexture(texture)
  if not texture then return false, "no texture target" end
  if assistedArrowTexture and assistedArrowTexture ~= "" then
    texture:SetTexture(assistedArrowTexture)
    texture:SetBlendMode("BLEND")
    texture:SetAlpha(1)
    texture:SetVertexColor(1, 1, 1, 1)
    texture:SetTexCoord(0, 1, 0, 1)
    return true, "static assist_arrow.tga", {
      source = "static-assist-arrow-tga",
      shown = 1, alpha = 1, vertexAlpha = 1, blendMode = "BLEND",
    }
  end
  local chosenInfo = nil
  local debugSeenRotationFrames = 0
  local debugSeenActiveTextures = 0
  local debugSeenInactiveTextures = 0
  local debugSeenButtons = 0
  local debugSeenChildCandidates = 0
  local debugSeenRegions = 0
  local debugSeenDotGlobals = 0
  local hasAssistedButtonsFlag = nil
  local function ProbeButtonTextureIds(buttonName)
    local btn = _G[buttonName]
    if not btn then return "no-button" end
    local seen = {}
    local out = {}
    local function addTex(tex)
      if not tex then return end
      local key = tostring(tex)
      if seen[key] then return end
      seen[key] = true
      out[#out + 1] = key
    end
    if btn.GetRegions then
      local rc = select("#", btn:GetRegions())
      for i = 1, rc do
        local r = select(i, btn:GetRegions())
        if r and r.GetObjectType and r:GetObjectType() == "Texture" and r.GetTexture then addTex(r:GetTexture()) end
      end
    end
    if btn.GetChildren then
      local cc = select("#", btn:GetChildren())
      for i = 1, cc do
        local c = select(i, btn:GetChildren())
        if c and c.GetRegions then
          local crc = select("#", c:GetRegions())
          for j = 1, crc do
            local cr = select(j, c:GetRegions())
            if cr and cr.GetObjectType and cr:GetObjectType() == "Texture" and cr.GetTexture then addTex(cr:GetTexture()) end
          end
        end
      end
    end
    if #out == 0 then return "none" end
    local maxN = math.min(#out, 12)
    return table.concat(out, ",", 1, maxN)
  end
  local actionButtonUtil = _G.ActionButtonUtil
  local buttonNames = (actionButtonUtil and actionButtonUtil.ActionBarButtonNames) or _G.DEFAULT_ACTION_BUTTON_NAMES
  local buttonCount = tonumber(_G.NUM_ACTIONBAR_BUTTONS) or 12

  local function ForEachActionButton(callback)
    if type(buttonNames) ~= "table" then return false end
    for _, prefix in ipairs(buttonNames) do
      for index = 1, buttonCount do
        local btn = _G[prefix .. index]
        if btn then
          debugSeenButtons = debugSeenButtons + 1
          if callback(btn, prefix, index) then return true end
        end
      end
    end
    return false
  end

  local function ResolveButtonActionSlot(btn)
    if not btn then return nil end
    local slot = tonumber(btn.action)
    if not slot and btn.GetAttribute then slot = tonumber(btn:GetAttribute("action")) end
    if slot and slot > 0 then return slot end
    return nil
  end

  local function ResolveRotationFrameForButton(btn)
    if not btn then return nil end
    if btn.AssistedCombatRotationFrame then return btn.AssistedCombatRotationFrame end
    local name = btn.GetName and btn:GetName()
    if name and name ~= "" then
      local direct = _G[name .. "AssistedCombatRotationFrame"]
      if direct then return direct end
      local dotted = _G[name .. ".AssistedCombatRotationFrame"]
      if dotted then debugSeenDotGlobals = debugSeenDotGlobals + 1; return dotted end
    end
    if btn.GetChildren then
      local childCount = select("#", btn:GetChildren())
      for i = 1, childCount do
        local child = select(i, btn:GetChildren())
        if child and child.GetObjectType and child:GetObjectType() == "Frame" then
          local childName = child.GetName and child:GetName()
          if (childName and childName:find("AssistedCombatRotationFrame")) or child.ActiveTexture or child.InactiveTexture then
            debugSeenChildCandidates = debugSeenChildCandidates + 1
            return child
          end
        end
      end
    end
    return nil
  end

  local function TryGlobalRotationTexturesForButton(prefix, index)
    local base = prefix .. index .. "AssistedCombatRotationFrame"
    local active = _G[base .. "ActiveTexture"]
    local inactive = _G[base .. "InactiveTexture"]
    if (not active) and (not inactive) then
      local dottedBase = prefix .. index .. ".AssistedCombatRotationFrame"
      active = _G[dottedBase .. ".ActiveTexture"]
      inactive = _G[dottedBase .. ".InactiveTexture"]
      if active or inactive then debugSeenDotGlobals = debugSeenDotGlobals + 1 end
    end
    if active and CopyTextureVisual(texture, active) then
      texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
      debugSeenActiveTextures = debugSeenActiveTextures + 1
      chosenInfo = { source=base..":ActiveTexture", shown=active.IsShown and (active:IsShown() and 1 or 0) or -1,
        alpha=active.GetAlpha and active:GetAlpha() or -1, vertexAlpha=select(4,active:GetVertexColor()) or -1,
        blendMode=active.GetBlendMode and tostring(active:GetBlendMode()) or "n/a" }
      return true
    end
    if inactive and CopyTextureVisual(texture, inactive) then
      texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
      debugSeenInactiveTextures = debugSeenInactiveTextures + 1
      chosenInfo = { source=base..":InactiveTexture", shown=inactive.IsShown and (inactive:IsShown() and 1 or 0) or -1,
        alpha=inactive.GetAlpha and inactive:GetAlpha() or -1, vertexAlpha=select(4,inactive:GetVertexColor()) or -1,
        blendMode=inactive.GetBlendMode and tostring(inactive:GetBlendMode()) or "n/a" }
      return true
    end
    return false
  end

  local function FindRotationFrameFromAssistedSlots()
    if not (C_ActionBar and C_ActionBar.FindAssistedCombatActionButtons and C_ActionBar.HasAssistedCombatActionButtons) then
      return nil, nil
    end
    if not C_ActionBar.HasAssistedCombatActionButtons() then
      hasAssistedButtonsFlag = false; return nil, nil
    end
    hasAssistedButtonsFlag = true
    local slots = C_ActionBar.FindAssistedCombatActionButtons()
    if type(slots) ~= "table" then return nil, nil end
    local slotSet = {}
    for _, v in ipairs(slots) do
      local slot = tonumber(v); if slot and slot > 0 then slotSet[slot] = true end
    end
    for k, v in pairs(slots) do
      if v == true and type(k) == "number" then
        local slot = tonumber(k); if slot and slot > 0 then slotSet[slot] = true end
      end
    end
    local foundButton, foundName
    ForEachActionButton(function(btn, prefix, index)
      local slot = ResolveButtonActionSlot(btn)
      if slot and slotSet[slot] then
        foundButton = btn; foundName = prefix .. index; return true
      end
      return false
    end)
    if foundButton then return ResolveRotationFrameForButton(foundButton), foundButton, foundName end
    return nil, nil, nil
  end

  local hardcodedPrefixes = {
    "ActionButton","MultiBarBottomLeftButton","MultiBarBottomRightButton",
    "MultiBarLeftButton","MultiBarRightButton","MultiBar5Button","MultiBar6Button","MultiBar7Button",
  }

  local function TryRotationFrame(rot, sourceButton, sourceName)
    if not rot then return false end
    debugSeenRotationFrames = debugSeenRotationFrames + 1
    local activeTexture = rot.ActiveTexture
    local inactiveTexture = rot.InactiveTexture
    if (not activeTexture or not inactiveTexture) and rot.GetName then
      local rname = rot:GetName()
      if rname and rname ~= "" then
        activeTexture = activeTexture or _G[rname .. "ActiveTexture"]
        inactiveTexture = inactiveTexture or _G[rname .. "InactiveTexture"]
      end
    end
    if activeTexture then debugSeenActiveTextures = debugSeenActiveTextures + 1 end
    if inactiveTexture then debugSeenInactiveTextures = debugSeenInactiveTextures + 1 end
    local function captureAndApply(srcTexture, srcLabel)
      if not srcTexture or not CopyTextureVisual(texture, srcTexture) then return false end
      texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
      local _, _, _, sa = srcTexture:GetVertexColor()
      chosenInfo = { source=sourceName..":"..srcLabel,
        shown=srcTexture.IsShown and (srcTexture:IsShown() and 1 or 0) or -1,
        alpha=srcTexture.GetAlpha and srcTexture:GetAlpha() or -1, vertexAlpha=sa or -1,
        blendMode=srcTexture.GetBlendMode and tostring(srcTexture:GetBlendMode()) or "n/a",
        sourceButton=sourceButton, sourceRotationFrame=rot }
      return true
    end
    if activeTexture and activeTexture.IsShown and activeTexture:IsShown() and captureAndApply(activeTexture,"ActiveTexture") then return true end
    if inactiveTexture and inactiveTexture.IsShown and inactiveTexture:IsShown() and captureAndApply(inactiveTexture,"InactiveTexture") then return true end
    if activeTexture and captureAndApply(activeTexture,"ActiveTextureHidden") then return true end
    if inactiveTexture and captureAndApply(inactiveTexture,"InactiveTextureHidden") then return true end
    return false
  end

  local TryRegionScanForButton

  local function HardcodedButtonScan()
    for _, prefix in ipairs(hardcodedPrefixes) do
      for index = 1, 12 do
        local btn = _G[prefix .. index]
        if TryGlobalRotationTexturesForButton(prefix, index) then return true, prefix..index.." (global textures)" end
        if btn then
          debugSeenButtons = debugSeenButtons + 1
          if TryRotationFrame(ResolveRotationFrameForButton(btn), btn, prefix..index) then return true, prefix..index end
          if TryRegionScanForButton(btn, prefix..index) then return true, prefix..index.." (region heuristic)" end
        end
      end
    end
    return false, nil
  end

  TryRegionScanForButton = function(btn, sourceName)
    if not (btn and btn.GetRegions) then return false end
    local preferredRegion, preferredTexture
    local bw, bh = btn:GetSize()
    local buttonArea = (bw and bh and bw > 0 and bh > 0) and (bw * bh) or nil
    local bestRegion, bestScore = nil, -1
    local regionCount = select("#", btn:GetRegions())
    for i = 1, regionCount do
      local region = select(i, btn:GetRegions())
      if region and region.GetObjectType and region:GetObjectType() == "Texture" then
        debugSeenRegions = debugSeenRegions + 1
        local tex = region:GetTexture()
        if tex and region.IsShown and region:IsShown() then
          if tex == assistedArrowPreferredTextureA or tex == assistedArrowPreferredTextureB then
            preferredRegion = region; preferredTexture = tex
          end
          local w, h = region:GetSize()
          local blend = region.GetBlendMode and tostring(region:GetBlendMode()) or "n/a"
          local area = (w or 0) * (h or 0)
          local score = 0
          if buttonArea and buttonArea > 0 then
            local diff = math.abs(area - (buttonArea * 1.15)); score = score - diff
          else score = score + area end
          if blend == "BLEND" or blend == "ADD" then score = score + 10000 end
          if tex == assistedArrowVerifiedTexture then score = score + 25000 end
          if tex == assistedArrowPreferredTextureA or tex == assistedArrowPreferredTextureB then score = score + 50000 end
          if score > bestScore then bestScore = score; bestRegion = region end
        end
      end
    end
    if preferredRegion and CopyTextureVisual(texture, preferredRegion) then
      texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
      chosenInfo = { source=sourceName..":PreferredRegion("..tostring(preferredTexture)..")", shown=1,
        alpha=preferredRegion.GetAlpha and preferredRegion:GetAlpha() or -1,
        vertexAlpha=select(4,preferredRegion:GetVertexColor()) or -1,
        blendMode=preferredRegion.GetBlendMode and tostring(preferredRegion:GetBlendMode()) or "n/a",
        sourceButton=btn, sourceRegion=preferredRegion }
      return true
    end
    if bestRegion and CopyTextureVisual(texture, bestRegion) then
      texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
      local tex = bestRegion:GetTexture()
      chosenInfo = { source=sourceName..":RegionHeuristic("..tostring(tex)..")", shown=1,
        alpha=bestRegion.GetAlpha and bestRegion:GetAlpha() or -1,
        vertexAlpha=select(4,bestRegion:GetVertexColor()) or -1,
        blendMode=bestRegion.GetBlendMode and tostring(bestRegion:GetBlendMode()) or "n/a",
        sourceButton=btn, sourceRegion=bestRegion }
      return true
    end
    return false
  end

  local assistedRot, assistedBtn, assistedName = FindRotationFrameFromAssistedSlots()
  if TryRotationFrame(assistedRot, assistedBtn, assistedName or "assisted-slot") then
    return true, (chosenInfo and chosenInfo.source) or ("copied assisted-slot rotation frame from "..tostring(assistedName)), chosenInfo
  end
  if TryRegionScanForButton(assistedBtn, tostring(assistedName or "assisted-slot")) then
    return true, (chosenInfo and chosenInfo.source) or ("copied assisted-slot region heuristic from "..tostring(assistedName)), chosenInfo
  end

  if hasAssistedButtonsFlag == false then
    texture:SetTexture(nil); texture:SetAlpha(0)
    texture:SetTexture(assistedArrowPreferredTextureA)
    texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
    return false, "no assisted buttons; static assisted arrow texture", {
      source="static-no-assisted-buttons", shown=1, alpha=1, vertexAlpha=1, blendMode="BLEND",
      seenRotationFrames=debugSeenRotationFrames, seenActiveTextures=debugSeenActiveTextures,
      seenInactiveTextures=debugSeenInactiveTextures, seenButtons=debugSeenButtons,
      seenChildCandidates=debugSeenChildCandidates, seenRegions=debugSeenRegions, seenDotGlobals=debugSeenDotGlobals,
    }
  end

  if ForEachActionButton(function(btn, prefix, index)
    local rot = ResolveRotationFrameForButton(btn)
    if TryRotationFrame(rot, btn, prefix..index) then return true end
    if TryRegionScanForButton(btn, prefix..index) then return true end
    return false
  end) then
    return true, (chosenInfo and chosenInfo.source) or "copied assisted texture from full button scan", chosenInfo
  end

  local foundHardcoded, hardcodedName = HardcodedButtonScan()
  if foundHardcoded then
    return true, (chosenInfo and chosenInfo.source) or ("copied rotation frame from hardcoded scan "..tostring(hardcodedName)), chosenInfo
  end

  texture:SetTexture(nil); texture:SetAlpha(0)
  texture:SetTexture(assistedArrowPreferredTextureA)
  texture:SetBlendMode("BLEND"); texture:SetAlpha(1); texture:SetVertexColor(1,1,1,1)
  return false, "no assisted arrow source", {
    source="fallback", shown=1, alpha=texture.GetAlpha and texture:GetAlpha() or -1,
    vertexAlpha=1, blendMode="BLEND",
    seenRotationFrames=debugSeenRotationFrames, seenActiveTextures=debugSeenActiveTextures,
    seenInactiveTextures=debugSeenInactiveTextures, seenButtons=debugSeenButtons,
    seenChildCandidates=debugSeenChildCandidates, seenRegions=debugSeenRegions, seenDotGlobals=debugSeenDotGlobals,
    probeBR8=ProbeButtonTextureIds("MultiBarBottomRightButton8"),
  }
end

local function ApplyAssistedArrowGeometry(assistedArrow, iconFrame, sourceInfo)
  if not (assistedArrow and iconFrame and sourceInfo and sourceInfo.sourceButton) then return false end
  local btn = sourceInfo.sourceButton
  local bw, bh = btn:GetSize()
  if not (bw and bh) or bw <= 0 or bh <= 0 then return false end
  local rw, rh, rx, ry
  if sourceInfo.sourceRotationFrame then
    local rot = sourceInfo.sourceRotationFrame
    rw, rh = rot:GetSize(); rx, ry = rot:GetCenter()
  elseif sourceInfo.sourceRegion then
    local region = sourceInfo.sourceRegion
    rw, rh = region:GetSize(); rx, ry = region:GetCenter()
  end
  if not (rw and rh) or rw <= 0 or rh <= 0 then return false end
  local iconW, iconH = iconFrame:GetSize()
  local targetW = iconW * (rw / bw)
  local targetH = iconH * (rh / bh)
  local bx, by = btn:GetCenter()
  local offX, offY = 0, 0
  if bx and by and rx and ry then
    local btnScale = btn:GetEffectiveScale() or 1
    local dx = (rx - bx) / btnScale
    local dy = (ry - by) / btnScale
    offX = dx * (iconW / bw)
    offY = dy * (iconH / bh)
  end
  assistedArrow:ClearAllPoints()
  assistedArrow:SetSize(targetW, targetH)
  assistedArrow:SetPoint("CENTER", iconFrame, "CENTER", offX, offY)
  return true
end

local function SyncAssistedHighlightFromSavedCfg(af)
  if not af then return end
  local t = _G.rCompassCastbarCfg
  local function num(key, def)
    local v = t and t[key]
    if type(v) == "number" then return v end
    return def
  end
  local sz = num("assistedHighlightIconSize", 160)
  af:SetSize(sz, sz)
  af:SetScale(num("assistedHighlightIconScale", 0.2))
  local ox = num("assistedHighlightIconOffsetX", 320)
  local oy = num("assistedHighlightIconOffsetY", 0)
  if af.cursorAnchor then
    af:ClearAllPoints()
    af:SetPoint("CENTER", af.cursorAnchor, "CENTER", ox, oy)
  end
end

local function UpdateAssistedHighlightIcon()
  if not assistedHighlightFrame then return end
  if assistedHighlightPreviewMode then
    local t = _G.rCompassCastbarCfg
    local function bool(key, def)
      if not t or t[key] == nil then return def end
      local v = t[key]
      if v == true or v == 1 then return true end
      if v == false or v == 0 then return false end
      return def
    end
    if not bool("showAssistedHighlightIcon", showAssistedHighlightIcon) then
      assistedHighlightFrame:Hide()
      return
    end
    SyncAssistedHighlightFromSavedCfg(assistedHighlightFrame)
    assistedHighlightFrame.fadeIn = false
    assistedHighlightFrame.fadeOut = false
    assistedHighlightFrame:SetAlpha(1)
    if assistedHighlightFrame.cursorAnchor then
      assistedHighlightFrame.cursorAnchor:SetAlpha(1)
    end
    assistedHighlightFrame.texture:SetTexture(135887)
    assistedHighlightFrame:Show()
    return
  end
  if not showAssistedHighlightIcon or not IsAssistedHighlightCVarEnabled() then
    assistedHighlightFrame:Hide()
    return
  end
  local castActive = assistedHighlightCastActive or IsPlayerCastingOrChanneling()
  if assistedHighlightLastCastActive ~= castActive then
    assistedHighlightLastCastActive = castActive
  end
  if not castActive then
    assistedHighlightFrame:Hide()
    return
  end
  local spellID = GetAssistedSuggestedSpellID()
  if not spellID then
    local fallbackTexture = GetPlayerCastSpellTexture()
    if fallbackTexture then
      assistedHighlightFrame.texture:SetTexture(fallbackTexture)
      if not assistedHighlightFrame:IsShown() then
        assistedHighlightFrame.fadeOut = false; assistedHighlightFrame.fadeOutElapsed = 0
        assistedHighlightFrame.fadeIn = true; assistedHighlightFrame.fadeInElapsed = 0
        assistedHighlightFrame:SetAlpha(0)
        if assistedHighlightFrame.cursorAnchor then assistedHighlightFrame.cursorAnchor:SetAlpha(0) end
        assistedHighlightFrame:Show()
      end
      return
    end
    if not assistedHighlightLastShownSpellID then assistedHighlightFrame:Hide() end
    return
  end
  local spellInfo = C_Spell.GetSpellInfo(spellID)
  if not spellInfo or not spellInfo.iconID then
    assistedHighlightFrame:Hide()
    return
  end
  if assistedHighlightLastShownSpellID ~= spellID or not assistedHighlightFrame:IsShown() then
    assistedHighlightLastShownSpellID = spellID
  end
  assistedHighlightFrame.texture:SetTexture(spellInfo.iconID)
  if not assistedHighlightFrame:IsShown() then
    assistedHighlightFrame.fadeOut = false; assistedHighlightFrame.fadeOutElapsed = 0
    assistedHighlightFrame.fadeIn = true; assistedHighlightFrame.fadeInElapsed = 0
    assistedHighlightFrame:SetAlpha(0)
    if assistedHighlightFrame.cursorAnchor then assistedHighlightFrame.cursorAnchor:SetAlpha(0) end
    assistedHighlightFrame:Show()
  end
end

rCompassCastbarFrames.UpdateAssistedHighlightIcon = UpdateAssistedHighlightIcon
rCompassCastbarFrames.SyncAssistedHighlightFromSavedCfg = SyncAssistedHighlightFromSavedCfg
rCompassCastbarFrames.SyncPlayerFrameFromSavedCfg = SyncPlayerFrameFromSavedCfg

--Disable func
local function Disable(self, immediate)
  self.update, self.isCasting, self.isEnabled = false,false,false
  self.isChanneling = false
  self.isEmpowering = false
  self.empowerNumStages = nil
  self.spellName, self.spellText, self.spellTexture, self.startTime, self.endTime = nil,nil,nil,nil,nil,nil
  self.current, self.duration, self.elapsed, self.percent = 0,0,0,0
  if not self.fadeOut then
    self.fadeOut = true
    self.fadeOutElapsed = 0
  end
  self.fadeIn = false
  self.fadeInElapsed = 0
  if immediate then
    self:SetScript("OnUpdate",nil)
    ApplyFrameAlpha(self,0)
    self:Hide()
    if self.cursorAnchor then self.cursorAnchor:Hide() end
    return
  end
  self:SetScript("OnUpdate",OnUpdate)
end

--OnUpdate func
OnUpdate = function(self,elapsed)
  if self.fadeIn then
    self.fadeInElapsed = self.fadeInElapsed + elapsed
    local p = castbarFadeInDuration > 0 and math.min(self.fadeInElapsed / castbarFadeInDuration, 1) or 1
    ApplyFrameAlpha(self,p)
    if p >= 1 then self.fadeIn = false; self.fadeInElapsed = 0 end
  end

  if self.fadeOut then
    self.fadeOutElapsed = self.fadeOutElapsed + elapsed
    local p = castbarFadeOutDuration > 0 and math.min(self.fadeOutElapsed / castbarFadeOutDuration, 1) or 1
    ApplyFrameAlpha(self,1-p)
    if p >= 1 then
      self.fadeOut = false; self.fadeOutElapsed = 0
      self:SetScript("OnUpdate",nil); self:Hide()
      if self.castIcon then self.castIcon:Hide() end
      if self.cursorAnchor then self.cursorAnchor:Hide() end
      return
    end
    return
  end

  if not self.isEnabled then return end

  self.elapsed = self.elapsed + elapsed
  if self.update and self.unit ~= "gcd" then
    local isEmpowered, numStages
    if self.isEmpowering then
      self.spellName, self.spellText, self.spellTexture, self.startTime, self.endTime, _, _, _, isEmpowered, numStages = UnitChannelInfo(self.unit)
      self.isChanneling = false
      self.isEmpowering = isEmpowered and true or false
      self.empowerNumStages = (type(numStages) == "number" and numStages > 0) and numStages or nil
      if self.spellName and self.isEmpowering and GetUnitEmpowerHoldAtMaxTime then
        self.endTime = self.endTime + (GetUnitEmpowerHoldAtMaxTime(self.unit) or 0)
      end
    else
      self.spellName, self.spellText, self.spellTexture, self.startTime, self.endTime = UnitCastingInfo(self.unit)
      self.isChanneling = false; self.isEmpowering = false; self.empowerNumStages = nil
      if not self.spellName then
        self.spellName, self.spellText, self.spellTexture, self.startTime, self.endTime, _, _, _, isEmpowered, numStages = UnitChannelInfo(self.unit)
        if self.spellName then
          if isEmpowered then
            self.isEmpowering = true; self.isChanneling = false
            self.empowerNumStages = (type(numStages) == "number" and numStages > 0) and numStages or nil
            if GetUnitEmpowerHoldAtMaxTime then
              self.endTime = self.endTime + (GetUnitEmpowerHoldAtMaxTime(self.unit) or 0)
            end
          else
            self.isChanneling = true
          end
        end
      end
    end
    if not self.spellName then Disable(self); return end
    self.duration = (self.endTime - self.startTime) / 1000
    if self.duration <= 0 then Disable(self); return end
    self.current = GetTime() - self.startTime / 1000
    self.elapsed = 0; self.update = false
    if self.castIcon and showCurrentCastIcon and self.spellTexture then
      self.castIcon.texture:SetTexture(self.spellTexture)
      self.castIcon:Show()
    elseif self.castIcon then
      self.castIcon:Hide()
    end
  end
  if self.update and self.unit == "gcd" then
    local spellCooldownInfo = C_Spell.GetSpellCooldown(61304)
    if not spellCooldownInfo then Disable(self); return end
    self.startTime = spellCooldownInfo.startTime
    self.duration = spellCooldownInfo.duration
    if self.duration <= 0 then Disable(self); return end
    self.current = GetTime() - self.startTime
    self.elapsed = 0; self.update = false
    if self.castIcon then self.castIcon:Hide() end
  end
  if self.rightRingSpark and self.leftRingSpark then
    local sparkTexture = self.defaultSparkTexture
    if self.isChanneling then sparkTexture = self.channelSparkTexture end
    if self.activeSparkTexture ~= sparkTexture then
      self.rightRingSpark:SetTexture(sparkTexture)
      self.leftRingSpark:SetTexture(sparkTexture)
      self.activeSparkTexture = sparkTexture
    end
  end
  if self.current+self.elapsed-self.duration > 0 then
    if self.fadeOut then return end
    Disable(self)
    return
  end
  if self.cursorAnchor then
    local scale = UIParent:GetEffectiveScale()
    local x, y = GetCursorPosition()
    self.cursorAnchor:ClearAllPoints()
    self.cursorAnchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale)
  end
  self.percent = math.min(self.current+self.elapsed,self.duration)/self.duration
  ApplyEmpowerStageRingColor(self)
  if self.isChanneling then
    if self.percent > 0.5 then
      self.leftRingTexture:SetRotation(math.rad(self.leftRingTexture.baseDeg+180))
      self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg+180*(self.percent*2-1)))
    else
      self.leftRingTexture:SetRotation(math.rad(self.leftRingTexture.baseDeg+180*(self.percent*2)))
      self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg+0))
    end
    if self.rightRingSpark then self.rightRingSpark:SetRotation(math.rad(self.rightRingSpark.baseDeg+180*(self.percent*2))) end
    if self.leftRingSpark  then self.leftRingSpark:SetRotation(math.rad(self.leftRingSpark.baseDeg+180*(self.percent*2-1))) end
  else
    if self.percent > 0.5 then
      self.leftRingTexture:SetRotation(math.rad(self.leftRingTexture.baseDeg-180*(self.percent*2-1)))
      self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg-180))
    else
      self.leftRingTexture:SetRotation(math.rad(self.leftRingTexture.baseDeg-0))
      self.rightRingTexture:SetRotation(math.rad(self.rightRingTexture.baseDeg-180*(self.percent*2)))
    end
    if self.rightRingSpark then self.rightRingSpark:SetRotation(math.rad(self.rightRingSpark.baseDeg-180*(self.percent*2))) end
    if self.leftRingSpark  then self.leftRingSpark:SetRotation(math.rad(self.leftRingSpark.baseDeg-180*(self.percent*2-1))) end
  end
end

--Enable func
local function Enable(self)
  if self.isEnabled then return end
  if self.cursorAnchor then self.cursorAnchor:Show() end
  ApplyRingVertexColors(self)
  self:Show()
  self.fadeOut = false; self.fadeOutElapsed = 0
  self.fadeIn = true; self.fadeInElapsed = 0
  ApplyFrameAlpha(self,0)
  self:SetScript("OnUpdate",OnUpdate)
  self.isEnabled = true
end

--OnEvent func
local function OnEvent(self,event)
  if self.rCCBPreview then return end
  if self.unit ~= "gcd" and (event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_SUCCEEDED" or event == "UNIT_SPELLCAST_CHANNEL_STOP" or event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_EMPOWER_STOP") then
    if self.unit == "player" and assistedHighlightFrame then
      assistedHighlightCastActive = false
      assistedHighlightFrame.fadeIn = false; assistedHighlightFrame.fadeInElapsed = 0
      assistedHighlightFrame.fadeOut = true; assistedHighlightFrame.fadeOutElapsed = 0
      assistedHighlightLastShownSpellID = nil
    end
    Disable(self)
    return
  end
  if event == "UNIT_SPELLCAST_EMPOWER_START" or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
    self.isEmpowering = true
  elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
    self.isEmpowering = false
  end
  if self.unit ~= "gcd" then
    if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_EMPOWER_START" then
      if self.unit == "player" and assistedHighlightFrame then
        assistedHighlightCastActive = true
        assistedHighlightFrame.fadeOut = false; assistedHighlightFrame.fadeOutElapsed = 0
        assistedHighlightFrame.fadeIn = true; assistedHighlightFrame.fadeInElapsed = 0
        assistedHighlightFrame:SetAlpha(0)
        if assistedHighlightFrame.cursorAnchor then assistedHighlightFrame.cursorAnchor:SetAlpha(0) end
        UpdateAssistedHighlightIcon()
      end
      self.update = true
      Enable(self)
      return
    end
    if event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
      if self.isEnabled then self.update = true end
      return
    end
    return
  end
  self.update = true
  Enable(self)
end

--CreateCompassCastbar func
local function CreateCompassCastbar(unit,cfg)
  if not cfg.enabled then return end
  local f
  if cfg.point then
    f = CreateFrame("Frame", nil, UIParent)
    f.cursorAnchor = nil
    f:SetPoint(unpack(cfg.point))
  else
    local anchor = CreateFrame("Frame", nil, UIParent)
    anchor:SetSize(1, 1)
    anchor:SetFrameStrata("DIALOG")
    anchor:SetClampedToScreen(false)
    anchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", 0, 0)
    f = CreateFrame("Frame", nil, anchor)
    f.cursorAnchor = anchor
    f:SetPoint("CENTER", anchor, "CENTER", 0, 0)
  end
  f:SetSize(unpack(cfg.size))
  f:SetScale(cfg.scale)
  f.w, f.h = f:GetSize()
  if cfg.point then f:SetFrameStrata("DIALOG") end
  f.unit = unit
  f.cfgRing = cfg.ring
  if cfg.background and cfg.background.enabled then
    local t = f:CreateTexture(nil, "BACKGROUND", nil, -8)
    t:SetTexture(cfg.background.texture)
    t:SetAllPoints()
    t:SetVertexColor(unpack(cfg.background.color))
    t:SetBlendMode(cfg.background.blendmode)
    f.bg = t
  end
  local sf1 = CreateFrame("ScrollFrame",nil,f)
  sf1:SetSize(f.w/2,f.h); sf1:SetPoint("LEFT")
  local sc1 = CreateFrame("Frame")
  sf1:SetScrollChild(sc1); sc1:SetSize(f.w,f.h)
  local rt1 = sc1:CreateTexture(nil,"BACKGROUND",nil,-6)
  rt1:SetTexture(cfg.ring.texture); rt1:SetSize(f.w,f.h); rt1:SetPoint("CENTER")
  rt1:SetVertexColor(unpack(cfg.ring.color)); rt1:SetBlendMode(cfg.ring.blendmode)
  rt1.baseDeg = -180; f.leftRingTexture = rt1
  if cfg.spark and cfg.spark.enabled then
    local rs1 = sc1:CreateTexture(nil,"BACKGROUND",nil,-5)
    rs1:SetTexture(cfg.spark.texture); rs1:SetSize(f.w,f.h); rs1:SetPoint("CENTER")
    rs1:SetVertexColor(unpack(cfg.spark.color)); rs1:SetBlendMode(cfg.spark.blendmode)
    rs1.baseDeg = -180; f.leftRingSpark = rs1
  end
  local sf2 = CreateFrame("ScrollFrame",nil,f)
  sf2:SetSize(f.w/2,f.h); sf2:SetPoint("RIGHT")
  local sc2 = CreateFrame("Frame")
  sf2:SetScrollChild(sc2); sf2:SetHorizontalScroll(f.w/2); sc2:SetSize(f.w,f.h)
  local rt2 = sc2:CreateTexture(nil,"BACKGROUND",nil,-6)
  rt2:SetTexture(cfg.ring.texture); rt2:SetSize(f.w,f.h); rt2:SetPoint("CENTER")
  rt2:SetVertexColor(unpack(cfg.ring.color)); rt2:SetBlendMode(cfg.ring.blendmode)
  rt2.baseDeg = 0; f.rightRingTexture = rt2
  if cfg.spark and cfg.spark.enabled then
    local rs2 = sc2:CreateTexture(nil,"BACKGROUND",nil,-5)
    rs2:SetTexture(cfg.spark.texture); rs2:SetSize(f.w,f.h); rs2:SetPoint("CENTER")
    rs2:SetVertexColor(unpack(cfg.spark.color)); rs2:SetBlendMode(cfg.spark.blendmode)
    rs2.baseDeg = 0; f.rightRingSpark = rs2
  end
  if cfg.spark and cfg.spark.enabled then
    f.defaultSparkTexture = cfg.spark.texture
    f.channelSparkTexture = channelSparkTexture
    f.activeSparkTexture = nil
  end
  if unit ~= "gcd" then
    local castIcon = CreateFrame("Frame", nil, f)
    castIcon:SetSize(castIconSize, castIconSize)
    castIcon:SetPoint("TOPRIGHT", f, "TOPRIGHT", castIconOffsetX, castIconOffsetY)
    castIcon:SetFrameLevel(f:GetFrameLevel() + 4)
    castIcon:Hide()
    local iconTexture = castIcon:CreateTexture(nil, "ARTWORK", nil, 0)
    iconTexture:SetPoint("TOPLEFT", castIcon, "TOPLEFT", 4, -4)
    iconTexture:SetPoint("BOTTOMRIGHT", castIcon, "BOTTOMRIGHT", -4, 4)
    iconTexture:SetColorTexture(1, 0, 0, 1)
    castIcon.texture = iconTexture
    local border = castIcon:CreateTexture(nil, "OVERLAY", nil, 1)
    border:SetAllPoints(); border:SetTexture(iconBorderTexture)
    border:SetVertexColor(1, 1, 1, 1); castIcon.border = border
    f.castIcon = castIcon
    if not showCurrentCastIcon then castIcon:Hide() end
  end
  if unit ~= "gcd" then
    f:RegisterUnitEvent("UNIT_SPELLCAST_START", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_STOP", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_DELAYED", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_START", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_UPDATE", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_STOP", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", unit)
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", unit)
  end
  if unit == "pet" then f:RegisterUnitEvent("UNIT_PET","player") end
  if unit == "focus" then f:RegisterEvent("PLAYER_FOCUS_CHANGED") end
  if unit == "target" then f:RegisterEvent("PLAYER_TARGET_CHANGED") end
  if unit == "gcd" then
    f:RegisterEvent("SPELL_UPDATE_COOLDOWN")
    f:RegisterUnitEvent("UNIT_SPELLCAST_START", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_STOP", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_START", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_UPDATE", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_EMPOWER_STOP", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", "player")
    f:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player")
  end
  Disable(f,true)
  f:SetScript("OnEvent",OnEvent)
  -- In globale Frames-Tabelle eintragen
  tinsert(rCompassCastbarFrames.castbarFrames, f)
  if unit == "player" then rCompassCastbarFrames.playerFrame = f end
  if unit == "gcd"    then rCompassCastbarFrames.gcdFrame    = f end
  return f
end

local function CreateAssistedHighlightIcon()
  if not showAssistedHighlightIcon then return end
  local anchor = CreateFrame("Frame", nil, UIParent)
  anchor:SetSize(1, 1); anchor:SetFrameStrata("DIALOG")
  anchor:SetClampedToScreen(false)
  anchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", 0, 0)

  local icon = CreateFrame("Frame", nil, anchor)
  icon.cursorAnchor = anchor
  icon:SetPoint("CENTER", anchor, "CENTER", assistedHighlightIconOffsetX, assistedHighlightIconOffsetY)
  icon:SetSize(assistedHighlightIconSize, assistedHighlightIconSize)
  icon:SetScale(assistedHighlightIconScale)
  icon:SetFrameStrata("DIALOG"); icon:SetFrameLevel(100); icon:Hide()

  local iconTexture = icon:CreateTexture(nil, "ARTWORK", nil, 0)
  iconTexture:SetPoint("TOPLEFT", icon, "TOPLEFT", 4, -4)
  iconTexture:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -4, 4)
  icon.texture = iconTexture

  local border = icon:CreateTexture(nil, "OVERLAY", nil, 1)
  border:SetAllPoints(); border:SetTexture(iconBorderTexture)
  border:SetVertexColor(1, 1, 1, 1); icon.border = border

  local assistedArrow = icon:CreateTexture(nil, "OVERLAY", nil, 7)
  assistedArrow:SetSize(
    assistedHighlightIconSize * assistedArrowScaleMultiplier,
    assistedHighlightIconSize * assistedArrowScaleMultiplier
  )
  assistedArrow:SetPoint("CENTER", icon, "CENTER",
    assistedHighlightIconSize * assistedArrowOffsetXFactor,
    assistedHighlightIconSize * assistedArrowOffsetYFactor)
  assistedArrow:SetBlendMode("BLEND")
  local _, _, assistedArrowSourceInfo = ApplyAssistedArrowTexture(assistedArrow)
  ApplyAssistedArrowGeometry(assistedArrow, icon, assistedArrowSourceInfo)
  icon.assistedArrow = assistedArrow

  icon.eventFrame = CreateFrame("Frame")
  icon.eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "CVAR_UPDATE" then
      local cvarName = ...
      if cvarName ~= "assistedCombatHighlight" then return end
    end
    UpdateAssistedHighlightIcon()
  end)
  icon.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
  icon.eventFrame:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN")
  icon.eventFrame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
  icon.eventFrame:RegisterEvent("SPELL_UPDATE_CHARGES")
  icon.eventFrame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
  icon.eventFrame:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
  icon.eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
  icon.eventFrame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
  icon.eventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
  icon.eventFrame:RegisterEvent("CVAR_UPDATE")
  icon.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")

  icon:SetScript("OnUpdate", function(self, elapsed)
    local scale = UIParent:GetEffectiveScale()
    local x, y = GetCursorPosition()
    self.cursorAnchor:ClearAllPoints()
    self.cursorAnchor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale)
    if self.fadeIn then
      self.fadeInElapsed = self.fadeInElapsed + elapsed
      local p = castbarFadeInDuration > 0 and math.min(self.fadeInElapsed / castbarFadeInDuration, 1) or 1
      self:SetAlpha(p)
      if self.cursorAnchor then self.cursorAnchor:SetAlpha(p) end
      if p >= 1 then self.fadeIn = false; self.fadeInElapsed = 0 end
    end
    if self.fadeOut then
      self.fadeOutElapsed = self.fadeOutElapsed + elapsed
      local p = castbarFadeOutDuration > 0 and math.min(self.fadeOutElapsed / castbarFadeOutDuration, 1) or 1
      local a = 1 - p
      self:SetAlpha(a)
      if self.cursorAnchor then self.cursorAnchor:SetAlpha(a) end
      if p >= 1 then
        self.fadeOut = false; self.fadeOutElapsed = 0; self:Hide()
      end
      return
    end
    self._assistedElapsed = (self._assistedElapsed or 0) + elapsed
    if self._assistedElapsed >= 0.10 then
      self._assistedElapsed = 0
      UpdateAssistedHighlightIcon()
      if self.assistedArrow then
        local hasSource, _, sourceDetail = ApplyAssistedArrowTexture(self.assistedArrow)
        local geomApplied = ApplyAssistedArrowGeometry(self.assistedArrow, self, sourceDetail)
        if not hasSource and not geomApplied then
          self.assistedArrow:Hide()
        else
          self.assistedArrow:Show()
        end
      end
    end
  end)

  assistedHighlightFrame = icon
  rCompassCastbarFrames.assistedHighlight = icon
  UpdateAssistedHighlightIcon()
end

-----------------------------
-- SavedVariables (rCompassCastbarCfg): GUI → locals + ringCfg before frame build
-----------------------------
local function ApplyColor(dst, src)
  if type(src) ~= "table" or src[1] == nil then return end
  dst[1], dst[2], dst[3] = src[1], src[2], src[3]
  dst[4] = src[4] ~= nil and src[4] or 1
end

local function ApplySavedCfg()
  local t = _G.rCompassCastbarCfg
  if type(t) ~= "table" then
    _G.rCompassCastbarCfg = {}
    return
  end
  local function pick(k, cur)
    local v = t[k]
    if v == nil then return cur end
    return v
  end
  local function pickBool(k, cur)
    local v = t[k]
    if v == nil then return cur end
    if v == true or v == 1 then return true end
    if v == false or v == 0 then return false end
    return cur
  end

  useClassColor              = pickBool("useClassColor", useClassColor)
  useEmpowerStageColoring    = pickBool("useEmpowerStageColoring", useEmpowerStageColoring)
  showCurrentCastIcon        = pickBool("showCurrentCastIcon", showCurrentCastIcon)
  showAssistedHighlightIcon  = pickBool("showAssistedHighlightIcon", showAssistedHighlightIcon)
  castIconSize               = pick("castIconSize", castIconSize)
  castIconOffsetX            = pick("castIconOffsetX", castIconOffsetX)
  castIconOffsetY            = pick("castIconOffsetY", castIconOffsetY)
  assistedHighlightIconSize   = pick("assistedHighlightIconSize", assistedHighlightIconSize)
  assistedHighlightIconScale = pick("assistedHighlightIconScale", assistedHighlightIconScale)
  assistedHighlightIconOffsetX = pick("assistedHighlightIconOffsetX", assistedHighlightIconOffsetX)
  assistedHighlightIconOffsetY = pick("assistedHighlightIconOffsetY", assistedHighlightIconOffsetY)
  castbarFadeInDuration      = pick("castbarFadeInDuration", castbarFadeInDuration)
  castbarFadeOutDuration     = pick("castbarFadeOutDuration", castbarFadeOutDuration)
  overallScale               = pick("overallScale", overallScale)

  local rp = ringCfg.player
  local rg = ringCfg.gcd
  if rp then
    rp.enabled = pick("playerEnabled", rp.enabled)
    rp.scale = pick("playerScale", rp.scale)
    if t.playerFollowCursor ~= nil then
      rp.point = t.playerFollowCursor and nil or {"CENTER", 0, 0}
    end
    if t.playerBgEnabled ~= nil and rp.background then
      rp.background.enabled = t.playerBgEnabled
    end
    if t.playerSparkEnabled ~= nil and rp.spark then
      rp.spark.enabled = t.playerSparkEnabled
    end
    if type(t.playerRingColor) == "table" then ApplyColor(rp.ring.color, t.playerRingColor) end
    if type(t.playerBgColor) == "table" and rp.background then ApplyColor(rp.background.color, t.playerBgColor) end
  end
  if rg then
    rg.enabled = pick("gcdEnabled", rg.enabled)
    rg.scale = pick("gcdScale", rg.scale)
    if t.gcdBgEnabled ~= nil and rg.background then
      rg.background.enabled = t.gcdBgEnabled
    end
    if t.gcdSparkEnabled ~= nil and rg.spark then
      rg.spark.enabled = t.gcdSparkEnabled
    end
    if type(t.gcdRingColor) == "table" then ApplyColor(rg.ring.color, t.gcdRingColor) end
    if type(t.gcdBgColor) == "table" and rg.background then ApplyColor(rg.background.color, t.gcdBgColor) end
  end
end

-----------------------------
-- Init after PLAYER_LOGIN (SavedVariables reliably loaded)
-----------------------------
local addonInitFrame = CreateFrame("Frame")
addonInitFrame:RegisterEvent("PLAYER_LOGIN")
addonInitFrame:SetScript("OnEvent", function(self)
  self:UnregisterEvent("PLAYER_LOGIN")
  ApplySavedCfg()
  for unit, cfg in next, ringCfg do
    CreateCompassCastbar(unit, cfg)
  end
  CreateAssistedHighlightIcon()
end)

SLASH_RCCB1 = "/rccb"
SLASH_RCCB2 = "/rcompasscastbar"
SlashCmdList["RCCB"] = function()
  rCompassCastbar_ToggleConfig()
end
