local _, _ns = ...
local AC = _G.ArenaCore
if not AC then return end

-- =============================================================================
-- Core/BuffReminder.lua - Buff Reminder system
-- ported from Shade addon because forgetting buffs is embarrassing
-- shows glowing icons for class buffs you or your group are missing
-- =============================================================================

local BR = {}
AC.BuffReminder = BR

------------------------------------------------------------------------
-- class buff definitions -- spellID, icon, and whether its a targeted buff
-- raid-wide buffs: missing on player = show icon
-- targeted buffs: on player but NOT on a group member = show icon
------------------------------------------------------------------------
local EVOKER_BLESSING_OF_BRONZE_IDS = {
    381732, 381741, 381746, 381748, 381749, 381750,
    381751, 381752, 381753, 381754, 381756, 381757, 381758,
}

local CLASS_BUFFS = {
    DRUID       = {{ spellID = 1126,   name = "Mark of the Wild",       targeted = false }},
    MAGE        = {{ spellID = 1459,   name = "Arcane Intellect",       targeted = false }},
    PRIEST      = {{ spellID = 21562,  name = "Power Word: Fortitude",  targeted = false }},
    WARRIOR     = {{ spellID = 6673,   name = "Battle Shout",           targeted = false }},
    EVOKER      = {
        { spellIDs = EVOKER_BLESSING_OF_BRONZE_IDS, name = "Blessing of the Bronze", targeted = false },
        { spellID = 369459, name = "Source of Magic", targeted = true, knownSpellIDs = {369459} },
    },
    SHAMAN      = {
        { spellID = 974,    name = "Earth Shield", targeted = true, knownSpellIDs = {974, 383648} },
        { spellID = 462854, name = "Skyfury",      targeted = false },
    },
}

local CLASS_SELF_BUFFS = {
    SHAMAN = {
        {
            spellIDs = {52127, 79949, 79950, 192106},
            name = "Water Shield or Lightning Shield",
            iconSpellID = 192106,
            skipInBattleground = true,
        },
    },
}

local SHAMAN_EARTH_SHIELD_SPELLIDS = {974, 383648}

local CLASS_WEAPON_ENCHANTS = {
    SHAMAN = { name = "Weapon Imbue", iconSpellID = 318038 },
    ROGUE  = { name = "Poisons",      iconSpellID = 315584 },
}

local ROGUE_POISON_BUFFS = {
    lethal = {315584, 2823, 8679, 381664},
    nonLethal = {381637, 5761, 3408},
}

local WEAPON_EQUIP_LOCS = {
    INVTYPE_WEAPON = true,
    INVTYPE_2HWEAPON = true,
    INVTYPE_WEAPONMAINHAND = true,
    INVTYPE_WEAPONOFFHAND = true,
}

------------------------------------------------------------------------
-- state -- all external, nothing written onto blizzard frames
------------------------------------------------------------------------
local iconFrames    = {}
local anchorFrame   = nil
local eventFrame    = nil
local missingList   = {}
local throttle      = 0
local SCAN_INTERVAL = 0.5
local isDragging    = false

------------------------------------------------------------------------
-- helpers
------------------------------------------------------------------------
local function GetDB()
    if not AC.DB or not AC.DB.profile then
        return { enabled = false, iconSize = 40, spacing = 6, iconZoom = 0.08, posX = nil, posY = nil }
    end
    AC.DB.profile.buffReminder = AC.DB.profile.buffReminder or {}
    local db = AC.DB.profile.buffReminder
    if db.enabled    == nil then db.enabled    = false end
    if db.iconSize   == nil then db.iconSize   = 40    end
    if db.spacing    == nil then db.spacing     = 6     end
    if db.iconZoom   == nil then db.iconZoom   = 0.08  end
    if db.posX       == nil then db.posX       = nil   end
    if db.posY       == nil then db.posY       = nil   end

    if db.posX == nil and db.posY == nil then
        AC.DB.global = AC.DB.global or {}
        AC.DB.global.positions = AC.DB.global.positions or {}
        local globalPos = AC.DB.global.positions.buffReminder
        if globalPos and globalPos.posX ~= nil then
            db.posX = globalPos.posX
            db.posY = globalPos.posY
        end
    end

    return db
