-- Raven is an addon to monitor auras and cooldowns, providing timer bars and icons plus helpful notifications.
-- Author: Tomber
-- Copyright 2010-2016, All Rights Reserved

-- Main.lua contains initialization and update routines supporting Raven's core capability of tracking active auras and cooldowns.
-- It includes special cases for weapon buffs, stances, and trinkets.

-- Exported functions:
-- Raven:CheckAura(unit, name, isBuff) checks if an aura is active on a unit, returning detailed info if found
-- Raven:IterateAuras(unit, func, isBuff, p1, p2, p3) calls func for each active aura, parameters include a table with detailed aura info 
-- Raven:CheckCooldown(name) checks if cooldown with the specified name is active, returning detailed info if found
-- Raven:IterateCooldowns(func, p1, p2, p3) calls func for each active cooldown, parameters include a table with detailed cooldown info
-- Raven:UnitHasBuff(unit, type) returns true and table with detailed info if unit has an active buff of the specified type (e.g., "Mainhand")
-- Raven:UnitHasDebuff(unit, type) returns true and table with detailed info if unit has an active debuff of the specified type (e.g., "Poison")

Raven = LibStub("AceAddon-3.0"):NewAddon("Raven", "AceConsole-3.0", "AceEvent-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Raven")
local media = LibStub("LibSharedMedia-3.0")
local MOD = Raven
local MOD_Options = "Raven_Options"
local _
local addonInitialized = false -- set when the addon is initialized
local addonEnabled = false -- set when the addon is enabled
local optionsLoaded = false -- set when the load-on-demand options panel module has been loaded
local optionsFailed = false -- set if loading the option panel module t
MOD.updateOptions = false -- set this to cause the options panel to update (checked every half second)
MOD.LocalSpellNames = {} -- must be defined in first module loaded
local LSPELL = MOD.LocalSpellNames
MOD.frame = nil
MOD.db = nil
MOD.ldb = nil
MOD.ldbi = nil -- set when using DBIcon library
MOD.LibLDB = nil
MOD.myClass = nil; MOD.localClass = nil
MOD.myRace = nil; MOD.localRace = nil
MOD.lockoutSpells = {} -- spells for testing lock out of each school of magic for current player
MOD.classConditions = {} -- stores info about pre-defined conditions for each class
MOD.talents = {} -- table containing names and talent table location for each talent
MOD.talentList = {} -- table with list of talent names
MOD.runeSlots = {} -- cache information about each rune slot for DKs
MOD.runeCount = 0 -- current number of available runes
MOD.updateActions = true -- action bar changed
MOD.updateDispels = true -- need to update dispel types
MOD.knownBrokers = {} -- table of registered data brokers
MOD.brokerList = {} -- table of brokers suitable for a selection list

local doUpdate = true -- set by any event that can change bars (used to throttle major updates)
local forceUpdate = false -- set to cause immediate update (reserved for critical changes like to player's target or focus)
local updateCooldowns = false -- set when actionbar or inventory slot cooldown starts or stops
local units = {} -- list of units to track
local mainUnits = { "player", "pet", "target", "focus", "targettarget", "focustarget", "pettarget", "mouseover" } -- ordered list of main units
local partyUnits = { "party1", "party2", "party3", "party4" } -- optional party units
local bossUnits = { "boss1", "boss2", "boss3", "boss4", "boss5" } -- optional boss units
local arenaUnits = { "arena1", "arena2", "arena3", "arena4", "arena5" } -- optional arena units
local nameplateUnits = {} -- cache of 40 nameplate unit ids
local eventUnits = { "targettarget", "focustarget", "pettarget", "mouseover" } -- can't count on events for these units
local unitUpdate = {} -- boolean for each unit that indicates need to update auras
local unitStatus = {} -- status of each unit set on every update (0 = no unit, 1 = unit exists, "unit" = unit is other unit)
local unitBuffs = {} -- indexed by GUID for tracking buffs cast by player
local unitDebuffs = {} -- indexed by GUID for tracking debuffs cast by player
local activeBuffs = {} -- active buffs for each unit
local activeDebuffs = {} -- active debuffs for each unit
local cacheBuffs = {} -- cache of active buff names
local cacheDebuffs = {} -- cache of active debuff names
local cacheUnits = {} -- cache of unit IDs, indexed by GUID
local refreshUnits = {} -- unit id cache used to optimize refresh
local tablePool = {} -- pool of available tables
local activeCooldowns = {} -- spells/items that are currently on cooldown
local internalCooldowns = {} -- tracking entries for internal cooldowns
local spellEffects = {} -- tracking entries for spell effects
local lastTime = 0 -- time when last update happened
local lastTrackers = 0 -- time when last looked at trackers on major units
local lastWeapons = 0 -- time when last looked at weapon buffs
local elapsedTime = 0 -- time in seconds since last update
local updateCounter = 0 -- update counter included for testing
local refreshTime = 0 -- time since last animation refresh
local refreshCounter = 0 -- refresh counter included for testing
local throttleTime = 0 -- secondary throttle that resets once per second
local now = 0 -- refresh time value set at combat log and update events
local bufftooltip = nil -- used to store tooltip for scanning weapon buffs
local mhLastBuff = nil -- saves name of most recent main hand weapon buff
local ohLastBuff = nil -- saves name of most recent off hand weapon buff
local iconGCD = nil -- icon for global cooldown
local iconPotion = nil -- icon for shared potions cooldown
local iconElixir = nil -- icon for shared elixirs cooldown
local iconRune = nil -- icon for death knight runes
local lastTotems = {} -- cache last totems in each slot to see if changed
local lockedOut = false -- true if currently locked out of at least one spell school
local lockouts = {} -- schools of magic that we are currently locked out of
local lockstarts = {} -- start times for current school lockouts
local talentsInitialized = false -- set once talents have been initialized
local matchTable = {} -- passed from MOD:CheckAura with list of active auras
local startGCD, durationGCD = nil -- detect global cooldowns
local raidTargets = {} -- raid target to GUID
local petGUID = nil -- cache pet GUID so can properly remove trackers for them when dismissed
local enteredWorld = nil -- set by PLAYER_ENTERING_WORLD event
local trackerTag = 0 -- used for mark/sweep in AddTrackers
local professions = {} -- temporary table for profession indices
local knownSpells = {} -- table used to detect spells that are temporarily not in spellbook (e.g., Stormstrike during Ascendance)
local foundSpells = {} -- temporary table for checking known spells
local summonedCreatures = {} -- table of guids to expire time pairs used for tracking warlock creatures so they despawn properly
local soulEffigy = nil -- set to guid of warlock's soul effigy to handle special case when it despawns early
local ignoreGUID = nil -- set when need to start ignoring trackers from a unit that we know is no longer valid (e.g., soul effigy)
local activeBrokers = {} -- table of brokers that trigger update events
local hidingRunes = false -- used to prevent trying to show runes if didn't hide them

-- This table is used to fix the "not cast by player" bug for Jade Spirit, River's Song, and Dancing Steel introduced in 5.1
-- and the legendary meta gem procs Tempus Repit, Fortitude, Capacitance, and Lucidity added in 5.2
local fixEnchants = { [104993] = true, [120032] = true, [118334] = true, [118335] = true, [116660] = true,
	[137590] = true, [137593] = true, [137331] = true, [137323] = true, [137247] = true, [137596] = true }

-- Initialization called when addon is loaded
function MOD:OnInitialize()
	if addonInitialized then return end -- only run this code once
	addonInitialized = true
	
	MOD.localClass, MOD.myClass = UnitClass("player") -- cache the player's class
	MOD.localRace, MOD.myRace = UnitRace("player") -- cache the player's race
	LoadAddOn("LibDataBroker-1.1")
	LoadAddOn("LibDBIcon-1.0")
	LoadAddOn("LibBossIDs-1.0", true)
	MOD.MSQ = LibStub("Masque", true)
	now = GetTime() -- start tracking time
end

-- Print debug messages with variable number of arguments in a useful format
function MOD.Debug(a, ...)
	if type(a) == "table" then
		for k, v in pairs(a) do print(tostring(k) .. " = " .. tostring(v)) end -- if first parameter is a table, print out its fields
	else
		local s = tostring(a) -- otherwise first argument is a string but just make sure
		local parm = {...}
		for i = 1, #parm do s = s .. " " .. tostring(parm[i]) end -- append remaining arguments converted to strings
		print(s)
	end
end

-- Functions called to trigger updates
local function TriggerPlayerUpdate() unitUpdate.player = true; updateCooldowns = true; doUpdate = true end
local function TriggerCooldownUpdate() updateCooldowns = true; doUpdate = true end
local function TriggerActionsUpdate() MOD.updateActions = true; doUpdate = true end
function MOD:ForceUpdate() doUpdate = true; forceUpdate = true end

-- Event called when the player changes talents or specialization
local function CheckTalentSpecialization() talentsInitialized = false; unitUpdate.player = true; table.wipe(knownSpells); doUpdate = true end

-- Function called to detect global cooldowns
local function CheckGCD(event, unit, spell)
	if unit == "player" then
		local start, duration = GetSpellCooldown(spell)
		if start and duration and (duration > 0) and (duration <= 1.5) then startGCD = start; durationGCD = duration; TriggerCooldownUpdate() end
	end
end

-- Function called for successful spell cast
local function CheckSpellCasts(event, unit, spell)
	CheckGCD(event, unit, spell)
	if MOD.db.global.DetectSpellEffects then MOD:DetectSpellEffect(spell, unit) end -- check if spell triggers a spell effect
end

-- Create and delete routines for managing tables, using a recycling pool to minimize garbage collection
local function AllocateTable() local b = next(tablePool); if b then tablePool[b] = nil else b = {} end return b end
local function ReleaseTable(b) table.wipe(b); tablePool[b] = true; return nil end

-- Compare unit and global ids, updating cache with latest info
local function CheckUnitIDs(uid, guid)
	local id = UnitGUID(uid)
	if id == guid then return uid end
	if id then cacheUnits[id] = uid end
	return nil
end

-- Add or update a tracker entry, including an option tag useful for mark/sweep type garbage collection
local function AddTracker(dstGUID, dstName, isBuff, name, rank, icon, count, btype, duration, expire, caster, isStealable, spellID, boss, apply, tag)
	doUpdate = true
	local tracker = isBuff and unitBuffs[dstGUID] or unitDebuffs[dstGUID] -- get or create the aura tracking table
	if not tracker then tracker = AllocateTable() if isBuff then unitBuffs[dstGUID] = tracker else unitDebuffs[dstGUID] = tracker end end
	local id = name .. tostring(spellID or "") -- append spellID if known to the tracker so can track multiple with same name (e.g., sacred shield)
	local t = tracker[id] -- get or create a tracker entry for the spell
	if not t then t = AllocateTable(); tracker[id] = t end -- create the tracker if necessary
	local vehicle = UnitHasVehicleUI("player")
	t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8], t[9], t[10], t[11], t[12], t[13], t[14], t[15], t[16], t[17], t[18], t[19], t[20], t[21], t[22] =
		true, 0, count, btype, duration, caster, isStealable, icon, rank, expire, "spell id", spellID, name, spellID,
		boss, UnitName("player"), apply, nil, vehicle, dstGUID, dstName, tag
