Quantcast
--[[--------------------------------------------------------------------
    Copyright (C) 2013, 2014 Johnny C. Lam.
    See the file LICENSE.txt for copying permission.
--]]--------------------------------------------------------------------

--[[
	This addon tracks all auras for all units.
--]]

local OVALE, Ovale = ...
local OvaleAura = Ovale:NewModule("OvaleAura", "AceEvent-3.0")
Ovale.OvaleAura = OvaleAura

--<private-static-properties>
local L = Ovale.L
local OvaleDebug = Ovale.OvaleDebug
local OvalePool = Ovale.OvalePool
local OvaleProfiler = Ovale.OvaleProfiler

-- Forward declarations for module dependencies.
local LibDispellable = LibStub("LibDispellable-1.0", true)
local OvaleData = nil
local OvaleFuture = nil
local OvaleGUID = nil
local OvalePaperDoll = nil
local OvaleSpellBook = nil
local OvaleState = nil

local bit_band = bit.band
local bit_bor = bit.bor
local floor = math.floor
local gmatch = string.gmatch
local ipairs = ipairs
local next = next
local pairs = pairs
local strfind = string.find
local strmatch = string.match
local strsub = string.sub
local tconcat = table.concat
local tinsert = table.insert
local tonumber = tonumber
local tsort = table.sort
local wipe = wipe
local API_GetTime = GetTime
local API_UnitAura = UnitAura
local API_UnitGUID = UnitGUID
local API_UnitHealth = UnitHealth
local API_UnitHealthMax = UnitHealthMax
local INFINITY = math.huge
local SCHOOL_MASK_ARCANE = SCHOOL_MASK_ARCANE
local SCHOOL_MASK_FIRE = SCHOOL_MASK_FIRE
local SCHOOL_MASK_FROST = SCHOOL_MASK_FROST
local SCHOOL_MASK_HOLY = SCHOOL_MASK_HOLY
local SCHOOL_MASK_NATURE = SCHOOL_MASK_NATURE
local SCHOOL_MASK_SHADOW = SCHOOL_MASK_SHADOW

-- Register for debugging messages.
OvaleDebug:RegisterDebugging(OvaleAura)
-- Register for profiling.
OvaleProfiler:RegisterProfiling(OvaleAura)

-- Player's GUID.
local self_guid = nil
-- Table pool.
local self_pool = OvalePool("OvaleAura_pool")
do
	self_pool.Clean = function(self, aura)
		-- Release reference-counted snapshot before wiping.
		if aura.snapshot then
			OvalePaperDoll:ReleaseSnapshot(aura.snapshot)
		end
	end
end

-- Some auras have a nil caster, so treat those as having a GUID of zero for indexing purposes.
local UNKNOWN_GUID = 0