end

local function PlayerKnowsSpell(spellID)
    if not spellID then return false end
    if IsPlayerSpell and IsPlayerSpell(spellID) then return true end
    if IsSpellKnownOrOverridesKnown and IsSpellKnownOrOverridesKnown(spellID) then return true end
    if C_SpellBook and C_SpellBook.IsSpellInSpellBook then
        return C_SpellBook.IsSpellInSpellBook(spellID, Enum.SpellBookSpellBank.Player, true) == true
    end
    return false
end

local function PlayerKnowsAnySpell(spellIDs)
    if not spellIDs then return true end
    for _, spellID in ipairs(spellIDs) do
        if PlayerKnowsSpell(spellID) then
            return true
        end
    end
    return false
end

local function GetDefSpellIDs(def)
    if def.spellIDs then
        return def.spellIDs
    elseif def.spellID then
        return {def.spellID}
    end
    return {}
end

local function UnitHasAnyAuraSpellIDs(unit, spellIDs)
    if not UnitExists(unit) then return false end
    if not spellIDs or #spellIDs == 0 then return false end

    local idMap = {}
    for _, spellID in ipairs(spellIDs) do
        idMap[spellID] = true
    end

    -- Fast path: direct spellID lookup
    for _, spellID in ipairs(spellIDs) do
        local aura = C_UnitAuras.GetUnitAuraBySpellID(unit, spellID)
        if aura ~= nil then return true end
    end

    -- Fallback scan: needed in instances where direct lookups fail on group members
    -- GetAuraSlots returns (continuationToken, slot1, slot2, ...) — when continuationToken
    -- is nil, {nil, slot1, ...} has a hole at [1] and # is unreliable, so use while loop
    if C_UnitAuras.GetAuraSlots and C_UnitAuras.GetAuraDataBySlot then
        local nameMap
        local continuationToken
        for _ = 1, 10 do
            local args = {C_UnitAuras.GetAuraSlots(unit, "HELPFUL", continuationToken)}
            local i = 2
            while args[i] do
                local slot = args[i]
                if not (issecretvalue and issecretvalue(slot)) then
                    local data = C_UnitAuras.GetAuraDataBySlot(unit, slot)
                    if data and data.spellId and not (issecretvalue and issecretvalue(data.spellId)) and idMap[data.spellId] then
                        return true
                    end
                    -- name-based fallback on same pass to avoid double scanning
                    if not nameMap then
                        nameMap = {}
                        for _, sid in ipairs(spellIDs) do
                            local info = C_Spell.GetSpellInfo(sid)
                            if info and info.name and info.name ~= "" then
                                nameMap[info.name] = true
                            end
                        end
                    end
                    if next(nameMap) and data.name and not (issecretvalue and issecretvalue(data.name)) and nameMap[data.name] then
                        return true
                    end
                end
                i = i + 1
            end

            local nextToken = args[1]
            if nextToken == nil or (issecretvalue and issecretvalue(nextToken)) then break end
            continuationToken = nextToken
        end
    end

    return false
end

local function PlayerHasAnyAuraSpellIDs(spellIDs)
    return UnitHasAnyAuraSpellIDs("player", spellIDs)
end

local function HasBuff(unit, spellID)
    if not UnitExists(unit) then return false end
    local spellIDs = type(spellID) == "table" and spellID or {spellID}
    return UnitHasAnyAuraSpellIDs(unit, spellIDs)
end

local function PlayerHasBuff(spellID)
    local spellIDs = type(spellID) == "table" and spellID or {spellID}
    return PlayerHasAnyAuraSpellIDs(spellIDs)
end

local function PlayerHasAnyBuff(spellIDs)
    return PlayerHasAnyAuraSpellIDs(spellIDs)