end

-- Remove tracker entries for a unit, if tag is specified then only remove if tracker tag not equal
function MOD:RemoveTrackers(dstGUID, tag)
	doUpdate = true
	local tracker = unitBuffs[dstGUID] -- table of buffs currently applied to this GUID
	if tracker then
		for id, t in pairs(tracker) do if not tag or t[22] ~= tag then tracker[id] = ReleaseTable(t) end end
		if not next(tracker) then unitBuffs[dstGUID] = ReleaseTable(tracker) end -- release the debuffs associated with the GUID
	end
	local tracker = unitDebuffs[dstGUID] -- table of auras currently applied to this GUID
	if tracker then
		for id, t in pairs(tracker) do if not tag or t[22] ~= tag then tracker[id] = ReleaseTable(t) end end
		if not next(tracker) then unitDebuffs[dstGUID] = ReleaseTable(tracker) end -- release the table associated with the GUID
	end
end

-- Remove trackers for all units that match the name of the designated unit
function MOD:RemoveMatchingTrackers(dstGUID)
	local name = nil
	local tracker = unitBuffs[dstGUID] -- find name by looking at active trackers
	if tracker then for id, t in pairs(tracker) do name = t[21]; if name then break end end end
	if not name then
		tracker = unitDebuffs[dstGUID]
		if tracker then for id, t in pairs(tracker) do name = t[21]; if name then break end end end
	end
	MOD:RemoveTrackers(dstGUID) -- start by removing the trackers for the unit passed in
	if name then
		local guids = {} -- build list of guids to remove
		for id, tracker in pairs(unitBuffs) do
			if tracker then for _, t in pairs(tracker) do if t[21] == name then guids[id] = true break end end end
		end
		for id, tracker in pairs(unitDebuffs) do
			if tracker then for _, t in pairs(tracker) do if t[21] == name then guids[id] = true break end end end
		end
		for id in pairs(guids) do MOD:RemoveTrackers(id) end
	end
end

-- Check tracker entries for a unit to see if one already exists for a spell
local function CheckTrackers(isBuff, dstGUID, name, spellID)
	local tracker = isBuff and unitBuffs[dstGUID] or unitDebuffs[dstGUID] -- get the aura tracking table
	if tracker then
		local id = name .. tostring(spellID or "") -- append spellID if known
		local t = tracker[id]
		if t then
			if t[13] == name then return t end
		end
	end
	return nil
end

-- Add trackers for a unit
function MOD:AddTrackers(unit)
	local dstGUID, dstName = UnitGUID(unit), UnitName(unit)
	if dstGUID and dstName and not refreshUnits[dstGUID] and (dstGUID ~= ignoreGUID) then
		refreshUnits[dstGUID] = true
		local name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, boss, apply
		trackerTag = trackerTag + 1 -- unique tag for this pass
		local i = 1
		repeat
			name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply = UnitAura(unit, i, "HELPFUL|PLAYER")
			if name and caster == "player" then
				AddTracker(dstGUID, dstName, true, name, rank, icon, count, btype, duration, expire, caster, isStealable, spellID, nil, apply, trackerTag)
				MOD.SetDuration(name, spellID, duration)
				MOD.SetSpellType(spellID, btype)
			end
			i = i + 1
		until not name
		i = 1
		repeat
			name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss = UnitAura(unit, i, "HARMFUL|PLAYER")
			if name and caster == "player" then
				if spellID ~= 146739 or duration ~= 0 or InCombatLockdown() then -- don't add Corruption if out-of-combat
					AddTracker(dstGUID, dstName, false, name, rank, icon, count, btype, duration, expire, caster, isStealable, spellID, boss, apply, trackerTag)
					MOD.SetDuration(name, spellID, duration)
					MOD.SetSpellType(spellID, btype)
				end
			end
			i = i + 1
		until not name
		MOD:RemoveTrackers(dstGUID, trackerTag) -- takes advantage of side-effect of saving current trackerTag with each tracker
	end
end

-- Check if currently tracking a unit
local function IsBeingTracked(dstGUID) return unitBuffs[dstGUID] and unitDebuffs[dstGUID] end

-- Validate cached ids, garbage collect any that are out-of-date
local function ValidateUnitIDs()
	for guid, uid in pairs(cacheUnits) do if UnitGUID(uid) ~= guid then cacheUnits[guid] = nil end end
end

-- Get a unit id suitable for calling UnitAura from a GUID
local function GetUnitIDFromGUID(guid)
	local uid = cacheUnits[guid] -- look up the guid in the cache and if it is there make sure it is still valid and then return it
	if uid then if guid == UnitGUID(uid) then return uid else uid = nil end end
	for _, unit in pairs(units) do uid = CheckUnitIDs(unit, guid); if uid then break end end -- first check primary units
	local inRaid = IsInRaid()
	if not uid and not inRaid then -- check party, party pet, and party target units
		for i = 1, GetNumGroupMembers() do
			uid = CheckUnitIDs("party"..i, guid); if uid then break end
			uid = CheckUnitIDs("partypet"..i, guid); if uid then break end
			uid = CheckUnitIDs("party"..i.."target", guid); if uid then break end
		end
	end
	if not uid and inRaid then -- check raid, raid pet, and raid target units
		for i = 1, GetNumGroupMembers() do
			uid = CheckUnitIDs("raid"..i, guid); if uid then break end
			uid = CheckUnitIDs("raidpet"..i, guid); if uid then break end
			uid = CheckUnitIDs("raid"..i.."target", guid); if uid then break end
		end
	end
	if not uid then -- check nameplates as last resort
		for i = 1, 40 do
			local np = nameplateUnits[i]
			local id = UnitGUID(np)
			if not id then break end
			if id == guid then uid = np; break end
		end
	end
	cacheUnits[guid] = uid
	return uid
end

-- Function called for combat log events to track hots and dots
local function CombatLogTracker(event, timeStamp, e, hc, srcGUID, srcName, sf1, sf2, dstGUID, dstName, df1, df2, spellID, spellName, spellSchool, auraType, amount)
	if bit.band(sf1, COMBATLOG_OBJECT_AFFILIATION_MASK) == COMBATLOG_OBJECT_AFFILIATION_MINE then -- make sure event controlled by the player
		-- MOD.Debug(e, srcGUID, srcName, sf1, sf2, dstGUID, dstName, df1, df2, spellID, spellName, spellSchool, auraType, tostring(amount)) -- display all events
		doUpdate = true
		now = GetTime()
		if e == "SPELL_CAST_SUCCESS" then -- check for special cases
			if spellID == 33763 then e = "SPELL_AURA_APPLIED"; auraType = "BUFF" end -- Lifebloom refreshes don't always generate aura applied events
			if spellID == 980 then -- Agony refresh does not always generate aura refresh event, even if debuff just expired
				local t = CheckTrackers(false, dstGUID, spellName, spellID)
				if t then
					t[10] = now + t[5] -- extend the time on current tracker (preserves the dose amount)
				else
					e = "SPELL_AURA_REFRESH" -- event not generated automatically by Agony
				end
			end
		end
		if e == "SPELL_AURA_APPLIED" or e == "SPELL_AURA_APPLIED_DOSE" or e == "SPELL_AURA_REMOVED_DOSE" or e == "SPELL_AURA_REFRESH" then
			local name, rank, icon, count, btype, duration, expire, caster, isStealable, boss, sid, apply, _
			local isBuff, dst = true, GetUnitIDFromGUID(dstGUID)
			if dst and UnitExists(dst) then
				name, rank, icon, count, btype, duration, expire, caster, isStealable, _, sid, apply = UnitAura(dst, spellName, nil, "HELPFUL|PLAYER")
				if not name and (srcGUID ~= dstGUID) then -- don't get debuffs cast by player on self (e.g., Sated)
					isBuff = false
					name, rank, icon, count, btype, duration, expire, caster, isStealable, _, sid, apply, boss = UnitAura(dst, spellName, nil, "HARMFUL|PLAYER")
				end
				if sid and spellID and spellID ~= sid then name = nil end -- not a match so must be a duplicate name
				if name then MOD.SetDuration(name, spellID, duration); MOD.SetSpellType(spellID, btype) end
			end
			if not spellID then spellID = MOD:GetSpellID(spellName) end
			if spellID and not icon then icon = MOD:GetIcon(spellName, spellID) end
			if not name then
				name = spellName; rank = ""; count = 1; btype = MOD.GetSpellType(spellID); duration = MOD.GetDuration(name, spellID); isBuff = (auraType == "BUFF")
				if duration > 0 then expire = now + duration else duration = 0; expire = 0 end
				if e == "SPELL_AURA_APPLIED_DOSE" or e == "SPELL_AURA_REMOVED_DOSE" then -- may be refresh of existing spell's stack count (e.g., Agony)
					count = amount
					local t = CheckTrackers(isBuff, dstGUID, name, spellID)
					if t then duration = t[5]; expire = t[10]; btype = t[4] end
				end
				caster = "player"; isStealable = nil; boss = nil; apply = nil
			end
			if name and caster == "player" and (isBuff or (srcGUID ~= dstGUID)) then
				AddTracker(dstGUID, dstName, isBuff, name, rank, icon, count, btype, duration, expire, caster, isStealable, spellID, boss, apply, nil)
			end
			if dstGUID == UnitGUID("target") and not IsBeingTracked(dstGUID) then ValidateUnitIDs() end -- refresh all auras when target changes
			if MOD.db.global.DetectInternalCooldowns then MOD:DetectInternalCooldown(spellName, false) end -- check internal cooldowns
		elseif e == "SPELL_ENERGIZE" or e == "SPELL_HEAL" then
			if MOD.db.global.DetectInternalCooldowns then MOD:DetectInternalCooldown(spellName, false) end -- check internal cooldowns
		elseif e == "SPELL_AURA_REMOVED" then
			local tracker = unitBuffs[dstGUID] -- table of buffs currently applied to this GUID
			if tracker then
				local id = spellName .. tostring(spellID or "")
				local t = tracker[id] -- get tracker entry for the spell, if one exists
				if t then tracker[id] = ReleaseTable(t) end -- release the tracker entry
				if not next(tracker) then unitBuffs[dstGUID] = ReleaseTable(tracker) end -- release table when no more entries for this GUID
			end
			tracker = unitDebuffs[dstGUID] -- table of debuffs currently applied to this GUID
			if tracker then
				local id = spellName .. tostring(spellID or "")
				local t = tracker[id] -- get tracker entry for the spell, if one exists
				if t then tracker[id] = ReleaseTable(t) end -- release the tracker entry
				if not next(tracker) then unitDebuffs[dstGUID] = ReleaseTable(tracker) end -- release table when no more entries for this GUID
			end
		elseif e == "SPELL_SUMMON" then
			if MOD.myClass == "MAGE" and spellID == 99063 then -- special case for mage T12 2-piece
				local name = GetSpellInfo(99061) -- T12 bonus spell name
				if name and name ~= "" then
					if MOD.db.global.DetectInternalCooldowns then MOD:DetectInternalCooldown(name, false) end
					if MOD.db.global.DetectSpellEffects then MOD:DetectSpellEffect(name, "player") end
				end
			elseif MOD.myClass == "WARLOCK" and dstGUID and spellID then
				local duration = MOD.warlockCreatures[spellID]
				if duration then
					summonedCreatures[dstGUID] = duration + now -- summoned creature table contains expire time
					if spellID == 205178 then -- special case for soul effigy
						if soulEffigy then MOD:RemoveTrackers(soulEffigy) end -- already one up so must remove trackers for it, if any
						soulEffigy = dstGUID -- save guid so can remove it later despite missing event
					end
				end
			end
		end
	elseif dstGUID == UnitGUID("player") then
		if e == "SPELL_AURA_APPLIED" or e == "SPELL_AURA_APPLIED_DOSE" or e == "SPELL_AURA_REFRESH" or e == "SPELL_ENERGIZE" or e == "SPELL_HEAL" then
			if MOD.db.global.DetectInternalCooldowns then MOD:DetectInternalCooldown(spellName, true) end -- check aura triggers or cancels an internal cooldown
		end
	end
	if e == "UNIT_DIED" or e == "UNIT_DESTROYED" or e == "UNIT_DISSIPATES" or e == "PARTY_KILL" then
		MOD:RemoveTrackers(dstGUID) -- remove the trackers currently associated with this GUID
		cacheUnits[dstGUID] = nil -- release the unit cache entry for this GUID too
	end