do
	local output = {}
	local debugOptions = {
		playerAura = {
			name = L["Auras (player)"],
			type = "group",
			args = {
				buff = {
					name = L["Auras on the player"],
					type = "input",
					multiline = 25,
					width = "full",
					get = function(info)
						wipe(output)
						local helpful = OvaleState.state:DebugUnitAuras("player", "HELPFUL")
						if helpful then
							output[#output + 1] = "== BUFFS =="
							output[#output + 1] = helpful
						end
						local harmful = OvaleState.state:DebugUnitAuras("player", "HARMFUL")
						if harmful then
							output[#output + 1] = "== DEBUFFS =="
							output[#output + 1] = harmful
						end
						return tconcat(output, "\n")
					end,
				},
			},
		},
		targetAura = {
			name = L["Auras (target)"],
			type = "group",
			args = {
				targetbuff = {
					name = L["Auras on the target"],
					type = "execute",
					type = "input",
					multiline = 25,
					width = "full",
					get = function(info)
						wipe(output)
						local helpful = OvaleState.state:DebugUnitAuras("target", "HELPFUL")
						if helpful then
							output[#output + 1] = "== BUFFS =="
							output[#output + 1] = helpful
						end
						local harmful = OvaleState.state:DebugUnitAuras("target", "HARMFUL")
						if harmful then
							output[#output + 1] = "== DEBUFFS =="
							output[#output + 1] = harmful
						end
						return tconcat(output, "\n")
					end,
				},
			},
		},
	}
	-- Insert debug options into OvaleDebug.
	for k, v in pairs(debugOptions) do
		OvaleDebug.options.args[k] = v
	end
end

-- Aura debuff types.
local DEBUFF_TYPES = {
	Curse = true,
	Disease = true,
	Magic = true,
	Poison = true,
}

-- CLEU events triggered by auras being applied, removed, refreshed, or changed in stack size.
local CLEU_AURA_EVENTS = {
	SPELL_AURA_APPLIED = true,
	SPELL_AURA_REMOVED = true,
	SPELL_AURA_APPLIED_DOSE = true,
	SPELL_AURA_REMOVED_DOSE = true,
	SPELL_AURA_REFRESH = true,
	SPELL_AURA_BROKEN = true,
	SPELL_AURA_BROKEN_SPELL = true,
}

-- CLEU events triggered by a periodic aura.
local CLEU_TICK_EVENTS = {
	SPELL_PERIODIC_DAMAGE = true,
	SPELL_PERIODIC_HEAL = true,
	SPELL_PERIODIC_ENERGIZE = true,
	SPELL_PERIODIC_DRAIN = true,
	SPELL_PERIODIC_LEECH = true,
}

-- Spell school bitmask to identify magic effects.
local CLEU_SCHOOL_MASK_MAGIC = bit_bor(SCHOOL_MASK_ARCANE, SCHOOL_MASK_FIRE, SCHOOL_MASK_FROST, SCHOOL_MASK_HOLY, SCHOOL_MASK_NATURE, SCHOOL_MASK_SHADOW)
--</private-static-properties>

--<public-static-properties>
-- Auras on the target (past & present): aura[guid][auraId][casterGUID] = aura.
OvaleAura.aura = {}
-- Current age of auras per unit: serial[guid] = age.
OvaleAura.serial = {}
--</public-static-properties>

--<private-static-methods>
local function PutAura(auraDB, guid, auraId, casterGUID, aura)
	if not auraDB[guid] then
		auraDB[guid] = self_pool:Get()
	end
	if not auraDB[guid][auraId] then
		auraDB[guid][auraId] = self_pool:Get()
	end
	-- Remove any pre-existing aura at that slot.
	if auraDB[guid][auraId][casterGUID] then
		self_pool:Release(auraDB[guid][auraId][casterGUID])
	end
	-- Save the aura into that slot.
	auraDB[guid][auraId][casterGUID] = aura
	-- Set aura properties as a result of where it's slotted.
	aura.guid = guid
	aura.spellId = auraId
	aura.source = casterGUID
end

local function GetAura(auraDB, guid, auraId, casterGUID)
	if auraDB[guid] and auraDB[guid][auraId] and auraDB[guid][auraId][casterGUID] then
		return auraDB[guid][auraId][casterGUID]
	end
end

local function GetAuraAnyCaster(auraDB, guid, auraId)
	local auraFound
	if auraDB[guid] and auraDB[guid][auraId] then
		for casterGUID, aura in pairs(auraDB[guid][auraId]) do
			-- Find the aura with the latest expiration time.
			if not auraFound or auraFound.ending < aura.ending then
				auraFound = aura
			end
		end
	end
	return auraFound
end

local function GetDebuffType(auraDB, guid, debuffType, filter, casterGUID)
	local auraFound
	if auraDB[guid] then
		for auraId, whoseTable in pairs(auraDB[guid]) do
			local aura = whoseTable[casterGUID]
			if aura and aura.debuffType == debuffType and aura.filter == filter then
				-- Find the aura with the latest expiration time.
				if not auraFound or auraFound.ending < aura.ending then
					auraFound = aura
				end
			end
		end
	end
	return auraFound
end

local function GetDebuffTypeAnyCaster(auraDB, guid, debuffType, filter)
	local auraFound
	if auraDB[guid] then
		for auraId, whoseTable in pairs(auraDB[guid]) do
			for casterGUID, aura in pairs(whoseTable) do
				if aura and aura.debuffType == debuffType and aura.filter == filter then
					-- Find the aura with the latest expiration time.
					if not auraFound or auraFound.ending < aura.ending then
						auraFound = aura
					end
				end
			end
		end
	end
	return auraFound
end

local function GetAuraOnGUID(auraDB, guid, auraId, filter, mine)
	local auraFound
	if DEBUFF_TYPES[auraId] then
		if mine then
			auraFound = GetDebuffType(auraDB, guid, auraId, filter, self_guid)
		else
			auraFound = GetDebuffTypeAnyCaster(auraDB, guid, auraId, filter)
		end
	else
		if mine then
			auraFound = GetAura(auraDB, guid, auraId, self_guid)
		else
			auraFound = GetAuraAnyCaster(auraDB, guid, auraId)
		end
	end
	return auraFound
end

local function RemoveAurasOnGUID(auraDB, guid)
	if auraDB[guid] then
		local auraTable = auraDB[guid]
		for auraId, whoseTable in pairs(auraTable) do
			for casterGUID, aura in pairs(whoseTable) do
				self_pool:Release(aura)
				whoseTable[casterGUID] = nil
			end
			self_pool:Release(whoseTable)
			auraTable[auraId] = nil
		end
		self_pool:Release(auraTable)
		auraDB[guid] = nil
	end
end

local function IsEnrageEffect(auraId)
	local boolean = OvaleData.buffSpellList.enrage[auraId]
	if LibDispellable then
		boolean = LibDispellable:IsEnrageEffect(auraId)
	end
	return boolean or nil
end

local function IsWithinAuraLag(time1, time2, factor)
	factor = factor or 1
	local auraLag = Ovale.db.profile.apparence.auraLag
	local tolerance = factor * auraLag / 1000
	return (time1 - time2 < tolerance) and (time2 - time1 < tolerance)
end
--</private-static-methods>

--<public-static-methods>
function OvaleAura:OnInitialize()
	-- Resolve module dependencies.
	OvaleData = Ovale.OvaleData
	OvaleFuture = Ovale.OvaleFuture
	OvaleGUID = Ovale.OvaleGUID
	OvalePaperDoll = Ovale.OvalePaperDoll
	OvaleSpellBook = Ovale.OvaleSpellBook
	OvaleState = Ovale.OvaleState
end

function OvaleAura:OnEnable()
	self_guid = API_UnitGUID("player")
	self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
	self:RegisterEvent("PLAYER_ENTERING_WORLD", "ScanAllUnitAuras")
	self:RegisterEvent("PLAYER_REGEN_ENABLED")
	self:RegisterEvent("PLAYER_TARGET_CHANGED")
	self:RegisterEvent("UNIT_AURA")
	self:RegisterMessage("Ovale_GroupChanged", "ScanAllUnitAuras")
	OvaleData:RegisterRequirement("buff", "RequireBuffHandler", self)
	OvaleData:RegisterRequirement("debuff", "RequireBuffHandler", self)
	OvaleData:RegisterRequirement("stealth", "RequireStealthHandler", self)
	OvaleData:RegisterRequirement("stealthed", "RequireStealthHandler", self)
	OvaleData:RegisterRequirement("target_buff", "RequireBuffHandler", self)
	OvaleData:RegisterRequirement("target_debuff", "RequireBuffHandler", self)
	OvaleData:RegisterRequirement("target_health_pct", "RequireTargetHealthPercentHandler", self)
	OvaleState:RegisterState(self, self.statePrototype)
end

function OvaleAura:OnDisable()
	OvaleState:UnregisterState(self)
	OvaleData:UnregisterRequirement("buff")
	OvaleData:UnregisterRequirement("debuff")
	OvaleData:UnregisterRequirement("stealth")
	OvaleData:UnregisterRequirement("stealthed")
	OvaleData:UnregisterRequirement("target_buff")
	OvaleData:UnregisterRequirement("target_debuff")
	OvaleData:UnregisterRequirement("target_health_pct")
	self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self:UnregisterEvent("PLAYER_REGEN_ENABLED")
	self:UnregisterEvent("PLAYER_TARGET_CHANGED")
	self:UnregisterEvent("PLAYER_UNGHOST")
	self:UnregisterEvent("UNIT_AURA")
	self:UnregisterMessage("Ovale_GroupChanged")
	for guid in pairs(self.aura) do
		RemoveAurasOnGUID(self.aura, guid)
	end
	self_pool:Drain()
end

function OvaleAura:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, cleuEvent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
	local arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25 = ...

	local mine = (sourceGUID == self_guid)
	if CLEU_AURA_EVENTS[cleuEvent] then
		local unitId = OvaleGUID:GetUnitId(destGUID)
		if unitId then
			-- Only update auras on the unit if it is not a unit type that receives UNIT_AURA events.
			if not OvaleGUID.UNIT_AURA_UNIT[unitId] then
				self:Debug(true, "%s: %s (%s)", cleuEvent, destGUID, unitId)
				self:ScanAuras(unitId, destGUID)
			end
		elseif mine then
			-- There is no unit ID, but the action was caused by the player, so update this aura on destGUID.
			local spellId, spellName, spellSchool = arg12, arg13, arg14
			self:Debug(true, "%s: %s (%d) on %s", cleuEvent, spellName, spellId, destGUID)
			local now = API_GetTime()
			if cleuEvent == "SPELL_AURA_REMOVED" or cleuEvent == "SPELL_AURA_BROKEN" or cleuEvent == "SPELL_AURA_BROKEN_SPELL" then
				self:LostAuraOnGUID(destGUID, now, spellId, sourceGUID)
			else
				local auraType, amount = arg15, arg16
				local filter = (auraType == "BUFF") and "HELPFUL" or "HARMFUL"
				local si = OvaleData.spellInfo[spellId]
				-- Find an existing aura applied by the player on destGUID.
				local aura = GetAuraOnGUID(self.aura, destGUID, spellId, filter, true)
				local duration
				if aura then
					-- Re-use the duration of the previous aura on the target.
					duration = aura.duration
				elseif si and si.duration then
					-- Look up the duration from the SpellInfo.
					duration = OvaleData:GetSpellInfoProperty(spellId, "duration", unitId)
					if si.addduration then
						duration = duration + si.addduration
					end
				else
					-- No aura duration information known and we can't scan the aura on that GUID,
					-- so assume the aura lasts 15 seconds.
					-- TODO: There is probably something smarter to be done here.
					duration = 15
				end
				local expirationTime = now + duration
				local count
				if cleuEvent == "SPELL_AURA_APPLIED" then
					count = 1
				elseif cleuEvent == "SPELL_AURA_APPLIED_DOSE" or cleuEvent == "SPELL_AURA_REMOVED_DOSE" then
					count = amount
				elseif cleuEvent == "SPELL_AURA_REFRESH" then
					count = aura and aura.stacks or 1
				end
				self:GainedAuraOnGUID(destGUID, now, spellId, sourceGUID, filter, true, nil, count, nil, duration, expirationTime, nil, spellName)
			end
		end
	elseif mine and CLEU_TICK_EVENTS[cleuEvent] then
		-- Update the latest tick time of the periodic aura cast by the player.
		local spellId, spellName, spellSchool = arg12, arg13, arg14
		local multistrike
		if strsub(cleuEvent, -7) == "_DAMAGE" then
			multistrike = arg25
		elseif strsub(cleuEvent, -5) == "_HEAL" then
			multistrike = arg19
		end
		if not multistrike then
			local unitId = OvaleGUID:GetUnitId(destGUID)
			if unitId then
				self:Debug(true, "%s: %s (%s)", cleuEvent, destGUID, unitId)
			else
				self:Debug(true, "%s: %s", cleuEvent, destGUID)
			end
			local aura = GetAura(self.aura, destGUID, spellId, self_guid)
			if self:IsActiveAura(aura) then
				local name = aura.name or "Unknown spell"
				local baseTick, lastTickTime = aura.baseTick, aura.lastTickTime
				local tick = baseTick
				if lastTickTime then
					-- Update the tick length based on the timestamps of the current tick and the previous tick.
					tick = timestamp - lastTickTime
				elseif not baseTick then
					-- This isn't a known periodic aura, but it's ticking so treat this as the first tick.
					self:Debug("    First tick seen of unknown periodic aura %s (%d) on %s.", name, spellId, destGUID)
					local si = OvaleData.spellInfo[spellId]
					baseTick = (si and si.tick) and si.tick or 3
					tick = OvaleData:GetTickLength(spellId)
				end
				aura.baseTick = baseTick
				aura.lastTickTime = timestamp
				aura.tick = tick
				self:Debug("    Updating %s (%s) on %s, tick=%s, lastTickTime=%s", name, spellId, destGUID, tick, lastTickTime)
			end
		end
	end
end

function OvaleAura:PLAYER_REGEN_ENABLED(event)
	self:RemoveAurasOnInactiveUnits()
	self_pool:Drain()
end

function OvaleAura:PLAYER_TARGET_CHANGED(event, cause)
	if cause == "NIL" or cause == "down" then
		-- Target was cleared.
	else
		-- Target has changed.
		self:Debug(event)
		self:ScanAuras("target")
	end
end

function OvaleAura:UNIT_AURA(event, unitId)
	self:Debug("%s: %s", event, unitId)
	self:ScanAuras(unitId)
end

function OvaleAura:ScanAllUnitAuras()
	-- Update auras on all visible units.
	for unitId in pairs(OvaleGUID.UNIT_AURA_UNIT) do
		self:ScanAuras(unitId)
	end
end

function OvaleAura:RemoveAurasOnInactiveUnits()
	-- Remove all auras from GUIDs that can no longer be referenced by a unit ID,
	-- i.e., not in the group or not targeted by anyone in the group or focus.
	for guid in pairs(self.aura) do
		local unitId = OvaleGUID:GetUnitId(guid)
		if not unitId then
			self:Debug("Removing auras from GUID %s", guid)
			RemoveAurasOnGUID(self.aura, guid)
			self.serial[guid] = nil
		end
	end
end

function OvaleAura:IsActiveAura(aura, now)
	local boolean = false
	if aura then
		now = now or API_GetTime()
		if aura.serial == self.serial[aura.guid] and aura.stacks > 0 and aura.gain <= now and now <= aura.ending then
			boolean = true
		elseif aura.consumed and IsWithinAuraLag(aura.ending, now) then
			boolean = true
		end
	end
	return boolean
end

function OvaleAura:GainedAuraOnGUID(guid, atTime, auraId, casterGUID, filter, visible, icon, count, debuffType, duration, expirationTime, isStealable, name, value1, value2, value3)
	self:StartProfiling("OvaleAura_GainedAuraOnGUID")
	-- Whose aura is it?
	casterGUID = casterGUID or UNKNOWN_GUID
	local mine = (casterGUID == self_guid)

	-- UnitAura() can return zero count for auras that are present.
	count = (count and count > 0) and count or 1
	-- "Zero" or nil duration and expiration actually mean the aura never expires.
	duration = (duration and duration > 0) and duration or INFINITY
	expirationTime = (expirationTime and expirationTime > 0) and expirationTime or INFINITY

	local aura = GetAura(self.aura, guid, auraId, casterGUID)
	local auraIsActive
	if aura then
		auraIsActive = (aura.stacks > 0 and aura.gain <= atTime and atTime <= aura.ending)
	else
		aura = self_pool:Get()
		PutAura(self.aura, guid, auraId, casterGUID, aura)
		auraIsActive = false
	end

	-- Only overwrite an active aura's information if the aura has changed.
	-- An aura's "fingerprint" is its: caster, duration, expiration time, stack count, value
	local auraIsUnchanged = (
		aura.source == casterGUID
			and aura.duration == duration
			and aura.ending == expirationTime
			and aura.stacks == count
			and aura.value1 == value1
			and aura.value2 == value2
			and aura.value3 == value3
	)

	-- Update age of aura, regardless of whether it's changed.
	aura.serial = self.serial[guid]

	if not auraIsActive or not auraIsUnchanged then
		self:Debug("    Adding %s %s (%s) to %s at %f, aura.serial=%d", filter, name, auraId, guid, atTime, aura.serial)
		aura.name = name
		aura.duration = duration
		aura.ending = expirationTime
		if duration < INFINITY and expirationTime < INFINITY then
			aura.start = expirationTime - duration
		else
			aura.start = atTime
		end
		aura.gain = atTime
		aura.lastUpdated = atTime
		local direction = aura.direction or 1
		if aura.stacks then
			if aura.stacks < count then
				direction = 1	-- increasing stack count
			elseif aura.stacks > count then
				direction = -1	-- decreasing stack count
			end
		end
		aura.direction = direction
		aura.stacks = count
		aura.consumed = nil
		aura.filter = filter
		aura.visible = visible
		aura.icon = icon
		aura.debuffType = debuffType
		aura.enrage = IsEnrageEffect(auraId)
		aura.stealable = isStealable
		aura.value1, aura.value2, aura.value3 = value1, value2, value3

		-- Map the GUID to a unit ID.
		local unitId = OvaleGUID:GetUnitId(guid)

		-- Snapshot stats for auras applied by the player.
		if mine then
			-- Determine whether to snapshot player stats for the aura or to keep the existing stats.
			local spellcast = OvaleFuture:LastInFlightSpell()
			if spellcast and spellcast.stop and not IsWithinAuraLag(spellcast.stop, atTime) then
				spellcast = OvaleFuture.lastSpellcast
				if spellcast and spellcast.stop and not IsWithinAuraLag(spellcast.stop, atTime) then
					spellcast = nil
				end
			end
			if spellcast and spellcast.target == guid then
				local spellId = spellcast.spellId
				local spellName = OvaleSpellBook:GetSpellName(spellId) or "Unknown spell"
				-- Parse the spell data for this aura to see if this is a "refresh_keep_snapshot" aura.
				local keepSnapshot = false
				local si = OvaleData.spellInfo[spellId]
				if si and si.aura and si.aura.target and si.aura.target[filter] then
					local spellData = si.aura.target[filter][auraId]
					if spellData and strsub(spellData, 1, 21) == "refresh_keep_snapshot" then
						local tokenIterator
						if strfind(spellData, ",") then
							tokenIterator = gmatch(spellData, "[^,]+")
							-- Advance past "refresh_keep_snapshot".
							tokenIterator()
						end
						if tokenIterator then
							keepSnapshot = OvaleData:CheckRequirements(spellId, tokenIterator, unitId)
						else
							keepSnapshot = true
						end
					end
				end
				if keepSnapshot then
					self:Debug("    Keeping snapshot stats for %s %s (%d) on %s refreshed by %s (%d) from %f, now=%f, aura.serial=%d",
						filter, name, auraId, guid, spellName, spellId, spellcast.snapshot.snapshotTime, atTime, aura.serial)
				else
					self:Debug("    Snapshot stats for %s %s (%d) on %s applied by %s (%d) from %f, now=%f, aura.serial=%d",
						filter, name, auraId, guid, spellName, spellId, spellcast.snapshot.snapshotTime, atTime, aura.serial)
					-- TODO: damageMultiplier isn't correct if spellId spreads the DoT.
					OvaleFuture:UpdateFromSpellcast(aura, spellcast)
				end
			end

			local si = OvaleData.spellInfo[auraId]
			if si then
				-- Set the tick information for known DoTs.
				if si.tick then
					self:Debug("    %s (%s) is a periodic aura.", name, auraId)
					-- Only set the initial tick information for new auras.
					if not auraIsActive then
						aura.baseTick = si.tick
						if spellcast and spellcast.target == guid then
							aura.tick = OvaleData:GetTickLength(auraId, spellcast.snapshot)
						else
							aura.tick = OvaleData:GetTickLength(auraId)
						end
					end
				end
				-- Set the cooldown expiration time for player buffs applied by items with a cooldown.
				if si.buff_cd and guid == self_guid then
					self:Debug("    %s (%s) is applied by an item with a cooldown of %ds.", name, auraId, si.buff_cd)
					if not auraIsActive then
						-- cooldownEnding is the earliest time at which we expect to gain this buff again.
						aura.cooldownEnding = aura.gain + si.buff_cd
					end
				end
			end
		end
		if not auraIsActive then
			self:SendMessage("Ovale_AuraAdded", atTime, guid, auraId, aura.source)
		elseif not auraIsUnchanged then
			self:SendMessage("Ovale_AuraChanged", atTime, guid, auraId, aura.source)
		end
		if unitId then
			Ovale.refreshNeeded[unitId] = true
		end
	end
	self:StopProfiling("OvaleAura_GainedAuraOnGUID")
end

function OvaleAura:LostAuraOnGUID(guid, atTime, auraId, casterGUID)
	self:StartProfiling("OvaleAura_LostAuraOnGUID")
	local aura = GetAura(self.aura, guid, auraId, casterGUID)
	if aura then
		local filter = aura.filter
		self:Debug("    Expiring %s %s (%d) from %s at %f.", filter, aura.name, auraId, guid, atTime)
		if aura.ending > atTime then
			aura.ending = atTime
		end

		local mine = (casterGUID == self_guid)
		if mine then
			-- Clear old tick information for player-applied periodic auras.
			aura.baseTick = nil
			aura.lastTickTime = nil
			aura.tick = nil

			-- Check if the aura was consumed by the last spellcast.
			-- The aura must have ended early, i.e., start + duration > ending.
			if aura.start + aura.duration > aura.ending then
				local spellcast
				if guid == self_guid then
					-- Player aura, so it was possibly consumed by an in-flight spell.
					spellcast = OvaleFuture:LastInFlightSpell()
				else
					-- Non-player aura, so it was possibly consumed by a spell that landed on its target.
					spellcast = OvaleFuture.lastSpellcast
				end
				if spellcast and spellcast.stop and IsWithinAuraLag(spellcast.stop, aura.ending) then
					aura.consumed = true
					local spellName = OvaleSpellBook:GetSpellName(spellcast.spellId) or "Unknown spell"
					self:Debug("    Consuming %s %s (%d) on %s with %s (%d) at %f.", filter, aura.name, auraId, guid, spellName, spellcast.spellId, spellcast.stop)
				end
			end
		end
		aura.lastUpdated = atTime

		self:SendMessage("Ovale_AuraRemoved", atTime, guid, auraId, aura.source)
		local unitId = OvaleGUID:GetUnitId(guid)
		if unitId then
			Ovale.refreshNeeded[unitId] = true
		end
	end
	self:StopProfiling("OvaleAura_LostAuraOnGUID")
end

-- Scan auras on the given GUID and update the aura database.
function OvaleAura:ScanAuras(unitId, guid)
	self:StartProfiling("OvaleAura_ScanAuras")
	guid = guid or OvaleGUID:GetGUID(unitId)
	if guid then
		self:Debug(true, "Scanning auras on %s (%s)", guid, unitId)

		-- Advance the age of the unit's auras.
		local serial = self.serial[guid] or 0
		serial = serial + 1
		self:Debug("    Advancing age of auras for %s (%s) to %d.", guid, unitId, serial)
		self.serial[guid] = serial

		-- Add all auras on the unit into the database.
		local i = 1
		local filter = "HELPFUL"
		local now = API_GetTime()
		while true do
			local name, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, shouldConsolidate, spellId,
				canApplyAura, isBossDebuff, isCastByPlayer, value1, value2, value3 = API_UnitAura(unitId, i, filter)
			if not name then
				if filter == "HELPFUL" then
					filter = "HARMFUL"
					i = 1
				else
					break
				end
			else
				local casterGUID = OvaleGUID:GetGUID(unitCaster)
				self:GainedAuraOnGUID(guid, now, spellId, casterGUID, filter, true, icon, count, debuffType, duration, expirationTime, isStealable, name, value1, value2, value3)
				i = i + 1
			end
		end

		-- Find recently expired auras on the unit.
		if self.aura[guid] then
			local auraTable = self.aura[guid]
			for auraId, whoseTable in pairs(auraTable) do
				for casterGUID, aura in pairs(whoseTable) do
					if aura.serial == serial - 1 then
						if aura.visible then
							-- Remove the aura if it was visible.
							self:LostAuraOnGUID(guid, now, auraId, casterGUID)
						else
							-- Age any hidden auras that are managed by outside modules.
							aura.serial = serial
						end
					end
				end
			end
		end
	end
	self:StopProfiling("OvaleAura_ScanAuras")
end

function OvaleAura:GetAuraByGUID(guid, auraId, filter, mine)
	-- If this GUID has no auras in the database, then do an aura scan.
	if not self.serial[guid] then
		local unitId = OvaleGUID:GetUnitId(guid)
		self:ScanAuras(unitId, guid)
	end

	local auraFound
	if OvaleData.buffSpellList[auraId] then
		for id in pairs(OvaleData.buffSpellList[auraId]) do
			local aura = GetAuraOnGUID(self.aura, guid, id, filter, mine)
			if aura and (not auraFound or auraFound.ending < aura.ending) then
				auraFound = aura
			end
		end
	else
		auraFound = GetAuraOnGUID(self.aura, guid, auraId, filter, mine)
	end
	return auraFound
end

function OvaleAura:GetAura(unitId, auraId, filter, mine)
	local guid = OvaleGUID:GetGUID(unitId)
	return self:GetAuraByGUID(guid, auraId, filter, mine)
end

-- Run-time check for an aura on the player or the target.
-- NOTE: Mirrored in statePrototype below.
function OvaleAura:RequireBuffHandler(spellId, requirement, tokenIterator, target)
	local verified = false
	local buffName = tokenIterator()
	if buffName then
		local isBang = false
		if strsub(buffName, 1, 1) == "!" then
			isBang = true
			buffName = strsub(buffName, 2)
		end
		local buffName = tonumber(buffName) or buffName
		local unitId, filter, mine
		if strsub(requirement, 1, 7) == "target_" then
			unitId = target
			filter = (strsub(requirement, 8) == "buff") and "HELPFUL" or "HARMFUL"
			mine = true
		else
			unitId = "player"
			filter = (requirement == "buff") and "HELPFUL" or "HARMFUL"
			mine = true
		end
		local aura = self:GetAura(unitId, buffName, filter, mine)
		local isActiveAura = self:IsActiveAura(aura)
		if not isBang and isActiveAura or isBang and not isActiveAura then
			verified = true
		end
		local result = verified and "passed" or "FAILED"
		if isBang then
			self:Log("    Require aura %s NOT on %s: %s", buffName, unitId, result)
		else
			self:Log("    Require aura %s on %s: %s", buffName, unitId, result)
		end
	else
		Ovale:OneTimeMessage("Warning: requirement '%s' is missing a buff argument.", requirement)
	end
	return verified, requirement
end

-- Run-time check for the player being stealthed.
-- NOTE: Mirrored in statePrototype below.
function OvaleAura:RequireStealthHandler(spellId, requirement, tokenIterator, target)
	local verified = false
	local stealthed = tokenIterator()
	if stealthed then
		stealthed = tonumber(stealthed)
		local aura = self:GetAura("player", "stealthed_buff", "HELPFUL", true)
		local isActiveAura = self:IsActiveAura(aura)
		if stealthed == 1 and isActiveAura or stealthed ~= 1 and not isActiveAura then
			verified = true
		end
		local result = verified and "passed" or "FAILED"
		if stealthed == 1 then
			self:Log("    Require stealth: %s", result)
		else
			self:Log("    Require NOT stealth: %s", result)
		end
	else
		Ovale:OneTimeMessage("Warning: requirement '%s' is missing an argument.", requirement)
	end
	return verified, requirement
end

-- Run-time check that the target is below a health percent threshold.
-- NOTE: Mirrored in statePrototype below.
-- TODO: This function should really be moved to a Health module.
function OvaleAura:RequireTargetHealthPercentHandler(spellId, requirement, tokenIterator, target)
	local verified = false
	local threshold = tokenIterator()
	if threshold then
		local isBang = false
		if strsub(threshold, 1, 1) == "!" then
			isBang = true
			threshold = strsub(threshold, 2)
		end
		threshold = tonumber(threshold) or 0
		local healthMax = API_UnitHealthMax(target)
		healthMax = healthMax > 0 and healthMax or 1
		local healthPercent = API_UnitHealth(target) / healthMax * 100
		if not isBang and healthPercent <= threshold or isBang and healthPercent > threshold then
			verified = true
		end
		local result = verified and "passed" or "FAILED"
		if isBang then
			self:Log("    Require target health > %f%%: %s", threshold, result)
		else
			self:Log("    Require target health <= %f%%: %s", threshold, result)
		end
	else
		Ovale:OneTimeMessage("Warning: requirement '%s' is missing a threshold argument.", requirement)
	end
	return verified, requirement
end
--</public-static-methods>

--[[----------------------------------------------------------------------------
	State machine for simulator.
--]]----------------------------------------------------------------------------

--<public-static-properties>
OvaleAura.statePrototype = {
	aura = nil,
	serial = nil,
}
--</public-static-properties>

--<private-static-properties>
local statePrototype = OvaleAura.statePrototype
--</private-static-properties>

--<state-properties>
-- Aura database: aura[guid][auraId][casterId] = aura
statePrototype.aura = nil
-- Age of active auras in the simulator.
statePrototype.serial = nil
--</state-properties>

--<public-static-methods>
-- Initialize the state.
function OvaleAura:InitializeState(state)
	state.aura = {}
	state.serial = 0
end

-- Reset the state to the current conditions.
function OvaleAura:ResetState(state)
	self:StartProfiling("OvaleAura_ResetState")
	-- Advance age of auras in state machine.
	state.serial = state.serial + 1

	-- Garbage-collect auras in the state machine that are more recently updated in the true aura database.
	if next(state.aura) then
		state:Log("Resetting aura state:")
	end
	for guid, auraTable in pairs(state.aura) do
		for auraId, whoseTable in pairs(auraTable) do
			for casterGUID, aura in pairs(whoseTable) do
				self_pool:Release(aura)
				whoseTable[casterGUID] = nil
				state:Log("    Aura %d on %s removed, now=%f.", auraId, guid, state.currentTime)
			end
			if not next(whoseTable) then
				self_pool:Release(whoseTable)
				auraTable[auraId] = nil
			end
		end
		if not next(auraTable) then
			self_pool:Release(auraTable)
			state.aura[guid] = nil
		end
	end
	self:StopProfiling("OvaleAura_ResetState")
end

-- Release state resources prior to removing from the simulator.
function OvaleAura:CleanState(state)
	for guid in pairs(state.aura) do
		RemoveAurasOnGUID(state.aura, guid)
	end
end

-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
function OvaleAura:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast)
	self:StartProfiling("OvaleAura_ApplySpellAfterCast")
	local si = OvaleData.spellInfo[spellId]
	-- Apply the auras on the player.
	if si and si.aura and si.aura.player then
		state:ApplySpellAuras(spellId, self_guid, startCast, endCast, isChanneled, si.aura.player, spellcast)
	end
	self:StopProfiling("OvaleAura_ApplySpellAfterCast")
end

-- Apply the effects of the spell on the target's state after it lands on the target.
function OvaleAura:ApplySpellAfterHit(state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast)
	self:StartProfiling("OvaleAura_ApplySpellAfterHit")
	local si = OvaleData.spellInfo[spellId]
	-- Apply the auras on the target.
	if si and si.aura and si.aura.target then
		state:ApplySpellAuras(spellId, targetGUID, startCast, endCast, isChanneled, si.aura.target, spellcast)
	end
	self:StopProfiling("OvaleAura_ApplySpellAfterHit")
end
--</public-static-methods>

--<state-methods>
local function GetStateAura(state, guid, auraId, casterGUID)
	local aura = GetAura(state.aura, guid, auraId, casterGUID)
	if not aura or aura.serial < state.serial then
		aura = GetAura(OvaleAura.aura, guid, auraId, casterGUID)
	end
	return aura
end

local function GetStateAuraAnyCaster(state, guid, auraId)
	--[[
		Loop over all of the auras in the true aura database and the state machine aura
		database and find the one with the latest expiration time.
	--]]
	local auraFound
	if OvaleAura.aura[guid] and OvaleAura.aura[guid][auraId] then
		for casterGUID in pairs(OvaleAura.aura[guid][auraId]) do
			local aura = GetStateAura(state, guid, auraId, casterGUID)
			-- Skip over auras found in the state machine for now.
			if not aura.state and OvaleAura:IsActiveAura(aura, state.currentTime) then
				if not auraFound or auraFound.ending < aura.ending then
					auraFound = aura
				end
			end
		end
	end
	if state.aura[guid] and state.aura[guid][auraId] then
		for casterGUID, aura in pairs(state.aura[guid][auraId]) do
			if aura.stacks > 0 then
				if not auraFound or auraFound.ending < aura.ending then
					auraFound = aura
				end
			end
		end
	end
	return auraFound
end

local function GetStateDebuffType(state, guid, debuffType, filter, casterGUID)
	--[[
		Loop over all of the auras in the true aura database and the state machine aura
		database and find the one with the latest expiration time.
	--]]
	local auraFound
	if OvaleAura.aura[guid] then
		for auraId in pairs(OvaleAura.aura[guid]) do
			local aura = GetStateAura(state, guid, auraId, casterGUID)
			-- Skip over auras found in the state machine for now.
			if aura and not aura.state and OvaleAura:IsActiveAura(aura, state.currentTime) then
				if aura.debuffType == debuffType and aura.filter == filter then
					if not auraFound or auraFound.ending < aura.ending then
						auraFound = aura
					end
				end
			end
		end
	end
	if state.aura[guid] then
		for auraId, whoseTable in pairs(state.aura[guid]) do
			local aura = whoseTable[casterGUID]
			if aura and aura.stacks > 0 then
				if aura.debuffType == debuffType and aura.filter == filter then
					if not auraFound or auraFound.ending < aura.ending then
						auraFound = aura
					end
				end
			end
		end
	end
	return auraFound
end

local function GetStateDebuffTypeAnyCaster(state, guid, debuffType, filter)
	--[[
		Loop over all of the auras in the true aura database and the state machine aura
		database and find the one with the latest expiration time.
	--]]
	local auraFound
	if OvaleAura.aura[guid] then
		for auraId, whoseTable in pairs(OvaleAura.aura[guid]) do
			for casterGUID in pairs(whoseTable) do
				local aura = GetStateAura(state, guid, auraId, casterGUID)
				if aura and not aura.state and OvaleAura:IsActiveAura(aura, state.currentTime) then
					if aura.debuffType == debuffType and aura.filter == filter then
						if not auraFound or auraFound.ending < aura.ending then
							auraFound = aura
						end
					end
				end
			end
		end
	end
	if state.aura[guid] then
		for auraId, whoseTable in pairs(state.aura[guid]) do
			for casterGUID, aura in pairs(whoseTable) do
				if aura and not aura.state and aura.stacks > 0 then
					if aura.debuffType == debuffType and aura.filter == filter then
						if not auraFound or auraFound.ending < aura.ending then
							auraFound = aura
						end
					end
				end
			end
		end
	end
	return auraFound
end

local function GetStateAuraOnGUID(state, guid, auraId, filter, mine)
	local auraFound
	if DEBUFF_TYPES[auraId] then
		if mine then
			auraFound = GetStateDebuffType(state, guid, auraId, filter, self_guid)
		else
			auraFound = GetStateDebuffTypeAnyCaster(state, guid, auraId, filter)
		end
	else
		if mine then
			local aura = GetStateAura(state, guid, auraId, self_guid)
			if aura and aura.stacks > 0 then
				auraFound = aura
			end
		else
			auraFound = GetStateAuraAnyCaster(state, guid, auraId)
		end
	end
	return auraFound
end

-- Print the auras matching the filter on the unit in alphabetical order.
do
	local array = {}

	statePrototype.DebugUnitAuras = function(state, unitId, filter)
		wipe(array)
		local guid = OvaleGUID:GetGUID(unitId)
		if OvaleAura.aura[guid] then
			for auraId, whoseTable in pairs(OvaleAura.aura[guid]) do
				for casterGUID in pairs(whoseTable) do
					local aura = GetStateAura(state, guid, auraId, casterGUID)
					if state:IsActiveAura(aura) and aura.filter == filter and not aura.state then
						local name = aura.name or "Unknown spell"
						tinsert(array, name .. ": " .. auraId)
					end
				end
			end
		end
		if state.aura[guid] then
			for auraId, whoseTable in pairs(state.aura[guid]) do
				for casterGUID, aura in pairs(whoseTable) do
					if state:IsActiveAura(aura) and aura.filter == filter then
						local name = aura.name or "Unknown spell"
						tinsert(array, name .. ": " .. auraId)
					end
				end
			end
		end
		if next(array) then
			tsort(array)
			return tconcat(array, "\n")
		end
	end
end

statePrototype.IsActiveAura = function(state, aura, atTime)
	-- Default to checking if an aura is active at the end of the current spellcast
	-- in the simulator, or at the current time if no spell is being cast.
	if not atTime then
		if state.endCast and state.endCast > state.currentTime then
			atTime = state.endCast
		else
			atTime = state.currentTime
		end
	end
	local boolean = false
	if aura then
		if aura.state then
			if aura.serial == state.serial and aura.stacks > 0 and aura.gain <= atTime and atTime <= aura.ending then
				boolean = true
			elseif aura.consumed and IsWithinAuraLag(aura.ending, atTime) then
				boolean = true
			end
		else
			boolean = OvaleAura:IsActiveAura(aura, atTime)
		end
	end
	return boolean
end

statePrototype.ApplySpellAuras = function(state, spellId, guid, startCast, endCast, isChanneled, auraList, spellcast)
	OvaleAura:StartProfiling("OvaleAura_state_ApplySpellAuras")
	local unitId = OvaleGUID:GetUnitId(guid)
	for filter, filterInfo in pairs(auraList) do
		for auraId, spellData in pairs(filterInfo) do
			--[[
				For lists described by SpellAddBuff(), etc., use the following interpretation:
					auraId=count,N		N is number of stacks to be set
					auraId=extend,N		aura is extended by N seconds, no change to stacks
					auraId=refresh		aura is refreshed, no change to stacks
					auraId=refresh_keep_snapshot
										aura is refreshed and the snapshot is carried over from the previous aura.
					auraId=N, N > 0		N is duration if aura has no duration SpellInfo() [deprecated].
					auraId=N, N > 0		N is number of stacks added
					auraId=0			aura is removed
					auraId=N, N < 0		N is number of stacks of aura removed
			--]]
			local si = OvaleData.spellInfo[auraId]
			local duration = OvaleData:GetBaseDuration(auraId, spellcast)
			local stacks = 1

			local count
			local extend = 0
			local refresh = false
			local keepSnapshot = false

			local tokenIterator, value
			if strfind(spellData, ",") then
				-- Lexer for spellData as comma-separated values.
				tokenIterator = gmatch(spellData, "[^,]+")
				value = tokenIterator()
			else
				value = spellData
			end

			-- Set stacks and refresh based on spellData.
			if value == "refresh" then
				refresh = true
			elseif value == "refresh_keep_snapshot" then
				refresh = true
				keepSnapshot = true
			elseif value == "count" then
				local N = tokenIterator and tokenIterator() or nil
				if N then
					count = tonumber(N)
				else
					Ovale:OneTimeMessage("Warning: '%d=%s' has '%s' missing final stack count.", auraId, spellData, value)
				end
			elseif value == "extend" then
				local seconds = tokenIterator and tokenIterator() or nil
				if seconds then
					extend = tonumber(seconds)
				else
					Ovale:OneTimeMessage("Warning: '%d=%s' has '%s' missing duration.", auraId, spellData, value)
				end
			else
				value = tonumber(value)
				if value then
					stacks = value
					-- Deprecated after transition.
					if not (si and si.duration) and value > 0 then
						-- Aura doesn't have duration SpellInfo(), so treat spell data as duration.
						Ovale:OneTimeMessage("Warning: '%s=%d' is deprecated for spell ID %d; aura ID %s should have duration information.", auraId, value, spellId, auraId)
						duration = value
						stacks = 1
					end
				end
			end

			-- Verify any run-time requirements for this aura.
			local verified
			if tokenIterator then
				verified = state:CheckRequirements(spellId, tokenIterator, unitId)
			else
				verified = true
			end
			if verified then
				local auraFound = state:GetAuraByGUID(guid, auraId, filter, true)
				local atTime = isChanneled and startCast or endCast

				if state:IsActiveAura(auraFound, atTime) then
					local aura
					if auraFound.state then
						-- Re-use existing aura in the simulator.
						aura = auraFound
					else
						-- Add an aura in the simulator and copy the existing aura information over.
						aura = state:AddAuraToGUID(guid, auraId, auraFound.source, filter, 0, INFINITY)
						for k, v in pairs(auraFound) do
							aura[k] = v
						end
						if auraFound.snapshot then
							aura.snapshot = OvalePaperDoll:GetSnapshot(auraFound.snapshot)
						end
						-- Reset the aura age relative to the state of the simulator.
						aura.serial = state.serial
						state:Log("Aura %d is copied into simulator.", auraId)
						-- Information that needs to be set below: stacks, start, ending, duration, gain.
					end
					-- Adjust stacks to add/remove if count is present.
					if count and count > 0 then
						stacks = count - aura.stacks
					end
					-- Spell starts channeling before the aura expires, or spellcast ends before the aura expires.
					if refresh or extend > 0 or stacks > 0 then
						-- Adjust stack count.
						if refresh then
							state:Log("Aura %d is refreshed to %d stack(s).", auraId, aura.stacks)
						elseif extend > 0 then
							state:Log("Aura %d is extended by %f seconds, preserving %d stack(s).", auraId, extend, aura.stacks)
						else -- if stacks > 0 then
							local maxStacks = 1
							if si and (si.max_stacks or si.maxstacks) then
								maxStacks = si.max_stacks or si.maxstacks
							end
							aura.stacks = aura.stacks + stacks
							if aura.stacks > maxStacks then
								aura.stacks = maxStacks
							end
							state:Log("Aura %d gains %d stack(s) to %d because of spell %d.", auraId, stacks, aura.stacks, spellId)
						end
						-- Set start, ending, and duration for the aura.
						if extend > 0 then
							-- aura.start is preserved.
							aura.duration = aura.duration + extend
							aura.ending = aura.ending + extend
						else
							aura.start = atTime
							if aura.tick and aura.tick > 0 then
								-- This is a periodic aura, so add new duration to extend the aura up to 130% of the normal duration.
								local remainingDuration = aura.ending - atTime
								local extensionDuration = 0.3 * duration
								if remainingDuration < extensionDuration then
									-- Aura is extended by the normal duration.
									aura.duration = remainingDuration + duration
								else
									aura.duration = extensionDuration + duration
								end
							else
								aura.duration = duration
							end
							aura.ending = aura.start + aura.duration
						end
						aura.gain = atTime
						state:Log("Aura %d with duration %s now ending at %s", auraId, aura.duration, aura.ending)
						if keepSnapshot then
							state:Log("Aura %d keeping previous snapshot.", auraId)
						elseif spellcast then
							OvaleFuture:UpdateFromSpellcast(aura, spellcast)
						end
					elseif stacks == 0 or stacks < 0 then
						if stacks == 0 then
							aura.stacks = 0
						else -- if stacks < 0 then
							aura.stacks = aura.stacks + stacks
							if aura.stacks < 0 then
								aura.stacks = 0
							end
							state:Log("Aura %d loses %d stack(s) to %d because of spell %d.", auraId, -1 * stacks, aura.stacks, spellId)
						end
						-- An existing aura is losing stacks, so inherit start, duration, ending and gain information.
						if aura.stacks == 0 then
							state:Log("Aura %d is completely removed.", auraId)
							-- The aura is completely removed, so set ending to the time that the aura is removed.
							aura.ending = atTime
							aura.consumed = true
						end
					end
				else
					-- Aura is not on the target.
					if not refresh and stacks > 0 then
						-- Spellcast causes a new aura.
						state:Log("New aura %d at %f on %s", auraId, atTime, guid)
						-- Add an aura in the simulator and copy the existing aura information over.
						local aura = state:AddAuraToGUID(guid, auraId, self_guid, filter, 0, INFINITY)
						-- Information that needs to be set below: stacks, start, ending, duration, gain.
						aura.stacks = stacks
						-- Set start and duration for aura.
						aura.start = atTime
						aura.duration = duration
						-- If "tick" is set explicitly in SpellInfo, then this is a known periodic aura.
						if si and si.tick then
							aura.baseTick = si.tick
							local snapshot = spellcast and spellcast.snapshot
							aura.tick = OvaleData:GetTickLength(auraId, snapshot)
						end
						aura.ending = aura.start + aura.duration
						aura.gain = aura.start
						if spellcast then
							OvaleFuture:UpdateFromSpellcast(aura, spellcast)
						end
					end
				end
			else
				state:Log("Aura %d (%s) is not applied.", auraId, spellData)
			end
		end
	end
	OvaleAura:StopProfiling("OvaleAura_state_ApplySpellAuras")
end

statePrototype.GetAuraByGUID = function(state, guid, auraId, filter, mine)
	local auraFound
	if OvaleData.buffSpellList[auraId] then
		for id in pairs(OvaleData.buffSpellList[auraId]) do
			local aura = GetStateAuraOnGUID(state, guid, id, filter, mine)
			if aura and (not auraFound or auraFound.ending < aura.ending) then
				state:Log("Aura %s matching '%s' found on %s with (%s, %s)", id, auraId, guid, aura.start, aura.ending)
				auraFound = aura
			else
				--state:Log("Aura %s matching '%s' is missing on %s.", id, auraId, guid)
			end
		end
		if not auraFound then
			state:Log("Aura matching '%s' is missing on %s.", auraId, guid)
		end
	else
		auraFound = GetStateAuraOnGUID(state, guid, auraId, filter, mine)
		if auraFound then
			state:Log("Aura %s found on %s with (%s, %s)", auraId, guid, auraFound.start, auraFound.ending)
		else
			state:Log("Aura %s is missing on %s.", auraId, guid)
		end
	end
	return auraFound
end

statePrototype.GetAura = function(state, unitId, auraId, filter, mine)
	local guid = OvaleGUID:GetGUID(unitId)
	return state:GetAuraByGUID(guid, auraId, filter, mine)
end

-- Add a new aura to the unit specified by GUID.
statePrototype.AddAuraToGUID = function(state, guid, auraId, casterGUID, filter, start, ending, snapshot)
	local aura = self_pool:Get()
	aura.state = true
	aura.serial = state.serial
	aura.lastUpdated = state.currentTime
	aura.filter = filter
	aura.mine = (casterGUID == self_guid)
	aura.start = start or 0
	aura.ending = ending or INFINITY
	aura.duration = ending - start
	aura.gain = aura.start
	aura.stacks = 1
	if snapshot then
		aura.snapshot = OvalePaperDoll:GetSnapshot(snapshot)
	end
	PutAura(state.aura, guid, auraId, casterGUID, aura)
	return aura
end

-- Remove an aura from the unit specified by GUID.
statePrototype.RemoveAuraOnGUID = function(state, guid, auraId, filter, mine, atTime)
	local auraFound = state:GetAuraByGUID(guid, auraId, filter, mine)
	if state:IsActiveAura(auraFound, atTime) then
		local aura
		if auraFound.state then
			-- Re-use existing aura in the simulator.
			aura = auraFound
		else
			-- Add an aura in the simulator and copy the existing aura information over.
			aura = state:AddAuraToGUID(guid, auraId, auraFound.source, filter, 0, INFINITY)
			for k, v in pairs(auraFound) do
				aura[k] = v
			end
			if auraFound.snapshot then
				aura.snapshot = OvalePaperDoll:GetSnapshot(auraFound.snapshot)
			end
			-- Reset the aura age relative to the state of the simulator.
			aura.serial = state.serial
		end

		-- Expire the aura.
		aura.stacks = 0
		aura.ending = atTime
		aura.lastUpdated = atTime
	end
end

statePrototype.GetAuraWithProperty = function(state, unitId, propertyName, filter)
	local count = 0
	local guid = OvaleGUID:GetGUID(unitId)
	local start, ending = INFINITY, 0

	-- Loop through auras not kept in the simulator that match the criteria.
	if OvaleAura.aura[guid] then
		for auraId, whoseTable in pairs(OvaleAura.aura[guid]) do
			for casterGUID in pairs(whoseTable) do
				local aura = GetStateAura(state, guid, auraId, self_guid)
				if state:IsActiveAura(aura) and not aura.state then
					if aura[propertyName] and aura.filter == filter then
						count = count + 1
						start = (aura.gain < start) and aura.gain or start
						ending = (aura.ending > ending) and aura.ending or ending
					end
				end
			end
		end
	end
	-- Loop through auras in the simulator that match the criteria.
	if state.aura[guid] then
		for auraId, whoseTable in pairs(state.aura[guid]) do
			for casterGUID, aura in pairs(whoseTable) do
				if state:IsActiveAura(aura) then
					if aura[propertyName] and aura.filter == filter then
						count = count + 1
						start = (aura.gain < start) and aura.gain or start
						ending = (aura.ending > ending) and aura.ending or ending
					end
				end
			end
		end
	end

	if count > 0 then
		state:Log("Aura with '%s' property found on %s (count=%s, minStart=%s, maxEnding=%s).", propertyName, unitId, count, start, ending)
	else
		state:Log("Aura with '%s' property is missing on %s.", propertyName, unitId)
		start, ending = nil
	end
	return start, ending
end

do
	-- The total count of the matched aura.
	local count
	-- The total number of stacks of the matched aura.
	local stacks
	-- The start and ending times of the first aura to expire that will change the total count.
	local startChangeCount, endingChangeCount
	-- The time interval over which count > 0.
	local startFirst, endingLast

	local function CountMatchingActiveAura(state, aura)
		state:Log("Counting aura %s found on %s with (%s, %s)", aura.spellId, aura.guid, aura.start, aura.ending)
		count = count + 1
		stacks = stacks + aura.stacks
		if aura.ending < endingChangeCount then
			startChangeCount, endingChangeCount = aura.gain, aura.ending
		end
		if aura.gain < startFirst then
			startFirst = aura.gain
		end
		if aura.ending > endingLast then
			endingLast = aura.ending
		end
	end

	--[[
		Return the total count and stacks of the given aura across all units, the start/end times of
		the first aura to expire that will change the total count, and the time interval over which
		the count is more than 0.  If excludeUnitId is given, then that unit is excluded from the count.
	--]]
	statePrototype.AuraCount = function(state, auraId, filter, mine, minStacks, excludeUnitId)
		OvaleAura:StartProfiling("OvaleAura_state_AuraCount")
		-- Initialize.
		minStacks = minStacks or 1
		count = 0
		stacks = 0
		startChangeCount, endingChangeCount = INFINITY, INFINITY
		startFirst, endingLast = INFINITY, 0
		local excludeGUID = excludeUnitId and OvaleGUID:GetGUID(excludeUnitId) or nil

		-- Loop through auras not kept in the simulator that match the criteria.
		for guid, auraTable in pairs(OvaleAura.aura) do
			if guid ~= excludeGUID and auraTable[auraId] then
				if mine then
					local aura = GetStateAura(state, guid, auraId, self_guid)
					if state:IsActiveAura(aura) and aura.filter == filter and aura.stacks >= minStacks and not aura.state then
						CountMatchingActiveAura(state, aura)
					end
				else
					for casterGUID in pairs(auraTable[auraId]) do
						local aura = GetStateAura(state, guid, auraId, casterGUID)
						if state:IsActiveAura(aura) and aura.filter == filter and aura.stacks >= minStacks and not aura.state then
							CountMatchingActiveAura(state, aura)
						end
					end
				end
			end
		end
		-- Loop through auras in the simulator that match the criteria.
		for guid, auraTable in pairs(state.aura) do
			if guid ~= excludeGUID and auraTable[auraId] then
				if mine then
					local aura = auraTable[auraId][self_guid]
					if aura then
						if state:IsActiveAura(aura) and aura.filter == filter and aura.stacks >= minStacks then
							CountMatchingActiveAura(state, aura)
						end
					end
				else
					for casterGUID, aura in pairs(auraTable[auraId]) do
						if state:IsActiveAura(aura) and aura.filter == filter and aura.stacks >= minStacks then
							CountMatchingActiveAura(state, aura)
						end
					end
				end
			end
		end

		state:Log("AuraCount(%d) is %s, %s, %s, %s, %s, %s", auraId, count, stacks, startChangeCount, endingChangeCount, startFirst, endingLast)
		OvaleAura:StopProfiling("OvaleAura_state_AuraCount")
		return count, stacks, startChangeCount, endingChangeCount, startFirst, endingLast
	end
end

-- Mirrored methods.
statePrototype.RequireBuffHandler = OvaleAura.RequireBuffHandler
statePrototype.RequireStealthHandler = OvaleAura.RequireStealthHandler
statePrototype.RequireTargetHealthPercentHandler = OvaleAura.RequireTargetHealthPercentHandler
--</state-methods>