end

local function GetGroupUnits()
    local units = {"player"}
    if IsInRaid() then
        for i = 1, GetNumGroupMembers() do
            units[#units + 1] = "raid" .. i
        end
    elseif IsInGroup() then
        local count = GetNumSubgroupMembers()
        for i = 1, count do
            units[#units + 1] = "party" .. i
        end
        -- some instanced content may expose raid tokens instead, catch those too
        for i = 1, 5 do
            local token = "raid" .. i
            if UnitExists(token) then
                local already = false
                for _, u in ipairs(units) do
                    if u == token then already = true break end
                end
                if not already then
                    units[#units + 1] = token
                end
            end
        end
    end
    return units
end

local function GetSpellIcon(spellID)
    local info = C_Spell.GetSpellInfo(spellID)
    return info and info.iconID or nil
end

local function SlotCanHaveTempEnchant(slotID)
    local itemID = GetInventoryItemID("player", slotID)
    if not itemID then return false end
    local _, _, _, equipLoc = C_Item.GetItemInfoInstant(itemID)
    return equipLoc and WEAPON_EQUIP_LOCS[equipLoc] == true
end

local function HasAnyBuffFromPlayer(unit, spellIDs)
    if not UnitExists(unit) then return false end
    local idMap = {}
    for _, spellID in ipairs(spellIDs) do
        idMap[spellID] = true
    end
    -- Fast path
    for _, spellID in ipairs(spellIDs) do
        local aura = C_UnitAuras.GetUnitAuraBySpellID(unit, spellID)
        if aura ~= nil and aura.sourceUnit == "player" then
            return true
        end
    end
    -- Slot fallback for instances
    if C_UnitAuras.GetAuraSlots and C_UnitAuras.GetAuraDataBySlot then
        local continuationToken
        for _ = 1, 10 do
            local args = {C_UnitAuras.GetAuraSlots(unit, "HELPFUL", continuationToken)}
            local i = 2
            while args[i] do
                local slot = args[i]
                if not (issecretvalue and issecretvalue(slot)) then
                    local data = C_UnitAuras.GetAuraDataBySlot(unit, slot)
                    if data and data.spellId and not (issecretvalue and issecretvalue(data.spellId)) and idMap[data.spellId] and data.sourceUnit == "player" then
                        return true
                    end
                end
                i = i + 1
            end
            local nextToken = args[1]
            if nextToken == nil or (issecretvalue and issecretvalue(nextToken)) then break end
            continuationToken = nextToken
        end
    end
    return false
end

local function PlayerHasAnyAuraBySpellID(spellIDs)
    if not spellIDs then return false end
    for _, spellID in ipairs(spellIDs) do
        local aura
        if C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID then
            aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
        end
        if aura == nil and C_UnitAuras and C_UnitAuras.GetUnitAuraBySpellID then
            aura = C_UnitAuras.GetUnitAuraBySpellID("player", spellID)
        end
        if aura ~= nil then
            return true
        end
    end
    -- Fallback for instances where direct lookups fail — same while-loop pattern
    if C_UnitAuras and C_UnitAuras.GetAuraSlots and C_UnitAuras.GetAuraDataBySlot then
        local idMap = {}
        for _, spellID in ipairs(spellIDs) do
            idMap[spellID] = true
        end
        local continuationToken
        for _ = 1, 10 do
            local args = {C_UnitAuras.GetAuraSlots("player", "HELPFUL", continuationToken)}
            local i = 2
            while args[i] do
                local slot = args[i]
                if not (issecretvalue and issecretvalue(slot)) then
                    local data = C_UnitAuras.GetAuraDataBySlot("player", slot)
                    if data and data.spellId and not (issecretvalue and issecretvalue(data.spellId)) and idMap[data.spellId] then
                        return true
                    end
                end
                i = i + 1
            end
            local nextToken = args[1]
            if nextToken == nil or (issecretvalue and issecretvalue(nextToken)) then break end
            continuationToken = nextToken
        end
    end
    return false
end

local function AddSelfBuffReminders(playerClass, list)
    local defs = CLASS_SELF_BUFFS[playerClass]
    if not defs then return end

    local _, instanceType = IsInInstance()
    local inInstancedPvP = instanceType == "arena" or instanceType == "pvp"

    for _, def in ipairs(defs) do
        local shouldCheck = true
        if def.skipInBattleground and inInstancedPvP then
            shouldCheck = false
        end
        if shouldCheck then
            local hasIt = PlayerHasAnyAuraBySpellID(def.spellIDs)
            if (not def.knownSpellID or PlayerKnowsSpell(def.knownSpellID)) and not hasIt then
                local icon = GetSpellIcon(def.iconSpellID or def.spellIDs[1])
                if icon then
                    list[#list + 1] = {
                        spellID = def.iconSpellID or def.spellIDs[1],
                        icon    = icon,
                        tip     = "You are missing " .. def.name .. "!",
                    }
                end
            end
        end
    end
end

local function AddWeaponEnchantReminder(playerClass, list)
    local rule = CLASS_WEAPON_ENCHANTS[playerClass]
    if not rule then return end

    if playerClass == "ROGUE" then
        local missingKinds = {}

        if not PlayerHasAnyBuff(ROGUE_POISON_BUFFS.lethal) then
            missingKinds[#missingKinds + 1] = "Lethal"
        end

        if not PlayerHasAnyBuff(ROGUE_POISON_BUFFS.nonLethal) then
            missingKinds[#missingKinds + 1] = "Non-Lethal"
        end

        if #missingKinds == 0 then
            return
        end

        local icon = GetSpellIcon(rule.iconSpellID)
        if icon then
            list[#list + 1] = {
                spellID = rule.iconSpellID,
                icon    = icon,
                tip     = rule.name .. " missing: " .. table.concat(missingKinds, " / ") .. "!",
            }
        end

        return
    end

    if playerClass == "SHAMAN" then
        local spec = GetSpecialization()
        local mainSlot = INVSLOT_MAINHAND or 16
        local offSlot = INVSLOT_OFFHAND or 17
        local hasMainHandEnchant, _, _, _, hasOffHandEnchant = GetWeaponEnchantInfo()
        local missingSlots = {}
        local imbueSpellID = rule.iconSpellID

        if spec == 3 then
            imbueSpellID = 382021
            if SlotCanHaveTempEnchant(mainSlot) and not hasMainHandEnchant then
                missingSlots[#missingSlots + 1] = "Main Hand"
            end
        elseif spec == 1 then
            imbueSpellID = 318038
            if SlotCanHaveTempEnchant(mainSlot) and not hasMainHandEnchant then
                missingSlots[#missingSlots + 1] = "Main Hand"
            end
        elseif spec == 2 then
            if SlotCanHaveTempEnchant(mainSlot) and not hasMainHandEnchant then
                missingSlots[#missingSlots + 1] = "Main Hand"
            end
            if SlotCanHaveTempEnchant(offSlot) and not hasOffHandEnchant then
                missingSlots[#missingSlots + 1] = "Off Hand"
            end
        end

        if #missingSlots == 0 then
            return
        end

        local icon = GetSpellIcon(imbueSpellID)
        if not icon then
            icon = GetInventoryItemTexture("player", mainSlot) or GetInventoryItemTexture("player", offSlot)
        end

        if icon then
            list[#list + 1] = {
                spellID = imbueSpellID,
                icon    = icon,
                tip     = rule.name .. " missing on " .. table.concat(missingSlots, " / ") .. "!",
            }
        end

        return
    end

    local mainSlot = INVSLOT_MAINHAND or 16
    local offSlot = INVSLOT_OFFHAND or 17
    local hasMainHandEnchant, _, _, _, hasOffHandEnchant = GetWeaponEnchantInfo()
    local missingSlots = {}

    if SlotCanHaveTempEnchant(mainSlot) and not hasMainHandEnchant then
        missingSlots[#missingSlots + 1] = "Main Hand"
    end

    if SlotCanHaveTempEnchant(offSlot) and not hasOffHandEnchant then
        missingSlots[#missingSlots + 1] = "Off Hand"
    end

    if #missingSlots == 0 then
        return
    end

    local icon = GetSpellIcon(rule.iconSpellID)
    if not icon then
        icon = GetInventoryItemTexture("player", mainSlot) or GetInventoryItemTexture("player", offSlot)
    end

    if icon then
        list[#list + 1] = {
            spellID = rule.iconSpellID,
            icon    = icon,
            tip     = rule.name .. " missing on " .. table.concat(missingSlots, " / ") .. "!",
        }
    end
end

------------------------------------------------------------------------
-- scanning -- figures out which buffs are missing and why
------------------------------------------------------------------------
local function ScanMissing()
    local newList = {}
    local db = GetDB()
    if not db.enabled then return newList end

    local _, playerClass = UnitClass("player")
    if not playerClass then return newList end

    local defs = CLASS_BUFFS[playerClass]

    local inGroup = IsInGroup() or IsInRaid()
    local inBattleground = C_PvP.IsBattleground()

    if defs then
        for _, def in ipairs(defs) do
            local displaySpellID = def.spellID or (def.spellIDs and def.spellIDs[1])
            local icon = GetSpellIcon(displaySpellID)
            if icon then
                local spellIDs = GetDefSpellIDs(def)
                if not def.targeted then
                    if not PlayerHasAnyAuraSpellIDs(spellIDs) then
                        newList[#newList + 1] = {
                            spellID = displaySpellID,
                            icon    = icon,
                            tip     = "You are missing " .. def.name .. "!",
                        }
                    elseif inGroup and not inBattleground then
                        local units = GetGroupUnits()
                        for _, unit in ipairs(units) do
                            if UnitExists(unit) and not UnitIsDeadOrGhost(unit) and UnitIsConnected(unit) then
                                if not HasBuff(unit, spellIDs) then
                                    newList[#newList + 1] = {
                                        spellID = displaySpellID,
                                        icon    = icon,
                                        tip     = def.name .. " missing on group member(s)!",
                                    }
                                    break
                                end
                            end
                        end
                    end
                else
                    if inGroup and not inBattleground then
                        if not def.knownSpellIDs or PlayerKnowsAnySpell(def.knownSpellIDs) then
                            local anyoneHasIt = false
                            local units = GetGroupUnits()
                            for _, unit in ipairs(units) do
                                if UnitExists(unit) and not UnitIsDeadOrGhost(unit) and UnitIsConnected(unit) then
                                    if playerClass == "SHAMAN" and def.spellID == 974 then
                                        if HasAnyBuffFromPlayer(unit, SHAMAN_EARTH_SHIELD_SPELLIDS) then
                                            anyoneHasIt = true
                                            break
                                        end
                                    elseif HasBuff(unit, spellIDs) then
                                        anyoneHasIt = true
                                        break
                                    end
                                end
                            end
                            if not anyoneHasIt then
                                newList[#newList + 1] = {
                                    spellID = displaySpellID,
                                    icon    = icon,
                                    tip     = def.name .. " is not on any group member -- pick a target!",
                                }
                            end
                        end
                    elseif playerClass == "SHAMAN" and def.spellID == 974 and (PlayerKnowsSpell(974) or PlayerKnowsSpell(383648)) then
                        if not PlayerHasAnyBuff(SHAMAN_EARTH_SHIELD_SPELLIDS) then
                            newList[#newList + 1] = {
                                spellID = 974,
                                icon    = icon,
                                tip     = "You are missing " .. def.name .. "!",
                            }
                        end
                    end
                end
            end
        end
    end

    AddSelfBuffReminders(playerClass, newList)
    AddWeaponEnchantReminder(playerClass, newList)
    return newList
end

------------------------------------------------------------------------
-- display -- icon frames with glow border
------------------------------------------------------------------------
local function EnsureAnchor()
    if anchorFrame then return end

    anchorFrame = CreateFrame("Frame", "ArenaCoreBuffReminderAnchor", UIParent)
    anchorFrame:SetSize(1, 1)
    anchorFrame:SetFrameStrata("MEDIUM")
    anchorFrame:SetFrameLevel(50)
    anchorFrame:SetClampedToScreen(true)

    local db = GetDB()
    if db.posX and db.posY then
        anchorFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", db.posX, db.posY)
    else
        anchorFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 150)
    end

    -- draggable -- left-click + drag, saves position in real time
    anchorFrame:SetMovable(true)
    anchorFrame:EnableMouse(false)
end

local function CreateIconFrame(index)
    local f = CreateFrame("Frame", "ArenaCoreBuffReminderIcon" .. index, anchorFrame, "BackdropTemplate")
    f:SetFrameStrata("MEDIUM")
    f:SetFrameLevel(51)

    -- icon texture
    f.icon = f:CreateTexture(nil, "ARTWORK")
    f.icon:SetAllPoints()

    -- glow border -- IconAlert scales cleanly with the frame and looks way nicer
    f.glowBorder = f:CreateTexture(nil, "OVERLAY")
    f.glowBorder:SetTexture("Interface\\SpellActivationOverlay\\IconAlert")
    f.glowBorder:SetTexCoord(0.00781250, 0.50781250, 0.27734375, 0.52734375)
    f.glowBorder:SetPoint("TOPLEFT", f.icon, "TOPLEFT", -14, 14)
    f.glowBorder:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 14, -14)
    f.glowBorder:SetBlendMode("ADD")
    f.glowBorder:SetVertexColor(0.8, 0.4, 0.9, 0.85)

    -- pulse animation for that "hey dummy you forgot" vibe
    f.glowAnim = f.glowBorder:CreateAnimationGroup()
    f.glowAnim:SetLooping("BOUNCE")
    local fadeOut = f.glowAnim:CreateAnimation("Alpha")
    fadeOut:SetFromAlpha(0.85)
    fadeOut:SetToAlpha(0.35)
    fadeOut:SetDuration(0.8)
    fadeOut:SetSmoothing("IN_OUT")
    f.glowAnim:Play()

    -- tooltip on hover
    f:EnableMouse(true)
    f:SetScript("OnEnter", function(self)
        if self.tipText then
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:AddLine(self.tipText, 1, 1, 1, true)
            GameTooltip:Show()
        end
    end)
    f:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    -- drag support -- each icon drags the whole anchor
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function()
        isDragging = true
        anchorFrame:StartMoving()
    end)
    f:SetScript("OnDragStop", function()
        anchorFrame:StopMovingOrSizing()
        isDragging = false
        local db = GetDB()
        local cx, cy = anchorFrame:GetCenter()
        if cx and cy then
            db.posX = cx
            db.posY = cy

            -- Save to global for account-wide sync
            AC.DB.global = AC.DB.global or {}
            AC.DB.global.positions = AC.DB.global.positions or {}
            AC.DB.global.positions.buffReminder = AC.DB.global.positions.buffReminder or {}
            AC.DB.global.positions.buffReminder.posX = cx
            AC.DB.global.positions.buffReminder.posY = cy
        end
        -- catch any buff changes that happened while we were dragging
        BR:Refresh()
    end)

    f:Hide()
    return f
end

local function LayoutIcons()
    local db = GetDB()
    local size = db.iconSize or 40
    local spacing = db.spacing or 6
    local zoom = db.iconZoom or 0.08

    EnsureAnchor()

    -- hide all existing icons first
    for _, f in ipairs(iconFrames) do
        f:Hide()
    end

    if not db.enabled or #missingList == 0 then
        if anchorFrame then anchorFrame:Hide() end
        return
    end

    -- show anchor and resize to fit the row
    anchorFrame:Show()
    local totalWidth = (#missingList * size) + ((#missingList - 1) * spacing)
    anchorFrame:SetSize(totalWidth, size)

    for i, info in ipairs(missingList) do
        local f = iconFrames[i]
        if not f then
            f = CreateIconFrame(i)
            iconFrames[i] = f
        end

        f:SetSize(size, size)
        f:ClearAllPoints()
        if i == 1 then
            f:SetPoint("LEFT", anchorFrame, "LEFT", 0, 0)
        else
            f:SetPoint("LEFT", iconFrames[i - 1], "RIGHT", spacing, 0)
        end

        f.icon:SetTexture(info.icon)
        f.icon:SetTexCoord(zoom, 1 - zoom, zoom, 1 - zoom)
        f.tipText = info.tip

        -- restart glow animation
        if f.glowAnim and not f.glowAnim:IsPlaying() then
            f.glowAnim:Play()
        end

        f:Show()
    end

    -- hide any excess icon frames
    for j = #missingList + 1, #iconFrames do
        if iconFrames[j] then
            iconFrames[j]:Hide()
        end
    end
end

------------------------------------------------------------------------
-- public API
------------------------------------------------------------------------
function BR:Refresh()
    local db = GetDB()
    if not db.enabled then return end

    local newList = ScanMissing()
    local changed = (#newList ~= #missingList)
    if not changed then
        for i = 1, #newList do
            if newList[i].spellID ~= missingList[i].spellID then
                changed = true
                break
            end
        end
    end

    if changed then
        missingList = newList
        LayoutIcons()
    end
end

function BR:Enable()
    local db = GetDB()
    db.enabled = true
    self:Refresh()
end

function BR:Disable()
    local db = GetDB()
    db.enabled = false
    for _, f in ipairs(iconFrames) do
        f:Hide()
    end
end

function BR:UpdateIconSize(val)
    local db = GetDB()
    db.iconSize = val
    self:Refresh()
end

function BR:UpdateSpacing(val)
    local db = GetDB()
    db.spacing = val
    self:Refresh()
end

function BR:UpdateIconZoom(val)
    local db = GetDB()
    db.iconZoom = val
    self:Refresh()
end

------------------------------------------------------------------------
-- initialization -- hooks events, starts the scan loop
------------------------------------------------------------------------
function BR:Initialize()
    if self._initialized then return end
    self._initialized = true

    local db = GetDB()
    if not db.enabled then return end

    EnsureAnchor()

    eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("UNIT_AURA")
    eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
    eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")

    eventFrame:SetScript("OnEvent", function(_, event, unit)
        local db = GetDB()
        if not db.enabled then return end

        if isDragging then return end
        -- only care about player / group aura changes
        if event == "UNIT_AURA" then
            if unit ~= "player" and not unit:match("^party") and not unit:match("^raid") then
                return
            end
            -- in combat, aura events fire a LOT -- coalesce into one refresh instead of stacking timers
            if InCombatLockdown() then
                if not BR._refreshPending then
                    BR._refreshPending = true
                    C_Timer.After(0.2, function()
                        BR._refreshPending = false
                        BR:Refresh()
                    end)
                end
                return
            end
        elseif event == "UNIT_INVENTORY_CHANGED" then
            if unit ~= "player" then return end
        end
        BR:Refresh()
    end)

    self:Refresh()
end

-- called when the user toggles on/off from settings without reload
function BR:Toggle(enabled)
    if enabled then
        self._initialized = false
        self:Initialize()
        self:Enable()
    else
        local db = GetDB()
        db.enabled = false
        for _, f in ipairs(iconFrames) do
            f:Hide()
        end
        if eventFrame then
            eventFrame:UnregisterEvent("UNIT_AURA")
            eventFrame:UnregisterEvent("GROUP_ROSTER_UPDATE")
            eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
            eventFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
            eventFrame:UnregisterEvent("UNIT_INVENTORY_CHANGED")
            eventFrame:UnregisterEvent("ZONE_CHANGED_NEW_AREA")
        end
    end
end