end

-- Check if there is a raid target on a unit
local function CheckRaidTarget(unit)
	local id = UnitGUID(unit)
	if id then
		local index = GetRaidTargetIndex(unit)
		for k, v in pairs(raidTargets) do if (v == id) and (k ~= index) then raidTargets[k] = nil end end 
		if index then raidTargets[index] = id end
	end
end

-- Check raid targets on all addressable units
local function CheckRaidTargets()
	doUpdate = true
	for _, unit in pairs(units) do CheckRaidTarget(unit) end -- first check primary units
	if IsInRaid() then
		for i = 1, GetNumGroupMembers() do CheckRaidTarget("raid"..i); CheckRaidTarget("raidpet"..i); CheckRaidTarget("raid"..i.."target") end
	else
		for i = 1, GetNumGroupMembers() do CheckRaidTarget("party"..i); CheckRaidTarget("partypet"..i); CheckRaidTarget("party"..i.."target") end
	end
end

-- Check raid target on mouseover unit
local function CheckMouseoverRaidTarget() CheckRaidTarget("mouseover"); CheckRaidTarget("mouseovertarget"); doUpdate = true end

-- Return the raid target index for a GUID
function MOD:GetRaidTarget(id) for k, v in pairs(raidTargets) do if v == id then return k end end return nil end

-- Event called when addon is enabled
function MOD:OnEnable()
	if addonEnabled then return end -- only run this code once
	addonEnabled = true
	
	MOD:InitializeProfile() -- initialize the profile database
	MOD:InitializeLDB() -- initialize the data broker
	MOD:RegisterChatCommand("raven", function() MOD:OptionsPanel() end)
	MOD.Nest_Initialize() -- initialize the graphics module
	MOD:InitializeConditions() -- initialize condition evaluation module

	-- Create a frame so that updates can be registered
	MOD.frame = CreateFrame('Frame')
	-- Set frame level high so visible above other addons
	MOD.frame:SetFrameLevel(MOD.frame:GetFrameLevel() + 8)
	-- Register events called prior to starting play
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self:RegisterEvent("UNIT_AURA")
	self:RegisterEvent("UNIT_POWER")
	self:RegisterEvent("UNIT_PET")
	self:RegisterEvent("UNIT_TARGET")
	self:RegisterEvent("PLAYER_FOCUS_CHANGED")
	self:RegisterEvent("PLAYER_TARGET_CHANGED")
	self:RegisterEvent("PLAYER_TALENT_UPDATE", CheckTalentSpecialization)
	self:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED", CheckTalentSpecialization)
	self:RegisterEvent("SPELLS_CHANGED")
	self:RegisterEvent("VEHICLE_UPDATE")
	self:RegisterEvent("RAID_TARGET_UPDATE", CheckRaidTargets)
	self:RegisterEvent("UPDATE_MOUSEOVER_UNIT", CheckMouseoverRaidTarget)
	self:RegisterEvent("UPDATE_SHAPESHIFT_FORM", TriggerPlayerUpdate)
	self:RegisterEvent("MINIMAP_UPDATE_TRACKING", TriggerPlayerUpdate)	
	self:RegisterEvent("SPELL_UPDATE_COOLDOWN", TriggerCooldownUpdate)
	self:RegisterEvent("UNIT_INVENTORY_CHANGED", TriggerCooldownUpdate)
	self:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", TriggerCooldownUpdate)
	self:RegisterEvent("BAG_UPDATE_COOLDOWN", TriggerCooldownUpdate)
	self:RegisterEvent("PET_BAR_UPDATE_COOLDOWN", TriggerCooldownUpdate)
	self:RegisterEvent("ACTIONBAR_SLOT_CHANGED", TriggerActionsUpdate)
	self:RegisterEvent("ACTIONBAR_PAGE_CHANGED", TriggerActionsUpdate)
	self:RegisterEvent("RUNE_POWER_UPDATE", TriggerCooldownUpdate)
	self:RegisterEvent("RUNE_TYPE_UPDATE", TriggerCooldownUpdate)
	self:RegisterEvent("PLAYER_TOTEM_UPDATE", TriggerPlayerUpdate)
	self:RegisterEvent("UNIT_SPELLCAST_START", CheckGCD)
	self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", CheckSpellCasts)
	self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", CombatLogTracker)
	MOD:InitializeBars() -- initialize routine that manages the bar library
	MOD:InitializeMedia(media) -- add sounds to LibSharedMedia
	MOD.LibBossIDs = LibStub("LibBossIDs-1.0", true)
	MOD.db.global.Version = "7" -- version number for database validation
end

-- Event called when addon is disabled but this is probably never called
function MOD:OnDisable() end

-- Cache icons for special purposes such as shared cooldowns
local function InitializeIcons()
	iconGCD = GetSpellTexture(28730) -- cached for global cooldown (using same icon as Arcane Torrent, must be valid)
	iconPotion = GetItemIcon(31677) -- icon for shared potions cooldown
	iconElixir = GetItemIcon(28104) -- icon for shared elixirs cooldown
	
	MOD:SetIcon(L["Rune"], GetSpellTexture(48266)) -- cached for death knight runes (this is for Frost Presence)
end

-- Updates will be driven by the new timer function, compute elapsed time since last update
local function UpdateHandler()
	now = GetTime()
	local elapsed = now - lastTime -- seconds since last call to update
	if elapsed > 1.0 then elapsed = 1.0 end -- should only happen during initialization
	MOD:Update(elapsed)
	lastTime = now
	C_Timer.After(0.001, UpdateHandler) -- register to be called for next frame
end

-- Initialize list of units that are tracked
function MOD:InitializeUnits()
	table.wipe(units)
	for i, k in pairs(mainUnits) do units[i] = k end
	if MOD.db.global.IncludePartyUnits then for _, k in pairs(partyUnits) do table.insert(units, k) end end
	if MOD.db.global.IncludeBossUnits then for _, k in pairs(bossUnits) do table.insert(units, k) end end
	if MOD.db.global.IncludeArenaUnits then for _, k in pairs(arenaUnits) do table.insert(units, k) end end
	for i = 1, 40 do nameplateUnits[i] = "nameplate"..i end
end

-- Initialize when play starts, deferred to allow system initialization to complete
function MOD:PLAYER_ENTERING_WORLD()
	if not enteredWorld then
		MOD:InitializeUnits() -- initialize list of units to track (this requires /reload to update)
		for _, k in pairs(units) do unitUpdate[k] = true; activeBuffs[k] = {}; activeDebuffs[k] = {}; cacheBuffs[k] = {}; cacheDebuffs[k] = {}  end -- track auras
		updateCooldowns = true -- start tracking cooldowns
		MOD:InitializeBuffTooltip() -- initialize tooltip used to monitor weapon buffs
		InitializeIcons() -- cache special purpose icons
		MOD:InitializeOverlays() -- initialize overlays used to cancel player buffs
		MOD:InitializeInCombatBar() -- initialize special bar for cancelling buffs in combat
		MOD:UpdateAllBarGroups() -- final update before starting event-based updates
		enteredWorld = true; doUpdate = true
		UpdateHandler() -- register for calls on every frame
	end
	if not InCombatLockdown() then collectgarbage("collect") end -- recover deleted preset data but not if in combat
end

-- Event called when an aura changes on a unit, returns the unit name
function MOD:UNIT_AURA(e, unit)
	if unit and (unitUpdate[unit] ~= nil) then
		if unit == "vehicle" then unitUpdate.player = true end -- any time vehicle updates, also update player
		unitUpdate[unit] = true; doUpdate = true
	end
end

-- Event called when a unit's power changes
function MOD:UNIT_POWER(e, unit) if unit == "player" then unitUpdate[unit] = true; doUpdate = true end end

-- Event for when vehicle info changes
function MOD:VEHICLE_UPDATE() TriggerPlayerUpdate() end

-- Event called with a unit's target changes
function MOD:UNIT_TARGET(e, unit)
	if unit == "player" then
		unitUpdate.target = true; doUpdate = true
	elseif unit == "target" then
		unitUpdate.targettarget = true; doUpdate = true
	elseif unit == "focus" then
		unitUpdate.focustarget = true; doUpdate = true
	elseif unit == "pet" then
		unitUpdate.pettarget = true; doUpdate = true
	end
end

-- Event called when a pet changes
function MOD:UNIT_PET() unitUpdate.pet = true; unitUpdate.pettarget = true; doUpdate = true end

-- Event called when the focus is changed
function MOD:PLAYER_FOCUS_CHANGED() unitUpdate.focus = true; unitUpdate.focustarget = true; doUpdate = true; forceUpdate = true end

-- Event called when the player's target is changed
function MOD:PLAYER_TARGET_CHANGED() unitUpdate.target = true; unitUpdate.targettarget = true; doUpdate = true; forceUpdate = true end

-- Event called when spells in spell book change
function MOD:SPELLS_CHANGED() MOD:SetIconDefaults(); MOD:SetCooldownDefaults(); updateCooldowns = true; doUpdate = true end

-- Create cache of talent info
local function InitializeTalents()	
	local tabs = GetNumSpecializations(false, false)
	if tabs == 0 then return end

	local currentSpec = GetSpecialization()
	local specGroup = GetActiveSpecGroup()
	talentsInitialized = true; doUpdate = true
	table.wipe(MOD.talents); table.wipe(MOD.talentList)
	
	local select = 1
	for tier = 1, MAX_TALENT_TIERS do
		for column = 1, NUM_TALENT_COLUMNS do
			local talentID, name, texture, selected = GetTalentInfo(tier, column, specGroup) -- player's active talents
			if name then
				MOD.talents[name] = { tab = currentSpec, column = column, tier = tier, icon = texture, active = selected }
				MOD.talentList[select] = name
				select = select + 1
			end
		end
	end

	table.sort(MOD.talentList)
	for i, t in pairs(MOD.talentList) do
		MOD.talents[t].select = i
	end
	MOD.updateDispels = true
end

-- Check if the options panel is loaded, if not then get it loaded and ask it to toggle open/close status
function MOD:OptionsPanel()
    if not optionsLoaded then
        optionsLoaded = true
        local loaded, reason = LoadAddOn(MOD_Options)
        if not loaded then
            print(L["Failed to load "] .. tostring(MOD_Options) .. ": " .. tostring(reason))
			optionsFailed = true
        end
	end
	if not optionsFailed then MOD:ToggleOptions() end
end

-- If the options panel is loaded then update it so it reflects any changes made thru anchors, etc.
function MOD:UpdateOptionsPanel()
	if optionsLoaded and not optionsFailed and not IsMouseButtonDown("LeftButton") then MOD:UpdateOptions(); MOD.updateOptions = false end
	doUpdate = true
end

-- Add a registered data broker
local function RegisterDataBroker(event, name, broker)
	-- MOD.Debug("ldb_register", event, name, key, value)
	MOD.knownBrokers[name] = broker

	table.wipe(MOD.brokerList) -- recreate the broker list table
	local i = 1
	for k, v in pairs(MOD.knownBrokers) do MOD.brokerList[i] = k; i = i + 1 end
	table.sort(MOD.brokerList)
end

-- Update event handler for an activated data broker
local function UpdateDataBroker(event, name, key, value, dataobj)
	-- MOD.Debug("ldb_update", event, name, key, value)
	doUpdate = true
end

-- Activate a registered data broker when creating a custom bar reference (no need to deactivate since only used for updates)
function MOD:ActivateDataBroker(name)
	if not activeBrokers[name] then -- first time reference
		-- MOD.Debug("ldb_activate", name)
		activeBrokers[name] = true
		MOD.LibLDB.RegisterCallback("MyAnonCallback", "LibDataBroker_AttributeChanged_" .. name, UpdateDataBroker)
		doUpdate = true
	end
end

-- Tie into LibDataBroker
function MOD:InitializeLDB()
	MOD.LibLDB = LibStub("LibDataBroker-1.1", true)
	if not MOD.LibLDB then return end
	MOD.ldb = MOD.LibLDB:NewDataObject("Raven", {
		type = "launcher",
		text = "Raven",
		icon = "Interface\\Icons\\Spell_Nature_RavenForm",
		-- icon = [[Interface\AddOns\Raven\Raven]],
		OnClick = function(_, msg)
			if msg == "RightButton" then
				if IsShiftKeyDown() then
					MOD.db.profile.hideBlizz = not MOD.db.profile.hideBlizz
				else
					MOD:ToggleBarGroupLocks()
				end
			elseif msg == "LeftButton" then
				if IsShiftKeyDown() then
					MOD.db.profile.enabled = not MOD.db.profile.enabled
				else
					MOD:OptionsPanel()
				end
			end
			doUpdate = true
		end,
		OnTooltipShow = function(tooltip)
			if not tooltip or not tooltip.AddLine then return end
			tooltip:AddLine(L["Raven"])
			tooltip:AddLine(L["Raven left click"])
			tooltip:AddLine(L["Raven right click"])
			tooltip:AddLine(L["Raven shift left click"])
			tooltip:AddLine(L["Raven shift right click"])
		end,
	})
	MOD.ldbi = LibStub("LibDBIcon-1.0", true)
	if MOD.ldbi then MOD.ldbi:Register("Raven", MOD.ldb, MOD.db.global.Minimap) end
	
	for name, broker in MOD.LibLDB:DataObjectIterator() do RegisterDataBroker("register", name, broker) end
	MOD.LibLDB.RegisterCallback("MyAnonCallback", "LibDataBroker_DataObjectCreated", RegisterDataBroker)
end

-- Show or hide the blizzard buff frames, called during update so synched with other changes
local function CheckBlizzFrames()
	local visible = BuffFrame:IsShown()
	if MOD.db.profile.hideBlizz then
		if visible then BuffFrame:Hide(); TemporaryEnchantFrame:Hide(); BuffFrame:UnregisterAllEvents() end
	else
		if not visible then BuffFrame:Show(); TemporaryEnchantFrame:Show(); BuffFrame:RegisterEvent("UNIT_AURA") end
	end
	if MOD.myClass == "DEATHKNIGHT" then -- only change rune frame if on deathknight currently
		visible = RuneFrame:IsShown()
		if MOD.db.profile.hideRunes then
			if visible then RuneFrame:UnregisterAllEvents(); RuneFrame:Hide(); hidingRunes = true end
		else
			if hidingRunes and not visible then 
				RuneFrame:Show()
				RuneFrame:GetScript("OnLoad")(RuneFrame); RuneFrame:GetScript("OnEvent")(RuneFrame, "PLAYER_ENTERING_WORLD")
				hidingRunes = false -- don't want to keep trying if other addon keeps hiding them
			end
		end
	end
end

-- See if totems have changed since last update because can't count on events for totems
local function CheckTotemUpdates()
	local cl = MOD.myClass
	if cl == "SHAMAN" or cl == "DRUID" or cl == "MAGE" then
		local changed = false
		for i = 1, MAX_TOTEMS do
			local haveTotem, name, startTime, duration = GetTotemInfo(i)
			if haveTotem and name and name ~= "" and now <= (startTime + duration) then
				if not lastTotems[i] or name ~= lastTotems[i] then changed = true end
				lastTotems[i] = name
			else
				if lastTotems[i] then changed = true end
				lastTotems[i] = nil
			end
		end
		if changed then updateCooldowns = true; unitUpdate.player = true; doUpdate = true; forceUpdate = true end
	end
end

-- Check for possess bar and vehicle updates which are not triggered by events
local function CheckMiscellaneousUpdates()
	if IsPossessBarVisible() or UnitHasVehicleUI("player") then updateCooldowns = true; unitUpdate.player = true; doUpdate = true end
end

-- Update routine called before each frame is displayed, throttled to minimize CPU usage
function MOD:Update(elapsed)
	local elapsedTarget = MOD.db.global.UpdateRate or 0.2
	local refreshTarget = MOD.db.global.AnimationRate or 0.03
	if elapsedTime < 0 then elapsedTime = elapsed else elapsedTime = elapsedTime + elapsed end -- timer for update cycles
	if refreshTime < 0 then refreshTime = elapsed else refreshTime = refreshTime + elapsed end -- timer for refresh cycles
	throttleTime = throttleTime + elapsed -- timer for things that need to happen about once per second
	if throttleTime >= 1.0 then throttleTime = 0; doUpdate = true end -- equal to zero once per second
	if MOD.db.profile.enabled then
		if forceUpdate or (elapsedTime >= elapsedTarget) then -- limit update rate
			forceUpdate = false; updateCounter = updateCounter + 1; refreshCounter = refreshCounter + 1
			if not talentsInitialized then InitializeTalents() end -- retry until talents initialized
			CheckTotemUpdates() -- check if totems have changed since last update
			CheckMiscellaneousUpdates() -- check for update requirements that don't have events
			MOD:UpdateInternalCooldowns() -- check for expiring internal cooldowns
			MOD:UpdateCooldownTimes() -- check for expiring normal cooldowns
			if doUpdate or MOD:CheckTimeEvents() then -- only do major updates when events warrant it (but at least once a second)
				MOD:UpdateSpellEffects() -- update spell effect timers
				MOD:UpdateAuras() -- update table containing current auras (actual processing is deferred until needed)
				MOD:UpdateTrackers() -- update aura trackers for multiple targets
				MOD:UpdateCooldowns() -- update table containing current cooldowns on spells and trinkets
				MOD:UpdateConditions() -- update table containing currently triggered conditions
				MOD.Nest_CheckDisplayDimensions() -- check display dimensions and update anchors if they have changed
				MOD:UpdateBars() -- update timer bars for auras and cooldowns
				MOD:UpdateInCombatBar() -- update the in-combat bar if necessary
				MOD.Nest_Update() -- update the display using the Nest graphics package
			else
				MOD:RefreshInCombatBar() -- update in-combat bar animations only
				MOD.Nest_Refresh() -- refresh bars in the Nest graphics package (helps smooth animations)
			end
			elapsedTime = elapsedTime - elapsedTarget; refreshTime = refreshTime - refreshTarget; doUpdate = false
		else
			if refreshTime >= refreshTarget then -- limit animation refesh rate
				MOD:RefreshInCombatBar() -- update in-combat bar animations only
				MOD.Nest_Refresh() -- refresh bars in the Nest graphics package (helps smooth animations)
				refreshTime = refreshTime - refreshTarget; refreshCounter = refreshCounter + 1
			end
		end
	else
		if throttleTime == 0 then -- check occasionally to make sure everything is in the right state
			elapsedTime = 0; refreshTime = 0 -- reset these counters once per second as well
			MOD:HideBars()
			MOD:HideInCombatBar()
		end
	end
	if throttleTime == 0 then
		-- if IsAltKeyDown() then MOD.Debug("update", updateCounter, "refresh", refreshCounter) end
		updateCounter = 0; refreshCounter = 0 -- these counters are only used for testing purposes
		CheckBlizzFrames() -- need to check blizz settings occasionally
		if MOD.updateOptions then MOD:UpdateOptionsPanel() end -- update option panel once per second, if requested
	end
end

-- Aura tables have this structure:
-- b[1] = isBuff, b[2] = timeLeft, b[3] = stackCount, b[4] = auraType, b[5] = duration, b[6] = caster, b[7] = isStealable/effectCaster, b[8] = icon,
-- b[9] = rank, b[10] = expireTime, b[11] = tooltipType, b[12] = tooltipArgument, b[13] = name, b[14] = spellID, b[15] = isBoss, b[16] = casterName,
-- b[17] = castable, b[18] = casterIsNPC, b[19] = casterVehicle

-- Calculate aura time left from expiration time and current time, this is always done before returning aura descriptors
-- If no duration or has expired then set to 0 (Blizzard may not yet have sent aura update event so could sit at 0 for a moment)
local function SetAuraTimeLeft(b) if b[5] > 0 then b[2] = b[10] - now if b[2] < 0 then b[2] = 0 end else b[2] = 0 end end

-- Check if a GUID belongs to a boss per LibBossIDs
function MOD.CheckLibBossIDs(guid)
	if type(guid) == "string" then
		local id
		_, _, _, _, _, id = string.match(guid, "(%a+)%-(%d+)%-(%d+)%-(%d+)%-(%d+)%-(%d+)")
		if id then
			id = tonumber(id)
			if id and MOD.LibBossIDs.BossIDs[id] then return true end
		end
	end
	return false
end
	
-- Add an active aura to the table for the specified unit
local function AddAura(unit, name, isBuff, spellID, count, btype, duration, caster, steal, boss, apply, icon, rank, expire, tt_type, tt_arg)
	local auraTable = isBuff and activeBuffs[unit] or activeDebuffs[unit]
	local auraCache = isBuff and cacheBuffs[unit] or cacheDebuffs[unit]
	if auraTable then
		local b = AllocateTable() -- get an empty aura descriptor
		local cname, isNPC, vehicle = nil, false, false
		if caster then
			local guid = UnitGUID(caster); cname = UnitName(caster); vehicle = UnitHasVehicleUI(caster)
			if guid then
				local unitType = string.match(guid, "(%a+)%-")
				isNPC = (unitType == "Creature") or (unitType == "Vignette"); vehicle = vehicle or (unitType == "Vehicle")
				if isNPC and MOD.LibBossIDs then boss = boss or MOD.CheckLibBossIDs(guid) end
			end
		end
		b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19] =
			isBuff, 0, count, btype, duration, caster, steal, icon, rank, expire, tt_type, tt_arg, name, spellID, boss, cname, apply, isNPC, vehicle
		auraTable[#auraTable + 1] = b
		if auraCache then auraCache[name] = true end
		MOD:SetIcon(name, icon) --  cache icon for this aura
	end
end

-- Empty the aura tables for a unit by releasing all entries (except weapon buffs)
local function ReleaseAuras(unit)
	local buffTable, debuffTable, buffCache, debuffCache = activeBuffs[unit], activeDebuffs[unit], cacheBuffs[unit], cacheDebuffs[unit]
	table.wipe(buffCache); table.wipe(debuffCache)
	if buffTable then for index, b in pairs(buffTable) do if b[11] ~= "weapon" then buffTable[index] = ReleaseTable(b) else buffCache[b[13]] = true end end end
	if debuffTable then for index, b in pairs(debuffTable) do debuffTable[index] = ReleaseTable(b) end end
end

-- Check if aura(s) with given name are active on the unit (if isBuff is true only check buffs, otherwise only debuff)
-- Return a table with matching aura descriptors, potentially empty if none are found
-- The returned table is only valid until the next call to MOD:CheckAura since it is reused each time
function MOD:CheckAura(unit, name, isBuff)
	table.wipe(matchTable)
	unit = MOD:UnitStatusUpdate(unit)
	if unit then
		local auraTable = isBuff and activeBuffs[unit] or activeDebuffs[unit]
		local auraCache = isBuff and cacheBuffs[unit] or cacheDebuffs[unit]
		if auraTable then
			if auraCache and auraCache[name] then
				for _, b in pairs(auraTable) do if b[13] == name then SetAuraTimeLeft(b); matchTable[#matchTable + 1] = b end end
			elseif string.find(name, "^#%d+") then -- check if name is in special format for specific spell id (i.e., #12345)
				local id = tonumber(string.sub(name, 2)) -- extract the spell id
				if id then for _, b in pairs(auraTable) do if b[14] == id then SetAuraTimeLeft(b); matchTable[#matchTable + 1] = b end end end			
			end
		end
	end
	return matchTable
end

-- For all active auras on a given unit (if isBuff is true only buffs, otherwise only debuff), call the function that
-- is passed in with the unit, aura name, aura descriptor table, isBuff, and two optional parameters passed in
function MOD:IterateAuras(unit, func, isBuff, p1, p2, p3)
	local auraTable
	if unit == "all" then -- special case to get auras cast by player on multiple targets
		auraTable = isBuff and unitBuffs or unitDebuffs
		for _, tracker in pairs(auraTable) do
			for _, t in pairs(tracker) do
				SetAuraTimeLeft(t) -- update timeLeft from current time
				func(unit, t[13], t, isBuff, p1, p2, p3)
			end
		end
	else
		unit = MOD:UnitStatusUpdate(unit)
		if unit then
			auraTable = isBuff and activeBuffs[unit] or activeDebuffs[unit]
			if auraTable then
				for _, b in pairs(auraTable) do
					SetAuraTimeLeft(b) -- update timeLeft from current time
					func(unit, b[13], b, isBuff, p1, p2, p3)
				end
			end
		end
	end
end

-- Release a particular player buff, including multiple copies, used for weapon buffs and tracking due to non-standard detection
local function ReleasePlayerBuff(name)
	local auraTable = activeBuffs.player
	if auraTable then
		for index, b in pairs(auraTable) do if b[13] == name then auraTable[index] = ReleaseTable(b) end end
	end
end

-- Check all buffs on the unit to see if the specified buff type is currently active, return true and the first found
function MOD:UnitHasBuff(unit, btype)
	unit = MOD:UnitStatusUpdate(unit)
	if unit then
		local auraTable = activeBuffs[unit]
		if auraTable then
			for _, b in pairs(auraTable) do if (btype == "Steal") and (b[7] == 1) or (b[4] == btype) then SetAuraTimeLeft(b); return true, b end end
		end
	end
	return false, nil
end

-- Check all debuffs on the unit to see if the specified debuff type is currently active, return true and the first found
function MOD:UnitHasDebuff(unit, btype)
	unit = MOD:UnitStatusUpdate(unit)
	if unit then
		local auraTable = activeDebuffs[unit]
		if auraTable then
			for _, b in pairs(activeDebuffs[unit]) do if (b[4] == btype) then SetAuraTimeLeft(b); return true, b end end
		end
	end
	return false, nil
end

-- Initialize tooltip to be used for determining weapon buffs
-- This code is based on the Pitbull implementation
function MOD:InitializeBuffTooltip()
	bufftooltip = CreateFrame("GameTooltip", nil, UIParent)
	bufftooltip:SetOwner(UIParent, "ANCHOR_NONE")
	local fs = bufftooltip:CreateFontString()
	fs:SetFontObject(_G.GameFontNormal)
	bufftooltip.tooltiplines = {} -- cache of font strings for each line in the tooltip
	for i = 1, 30 do
		local ls = bufftooltip:CreateFontString()
		ls:SetFontObject(_G.GameFontNormal)
		bufftooltip:AddFontStrings(ls, fs)
		bufftooltip.tooltiplines[i] = ls
	end
end

-- Return the temporary table for storing buff tooltips
function MOD:GetBuffTooltip()
	bufftooltip:ClearLines()
	if not bufftooltip:IsOwned(UIParent) then bufftooltip:SetOwner(UIParent, "ANCHOR_NONE") end
	return bufftooltip
end

-- No easy way to get this info, so scan item slot info for mainhand and offhand weapons using a tooltip
-- Weapon buffs are usually formatted in tooltips as name strings followed by remaining time in parentheses
-- This routine scans the tooltip for the first line that is in this format and extracts the weapon buff name without rank or time
local function GetWeaponBuffName(weaponSlot)
	local tt = MOD:GetBuffTooltip()
	tt:SetInventoryItem("player", weaponSlot)
	for i = 1, 30 do
		local text = tt.tooltiplines[i]:GetText()
		if text then
			local name = text:match("^(.+) %(%d+ [^$)]+%)$") -- extract up to left paren if match weapon buff format
			if name then
				name = (name:match("^(.*) %d+$")) or name -- remove any trailing numbers
				return name
			end
		else
			break
		end
	end
	return nil
end

-- Get weapon buff duration, since this is not supplied by Blizzard look at current detected duration
-- and compare it to longest previous duration for the given weapon buff in order to find maximum ever detected
local function GetWeaponBuffDuration(buff, duration)
	local maxd = MOD.db.profile.WeaponBuffDurations[buff]
	if not maxd then maxd = MOD.db.global.BuffDurations[buff] end -- backward compatibility
	if not maxd or (duration > maxd) then
		MOD.db.profile.WeaponBuffDurations[buff] = math.floor(duration + 0.5) -- round up
	else
		if maxd > duration then duration = maxd end
	end
	return duration
end

-- Reset the weapon buff duration cache since it will be restored when buff is cast again
local function ResetWeaponBuffDuration(buff) MOD.db.profile.WeaponBuffDurations[buff] = nil; MOD.db.global.BuffDurations[buff] = nil end

-- Add player weapon buffs for mainhand and offhand to the aura table 
local function GetWeaponBuffs()
	-- old weapons buffs are now out-of-date so release them before regenerating		
	if mhLastBuff then ReleasePlayerBuff(mhLastBuff) end
	if ohLastBuff then ReleasePlayerBuff(ohLastBuff) end 
	
	-- first check if there are weapon auras then, only if necessary, use tooltip to scan for the buff names
	local mh, mhms, mhc, mx, oh, ohms, ohc, ox = GetWeaponEnchantInfo()
	if mh then -- add the mainhand buff, if any, to the table
		local islot = GetInventorySlotInfo("MainHandSlot")
		local mhbuff = GetWeaponBuffName(islot)
		if not mhbuff then -- if tooltip scan fails then use fallback of weapon name or slot name
			local weaponLink = GetInventoryItemLink("player", islot)
			if weaponLink then mhbuff = GetItemInfo(weaponLink) end
			if not mhbuff then mhbuff = L["Mainhand Weapon"] end
		end
		local icon = GetInventoryItemTexture("player", islot)					
		local timeLeft = mhms / 1000
		local expire = now + timeLeft
		local duration = GetWeaponBuffDuration(mhbuff, timeLeft)
		AddAura("player", mhbuff, true, nil, mhc, "Mainhand", duration, "player", nil, nil, 1, icon, nil, expire, "weapon", "MainHandSlot")
		mhLastBuff = mhbuff -- caches the name of the weapon buff so can clear it later
	elseif mhLastBuff then ResetWeaponBuffDuration(mhLastBuff); mhLastBuff = nil end
	
	if oh then -- add the offhand buff, if any, to the table
		local islot = GetInventorySlotInfo("SecondaryHandSlot")
		local ohbuff = GetWeaponBuffName(islot)
		if not ohbuff then -- if tooltip scan fails then use fallback of weapon name or slot name
			local weaponLink = GetInventoryItemLink("player", islot)
			if weaponLink then ohbuff = GetItemInfo(weaponLink) end
			if not ohbuff then ohbuff = L["Offhand Weapon"] end
		end
		local icon = GetInventoryItemTexture("player", islot)
		local timeLeft = ohms / 1000
		local expire = now + timeLeft
		local duration = GetWeaponBuffDuration(ohbuff, timeLeft)
		AddAura("player", ohbuff, true, nil, ohc, "Offhand", duration, "player", nil, nil, 1, icon, nil, expire, "weapon", "SecondaryHandSlot")
		ohLastBuff = ohbuff -- caches the name of the weapon buff so can clear it later
	elseif ohLastBuff then ResetWeaponBuffDuration(ohLastBuff); ohLastBuff = nil end
end

-- Add buffs for the specified unit to the active buffs table
local function GetBuffs(unit)
	local name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss
	local i = 1
	repeat
		name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss = UnitAura(unit, i, "HELPFUL")
		if name then
			if not caster then if spellID and fixEnchants[spellID] then caster = "player" else caster = "unknown" end -- fix Jade Spirit, Dancing Steel, River's Song
			elseif caster == "vehicle" then caster = "player" end -- vehicle buffs treated like player buffs
			if caster == "player" then MOD.SetDuration(name, spellID, duration); MOD.SetSpellType(spellID, btype) end
			AddAura(unit, name, true, spellID, count, btype, duration, caster, isStealable, boss, apply, icon, rank, expire, "buff", i)
		end
		i = i + 1
	until not name
	if unit ~= "player" or not UnitHasVehicleUI("player") then return end -- done for all but player, players also need to add vehicle buffs
	i = 1
	repeat
		name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss = UnitAura("vehicle", i, "HELPFUL")
		if name then
			if not caster then caster = "unknown" elseif caster == "vehicle" then caster = "player" end -- vehicle buffs treated like player buffs
			if caster == "player" then MOD.SetDuration(name, spellID, duration); MOD.SetSpellType(spellID, btype) end
			AddAura(unit, name, true, spellID, count, btype, duration, caster, isStealable, boss, apply, icon, rank, expire, "vehicle buff", i)
		end
		i = i + 1
	until not name
end

-- Add debuffs for the specified unit to the active debuffs table
local function GetDebuffs(unit)
	local name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss
	local i = 1
	repeat
		name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss = UnitAura(unit, i, "HARMFUL")
		if name then
			if not caster then caster = "unknown" elseif caster == "vehicle" then caster = "player" end -- vehicle debuffs treated like player debuffs
			if caster == "player" then MOD.SetDuration(name, spellID, duration); MOD.SetSpellType(spellID, btype) end
			AddAura(unit, name, false, spellID, count, btype, duration, caster, isStealable, boss, apply, icon, rank, expire, "debuff", i)
		end
		i = i + 1
	until not name
	if unit ~= "player" or not UnitHasVehicleUI("player") then return end -- done for all but player, players also need to add vehicle debuffs
	i = 1
	repeat
		name, rank, icon, count, btype, duration, expire, caster, isStealable, _, spellID, apply, boss = UnitAura("vehicle", i, "HARMFUL")
		if name then
			if not caster then caster = "unknown" elseif caster == "vehicle" then caster = "player" end -- vehicle debuffs treated like player debuffs
			if caster == "player" then MOD.SetDuration(name, spellID, duration); MOD.SetSpellType(spellID, btype) end
			AddAura(unit, name, false, spellID, count, btype, duration, caster, isStealable, boss, apply, icon, rank, expire, "vehicle debuff", i)
		end
		i = i + 1
	until not name
end

-- Add tracking auras (updated for Cataclysm which allows multiple active tracking types)
local function GetTracking()
	local notTracking, notTrackingIcon, found = L["Not Tracking"], "Interface\\Minimap\\Tracking\\None", false
	for i = 1, GetNumTrackingTypes() do
		local tracking, trackingIcon, active = GetTrackingInfo(i)
		if active then
			found = true
			AddAura("player", tracking, true, nil, 1, "Tracking", 0, "player", nil, nil, nil, trackingIcon, nil, 0, "tracking", tracking)
		end
	end
	if not found then
		AddAura("player", notTracking, true, nil, 1, "Tracking", 0, "player", nil, nil, nil, notTrackingIcon, nil, 0, "tracking", notTracking)
	end
end

-- Check if the spell triggers a spell effect
function MOD:DetectSpellEffect(name, caster)
	local ect = MOD.db.global.SpellEffects[name] -- check for new spell effect triggered by this spell	
	if ect and not ect.disable and MOD:CheckCastBy(caster, ect.caster or "player") then
		local duration = ect.duration
		if not duration then return end -- safety check
		if ect.talent and not MOD.CheckTalent(ect.talent) then return end -- check required talent
		if ect.buff then local auraList = MOD:CheckAura("player", ect.buff, true); if #auraList == 0 then return end end -- check required buff
		if ect.optbuff and ect.optduration then -- check optional buff and test safety for the duration
			local auraList = MOD:CheckAura("player", ect.optbuff, true)
			if #auraList > 0 then duration = ect.optduration end
		end
		if ect.condition and not MOD:CheckCondition(ect.condition) then return end -- check required condition
		local ec = spellEffects[name]
		if ec and ect.renew then spellEffects[name] = ReleaseTable(ec); ec = nil end -- check if already active spell effect and optionally renew
		if not ec then ec = AllocateTable(); ec.start = now; ec.expire = ec.start + duration; ec.caster = caster;
			spellEffects[name] = ec; TriggerPlayerUpdate() end
	end
end

-- Remove any spell effect entries that have expired
function MOD:UpdateSpellEffects()
	for id, ec in pairs(spellEffects) do if now >= ec.expire then spellEffects[id] = ReleaseTable(ec); TriggerPlayerUpdate(); TriggerCooldownUpdate() end end
end

-- Check if any spell effects are active and add them to the player auras
local function GetSpellEffectAuras()
	for name, ec in pairs(spellEffects) do
		local ect = MOD.db.global.SpellEffects[name]
		if ect and not ect.disable and ect.kind ~= "cooldown" then
			local spell = ect.spell or name
			AddAura("player", spell, not ect.kind, ect.id, 1, nil, ect.duration, ec.caster, UnitName(ec.caster), nil, nil, ect.icon, nil, ec.expire, "effect", name)
		end
	end
end

-- Create an aura for current stance for classes that don't include stance buffs
local function GetStanceAura()
	if MOD.myClass == "PALADIN" or MOD.myClass == "MONK" then
		local stance = GetShapeshiftForm()
		if stance and stance > 0 then
			local _, name = GetShapeshiftFormInfo(stance)
			if name then
				local icon = GetSpellTexture(name)
				local link = GetSpellLink(name)
				AddAura("player", name, true, nil, 1, "Stance", 0, "player", nil, nil, nil, icon, nil, 0, "spell link", link)
			end
		end
	end
end

-- Create an aura for class-specific power buffs: soul shards, holy power, shadow orbs, etc.
local function GetPowerBuffs()
	local power, id = nil, nil
	local myClass = MOD.myClass
	if myClass == "PALADIN" and IsSpellKnown(35395) then power = UnitPower("player", SPELL_POWER_HOLY_POWER); id = 85247
	elseif myClass == "PRIEST" and IsSpellKnown(8092) then power = UnitPower("player", SPELL_POWER_INSANITY); id = 57496
	elseif myClass == "WARLOCK" then power = UnitPower("player", SPELL_POWER_SOUL_SHARDS); id = 138556
	elseif myClass == "SHAMAN" and IsSpellKnown(193786) then power = UnitPower("player", SPELL_POWER_MAELSTROM); id = 190185
	elseif myClass == "MAGE" then
		if IsSpellKnown(116011) then -- rune of power
			local haveTotem, name, startTime, duration, icon = GetTotemInfo(1)
			if haveTotem and name and name ~= "" and now <= (startTime + duration) then
				local sp = GetSpellInfo(52623)
				AddAura("player", sp, true, 52623, 1, "Power", duration, "player", nil, nil, 1, icon, nil, startTime + duration, "text", sp)
			end
		end
		if IsSpellKnown(30451) then power = UnitPower("player", SPELL_POWER_ARCANE_CHARGES); id = 190427 end
	elseif myClass == "DEMONHUNTER" then
		if IsSpellKnown(203720) then -- vengeanance
			power = UnitPower("player", SPELL_POWER_PAIN); id = 185244
		else -- havoc
			power = UnitPower("player", SPELL_POWER_FURY); id = 67671
		end
	elseif myClass == "MONK" and IsSpellKnown(100780) then -- only windwalker has chi now
		local chi = UnitPower("player", SPELL_POWER_CHI)
		local _, pToken = UnitPowerType("player")
		local name = _G[pToken]
		local icon = GetSpellTexture(179126)
		if chi and chi > 0 then
			AddAura("player", name, true, nil, chi, "Power", 0, "player", nil, nil, nil, icon, nil, 0, "text", name)
			return
		end
	elseif myClass == "DRUID" then
		if IsSpellKnown(145205) then -- restoration druid has one mushroom linked to Efflorescence
			local haveTotem, name, startTime, duration, icon = GetTotemInfo(1)
			if haveTotem and name and name ~= "" and now <= (startTime + duration) then
				AddAura("player", name, true, 145205, 1, nil, duration or 0, "player", nil, nil, nil, icon, nil,
					(startTime or 0) + (duration or 0), "text", name)
				return
			end
		elseif IsSpellKnown(190984) then -- balance druid has astral power
			local ap = UnitPower("player", SPELL_POWER_LUNAR_POWER)
			local _, pToken = UnitPowerType("player")
			local name = _G[pToken]
			local icon = GetSpellTexture(164686) -- dark eclipse
			AddAura("player", name, true, nil, ap, "Power", 0, "player", nil, nil, nil, icon, nil, 0, "text", name)
			return
		end
	end
	if power and power > 0 then
		local name = GetSpellInfo(id)
		local icon = GetSpellTexture(id)
		if name and (name ~= "") then
			AddAura("player", name, true, id, power, "Power", 0, "player", nil, nil, nil, icon, nil, 0, "text", name)
		end
	end
end

-- Get buffs for shaman totems if option is selected
local function GetTotemBuffs()
	if MOD.myClass ~= "SHAMAN" then return end
	for i = 1, MAX_TOTEMS do
		local haveTotem, name, startTime, duration, icon = GetTotemInfo(i)
		if haveTotem and name and name ~= "" and now <= (startTime + duration) then -- generate buff for an active totem in the slot
			AddAura("player", name, true, nil, 1, "Totem", duration, "player", nil, nil, nil, icon, nil, startTime + duration, "totem", i)
		end
	end
end

-- Update unit auras if necessary (deferred until requested)
function MOD:UnitStatusUpdate(unit)
	local status = unitStatus[unit]
	if status ~= 0 then
		if status ~= 1 then unit = status end
		if unitUpdate[unit] then -- need to do an update for this unit
			ReleaseAuras(unit); GetBuffs(unit); GetDebuffs(unit)
			if unit == "player" then GetTracking(); GetSpellEffectAuras(); GetStanceAura(); GetPowerBuffs(); GetTotemBuffs() end
			unitUpdate[unit] = false
		end
		return unit
	end
	return nil
end

-- Check unit status, return 0 if doesn't exist, 1 if valid unit, "unit" if mirroring another unit
function MOD:ValidateUnit(unit)
	if UnitExists(unit) then
		for _, k in pairs(units) do
			if unit == k then return 1 end -- found unique unit
			if UnitIsUnit(unit, k) then return k end -- found match to higher priority unit
		end
	end
	return 0 -- not a valid unit
end

-- Check all the tracker entries and remove any that have expired
function MOD:UpdateTrackers()
	for _, tracker in pairs(unitBuffs) do
		for k, t in pairs(tracker) do SetAuraTimeLeft(t); if (t[5] > 0) and (t[2] == 0) then tracker[k] = ReleaseTable(t) end end
	end
	for _, tracker in pairs(unitDebuffs) do
		for k, t in pairs(tracker) do SetAuraTimeLeft(t); if (t[5] > 0) and (t[2] == 0) then tracker[k] = ReleaseTable(t) end end
	end

	if MOD.myClass == "WARLOCK" then -- check if warlock's summoned creatures have expired
		if soulEffigy then -- special case for soul effigy creature which is treated as totem 4
			local haveTotem = GetTotemInfo(4)
			if not haveTotem then summonedCreatures[soulEffigy] = now; ignoreGUID = soulEffigy; soulEffigy = nil end
		end
		for guid, expire in pairs(summonedCreatures) do
			if expire <= now then
				MOD:RemoveTrackers(guid) -- remove the trackers currently associated with this GUID
				cacheUnits[guid] = nil -- release the unit cache entry for this GUID too
				summonedCreatures[guid] = nil
			end
		end
		if not InCombatLockdown() then -- if out of combat then release unlimited duration trackers for Corruption (needed for Absolute Corruption talent)
			local corruption = GetSpellInfo(172) -- use localized string for Corruption instead of spell id, in case multiple ids are involved
			for _, tracker in pairs(unitDebuffs) do
				for k, t in pairs(tracker) do if (t[13] == corruption) and (t[5] == 0) then tracker[k] = ReleaseTable(t) end end
			end
		end
	end

	if (lastTrackers == 0) or ((now - lastTrackers) > 0.5) then -- things to do every half second...
		lastTrackers = now
		ValidateUnitIDs()
		table.wipe(refreshUnits) -- table of guids to prevent refreshing multiple times
		MOD:AddTrackers("player"); MOD:AddTrackers("target");  MOD:AddTrackers("focus")
		if IsInRaid() then
			for i = 1, GetNumGroupMembers() do MOD:AddTrackers("raid"..i); MOD:AddTrackers("raidpet"..i); MOD:AddTrackers("raid"..i.."target") end
		else
			for i = 1, GetNumGroupMembers() do MOD:AddTrackers("party"..i); MOD:AddTrackers("partypet"..i); MOD:AddTrackers("party"..i.."target") end
		end
		local pgid = UnitGUID("pet")
		if petGUID and (petGUID ~= pgid) then MOD:RemoveTrackers(petGUID) end
		petGUID = pgid; if pgid then MOD:AddTrackers("pet") end
		for i = 1, 40 do -- nameplate scanning improves accuracy dramatically
			local np = nameplateUnits[i]
			if UnitExists(np) then MOD:AddTrackers(np) else break end
		end
	end
end

--[[
function MOD.DebugTrackers(whence)
	MOD.Debug("Trackers: ", whence)
	for id, tracker in pairs(unitBuffs) do for k, t in pairs(tracker) do MOD.Debug("buff", id, t[13]) end end
	for id, tracker in pairs(unitDebuffs) do for k, t in pairs(tracker) do MOD.Debug("debuff", id, t[13]) end end
end
]]--

-- Update aura table with current player, target and focus auras and debuffs, include player weapon buffs
function MOD:UpdateAuras()
	for _, k in pairs(units) do unitStatus[k] = MOD:ValidateUnit(k)	end	 -- set current unit status, defer actual update until referenced
	for _, k in pairs(eventUnits) do unitUpdate[k] = (unitStatus[k] == 1) end -- can't count on events for these units
	if (lastWeapons == 0) or ((now - lastWeapons) > 1.0) then -- things to do every second...
		lastWeapons = now
		GetWeaponBuffs() -- get current weapon buffs, if any (less useful since WoD since no longer track shaman weapon enchants or rogue poisons)
	end
end

-- Cooldown tables have this structure (name of the cooldown is the index into the activeCooldowns table):
-- b[1] = timeLeft, b[2] = icon, b[3] = startTime, b[4] = duration, b[5] = tooltipType, b[6] = tooltipArgument, b[7] = unit, b[8] = id, b[9] = count

-- Check if valid cooldown table, if so then calculate time left from start time and duration and invalidate if cooldown has expired
-- Returns either the updated cooldown table or nil if not valid
local function ValidateCooldown(b) 
	if b[1] ~= nil then
		b[1] = b[3] + b[4] - now -- calculate timeLeft from start time and duration
		if b[1] > 0 then return b end -- check if the cooldown has expired 
		b[1] = nil -- this cooldown is no longer valid
		updateCooldowns = true; doUpdate = true
	end
	return nil
end

-- Add a cooldown to the current list of active cooldowns, cached info includes icon, start time, duration, tt_type, tt_arg, unit
local function AddCooldown(name, id, icon, start, duration, tt_type, tt_arg, unit, count)
	if lockedOut then -- check if this spell is on same cooldown as any lockout spell
		for ls, ld in pairs(lockouts) do if ld == duration and lockstarts[ls] == start then return end end
	end
	local t = activeCooldowns -- shared for player and pet cooldowns
	if not t[name] then
		MOD:SetIcon(name, icon) --  cache icon for this spell or item name
		t[name] = { 0, icon, start, duration, tt_type, tt_arg, unit, id, count }
	else
		local b = t[name]
		b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9] = 0, icon, start, duration, tt_type, tt_arg, unit, id, count
	end
end

-- Check if the named spell or item is on cooldown, return a cooldown table
function MOD:CheckCooldown(name)
	local b = activeCooldowns[name]
	if b then return ValidateCooldown(b) end
	return nil
end

-- Iterate over current cooldowns, calling the function with cooldown name, cooldown table, and optional parameters
function MOD:IterateCooldowns(func, p1, p2, p3)
	for n, cd in pairs(activeCooldowns) do if ValidateCooldown(cd) then func(n, cd, p1, p2, p3) end end
end

-- Release all spell cooldowns from active cooldowns table by setting first field to nil to indicate not active
local function ReleaseCooldowns() for _, cd in pairs(activeCooldowns) do cd[1] = nil end end

-- Update the expiration time for cooldowns, releasing any that have lapsed
function MOD:UpdateCooldownTimes() for _, b in pairs(activeCooldowns) do ValidateCooldown(b) end end

-- Get cooldown info for an inventory slot
local function CheckInventoryCooldown(slot)
	local id = GetInventorySlotInfo(slot)
	if id then
		local start, duration, enable = GetInventoryItemCooldown("player", id)
		if start and (start > 0) and (enable == 1) and (duration > 1.5) then
			local link = GetInventoryItemLink("player", id)
			if link then
				local spell = GetItemSpell(link)
				local name, _, _, _, _, _, _, _, equipSlot, icon = GetItemInfo(link)
				if spell and equipSlot ~= "INVTYPE_TRINKET" then name = spell end
				if name and icon then AddCooldown(name, id, icon, start, duration, "inventory", slot, "player") end
			end
		end
	end
end

-- Update info about the rune slots and add rune cooldowns
local function CheckRunes()
	local count = 0
	for i = 1, 6 do
		local rune = MOD.runeSlots[i]
		local start, duration, ready = GetRuneCooldown(i)
		if not rune then
			rune = { start = start, duration = duration, ready = ready }
			MOD.runeSlots[i] = rune
		else
			rune.start = start; rune.duration = duration; rune.ready = ready
		end
		if ready then count = count + 1 end
	end
	MOD.runeCount = count
end

-- Check if the spell is on cooldown because a rune is not available, return true only if on real cooldown
local function CheckRuneCooldown(name, duration)
	local runes = MOD.runeSpells[name]
	if runes and runes.count then
		if MOD.runeCount >= runes.count then return true end -- runes are available so real cooldown
		if duration <= 10 then return false end -- no spells that use runes have duration less than 10 seconds
	end
	return true
end

-- Check if an item is on cooldown
local function CheckItemCooldown(itemID)
	local start, duration = GetItemCooldown(itemID)
	if (start > 0) and (duration > 1.5) then -- don't include global cooldowns or really short cooldowns
		local name, link, _, _, _, itemType, itemSubType, _, _, icon = GetItemInfo(itemID)
		if name then
			local found = false
			if itemType == "Consumable" and (itemID ~= 86569) then -- check for shared cooldowns for potions/elixirs/flasks (special case Crystal of Insanity)
				if itemSubType == "Potion" then
					found = true
					if not MOD:CheckCooldown(L["Potions"]) then
						AddCooldown(L["Potions"], nil, iconPotion, start, duration, "text", L["Shared Potion Cooldown"], "player")
					end
				elseif (itemSubType == "Elixir") or (itemSubType == "Flask") then
					found = true
					if not MOD:CheckCooldown(L["Elixirs"]) then
						AddCooldown(L["Elixirs"], nil, iconElixir, start, duration, "text", L["Shared Elixir Cooldown"], "player")
					end
				end
			end
			if not found then
				AddCooldown(name, itemID, icon, start, duration, "item link", link, "player")
			end
		end
	end
end

-- Check if the aura either triggers or cancels an internal cooldown
-- Internal cooldown table indexed by aura that triggers the cooldown
function MOD:DetectInternalCooldown(name, caster)
	local up = false
	for id, cd in pairs(internalCooldowns) do -- check if cancels any active internal cooldowns
		if cd.cancel then
			for _, aura in pairs(cd.cancel) do if name == aura then internalCooldowns[id] = ReleaseTable(cd); up = true; break end end
		end
	end
	local ict = MOD.db.global.InternalCooldowns[name] -- check for new internal cooldown triggered by this aura
	if ict and not ict.disable and ((ict.caster == true) == caster) and (not ict.class or ict.class == MOD.myClass) and not internalCooldowns[name] then
		local cd = AllocateTable() -- get an empty tracker table
		cd.start = now; cd.expire = cd.start + ict.duration; cd.cancel = ict.cancel
		internalCooldowns[name] = cd
		up = true
	end
	if up then TriggerCooldownUpdate() end
end

-- Remove any internal cooldown entries that have expired
function MOD:UpdateInternalCooldowns()
	for name, cd in pairs(internalCooldowns) do if now >= cd.expire then internalCooldowns[name] = ReleaseTable(cd); TriggerCooldownUpdate() end end
end

-- Check for any internal cooldowns that are active
local function CheckInternalCooldowns()
	for name, cd in pairs(internalCooldowns) do
		local ict = MOD.db.global.InternalCooldowns[name]
		if ict and not ict.disable then AddCooldown(name, ict.id, ict.icon, cd.start, ict.duration, "internal", ict.id, "player") end
	end
end

-- Check for any internal cooldowns that are active
local function CheckSpellEffectCooldowns()
	for name, ec in pairs(spellEffects) do
		local ect = MOD.db.global.SpellEffects[name]
		if ect and not ect.disable and ect.kind == "cooldown" then
			local spell = ect.spell or name
			AddCooldown(spell, ect.id, ect.icon, ec.start, ec.expire - ec.start, "effect", name, "player")
		end
	end
end

-- Check for new and expiring cooldowns associated with all action bar slots plus trinkets (might want to add inventory slots someday)
function MOD:UpdateCooldowns()
	if updateCooldowns then
		ReleaseCooldowns() -- mark all cooldowns as not active
		if MOD.myClass == "DEATHKNIGHT" then CheckRunes() end
		lockedOut = false -- flag set if any lockout spells are found
		for school in pairs(lockouts) do lockouts[school] = 0 end -- clear any previous settings in lockout table
		if UnitLevel("player") >= 10 then -- don't detect lockouts for low-level characters, this allows more options for lockout detection spells
			for name, ls in pairs(MOD.lockoutSpells) do
				if not lockouts[ls.school] then lockouts[ls.school] = 0 end -- initialize when school seen for first time
				if ls.index and (lockouts[ls.school] == 0) then
					local start, duration = GetSpellCooldown(ls.index, "spell")
					if start and (start > 0) and (duration > 1.5) then -- locked out!
						lockouts[ls.school] = duration; lockstarts[ls.school] = start; lockedOut = true
						AddCooldown(ls.label, nil, iconGCD, start, duration, "spell", ls.text, "player")
					end
				end
			end
		end

		table.wipe(foundSpells)
		for tab = 1, 2 do -- scan first two tabs of player spell book (general and current spec) for player spells on cooldown
			local _, _, offset, numSpells = GetSpellTabInfo(tab)
			for i = 1, numSpells do
				local index = i + offset
				local stype, id = GetSpellBookItemInfo(index, "spell")
				if stype == "SPELL" then -- use spellbook index to check for cooldown
					local name = GetSpellInfo(index, "spell")
					if name and name ~= "" then
						knownSpells[name] = id; foundSpells[name] = id -- cache of previously seen spell names (ids are not sufficient)
						local start, duration, enable, count, charges
						count, charges, start, duration = GetSpellCharges(index, "spell")
						if count and charges and count < charges then enable = 1 else start, duration, enable = GetSpellCooldown(index, "spell") end
						if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
							if (MOD.myClass ~= "DEATHKNIGHT") or CheckRuneCooldown(name, duration) then -- if death knight check rune cooldown
								local icon = GetSpellTexture(id)
								AddCooldown(name, id, icon, start, duration, "spell id", id, "player", count)
							end
						end
					end
				elseif stype == "FLYOUT" then -- use spell id to check for cooldown
					local _, _, numSlots = GetFlyoutInfo(id)
					for slot = 1, numSlots do
						local spellID = GetFlyoutSlotInfo(id, slot)
						if spellID then
							local name = GetSpellInfo(spellID)
							if name and name ~= "" then -- make sure we have a valid spell name
								knownSpells[name] = spellID; foundSpells[name] = spellID -- cache of previously seen spell names
								local start, duration, enable = GetSpellCooldown(spellID)
								if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
									if (MOD.myClass ~= "DEATHKNIGHT") or CheckRuneCooldown(name, duration) then -- if death knight check rune cooldown
										local icon = GetSpellTexture(spellID)
										AddCooldown(name, spellID, icon, start, duration, "spell id", spellID, "player")
									end
								end
							end
						end
					end
				end
			end
		end

		for name, spellID in pairs(knownSpells) do -- special case for spells like Stormstrike with cooldowns that may not be in spellbook
			if not foundSpells[name] then -- found a previously known spell that is not currently in the spell book!
				local start, duration, enable = GetSpellCooldown(name)
				if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
					if (MOD.myClass ~= "DEATHKNIGHT") or CheckRuneCooldown(name, duration) then -- if death knight check rune cooldown
						local n = GetSpellInfo(name)
						if n and n ~= "" then
							local icon = GetSpellTexture(spellID)
							AddCooldown(name, spellID, icon, start, duration, "spell id", spellID, "player")
						end
					end
				end				
			end
		end
		
		local p = professions -- scan professions for spells on cooldown
		p[1], p[2], p[3], p[4], p[5], p[6] = GetProfessions()
		for index = 1, 6 do
			if p[index] then
				local prof, _, _, _, numSpells, offset = GetProfessionInfo(p[index])
				for i = 1, numSpells do
					local index = i + offset
					local stype, id = GetSpellBookItemInfo(index, "spell")
					if stype == "SPELL" then -- use spellbook index to check for cooldown
						local start, duration, enable = GetSpellCooldown(index, "spell")
						if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
							local name = GetSpellInfo(index, "spell")
							if name and name ~= "" then -- make sure we have a valid spell name
								local icon = GetSpellTexture(id)
								AddCooldown(name, id, icon, start, duration, "spell id", id, "player")
							end
						end
					end
				end
			end
		end

		local numSpells = HasPetSpells() -- returns the number of pet spells
		if numSpells and UnitExists("pet") then
			for i = 1, numSpells do
				local start, duration, enable = GetSpellCooldown(i, "pet")
				if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
					local name = GetSpellInfo(i, "pet")
					if name and name ~= "" then
						local _, spellID = GetSpellBookItemInfo(i, "pet")
						local icon = GetSpellTexture(spellID)
						AddCooldown(name, spellID, icon, start, duration, "spell id", spellID, "pet")
					end
				end
			end
		end
		
		local offset = nil -- check for override/vehicle bar actions on cooldown
		if HasVehicleActionBar() then offset = 132 elseif HasOverrideActionBar() then offset = 156 end
		if offset then
			for slot = 1, 6 do
				local actionType, spellID = GetActionInfo(slot + offset)
				if actionType == "spell" then
					local start, duration, enable = GetSpellCooldown(spellID)
					if start and (start > 0) and (enable == 1) and (duration > 1.5) then -- don't include global cooldowns
						local name = GetSpellInfo(spellID)
						if name and name ~= "" then
							local icon = GetSpellTexture(spellID)
							AddCooldown(name, spellID, icon, start, duration, "spell id", spellID, "player")
						end
					end
				end
			end
		end

		for bag = 0, NUM_BAG_SLOTS do
			local numSlots = GetContainerNumSlots(bag)
			for i = 1, numSlots do
				local itemID = GetContainerItemID(bag, i)
				if itemID then CheckItemCooldown(itemID) end
			end
		end
		
		CheckInventoryCooldown("HeadSlot")
		CheckInventoryCooldown("NeckSlot")
		CheckInventoryCooldown("BackSlot")
		CheckInventoryCooldown("ShoulderSlot")
		CheckInventoryCooldown("ChestSlot")
		CheckInventoryCooldown("ShirtSlot")
		CheckInventoryCooldown("TabardSlot")
		CheckInventoryCooldown("WristSlot")
		CheckInventoryCooldown("HandsSlot")
		CheckInventoryCooldown("WaistSlot")
		CheckInventoryCooldown("LegsSlot")
		CheckInventoryCooldown("FeetSlot")
		CheckInventoryCooldown("Finger0Slot")
		CheckInventoryCooldown("Finger1Slot")
		CheckInventoryCooldown("Trinket0Slot")
		CheckInventoryCooldown("Trinket1Slot")
		CheckInventoryCooldown("MainHandSlot")
		CheckInventoryCooldown("SecondaryHandSlot")

		if startGCD and durationGCD then -- detect global cooldowns
			local timeLeft = startGCD + durationGCD - now -- calculate timeLeft from start and duration
			if timeLeft > 0 then
				AddCooldown(L["GCD"], nil, iconGCD, startGCD, durationGCD, "text", L["Global Cooldown"], "player")
			else
				startGCD = nil; durationGCD = nil -- this cooldown is no longer valid
			end
		end
		
		CheckInternalCooldowns()
		CheckSpellEffectCooldowns()
		updateCooldowns = false
	end
end
