-------------------------------------------------------------------------------
-- Title: Mik's Scrolling Battle Text Triggers
-- Author: Mikord
-------------------------------------------------------------------------------

-- Create module and set its name.
local module = {}
local moduleName = "Triggers"
MikSBT[moduleName] = module


-------------------------------------------------------------------------------
-- Imports.
-------------------------------------------------------------------------------

-- Local references to various modules for faster access.
local MSBTProfiles = MikSBT.Profiles
local MSBTParser = MikSBT.Parser

-- Get local references to various functions for faster access.
local string_find = string.find
local string_gsub = string.gsub
local string_format = string.format
local string_gmatch = string.gmatch
local FormatLargeNumber = FormatLargeNumber
local Print = MikSBT.Print
local EraseTable = MikSBT.EraseTable
local DisplayEvent = MikSBT.Animations.DisplayEvent
local TestFlagsAny = MSBTParser.TestFlagsAny
local ShortenNumber = MikSBT.ShortenNumber
--local SeparateNumber = MikSBT.SeparateNumber

-- Local reference to various variables for faster access.
local REACTION_HOSTILE = MSBTParser.REACTION_HOSTILE
local unitMap = MSBTParser.unitMap
local classMap = MSBTParser.classMap

local MSBTGetSpellInfo = MikSBT.MSBTGetSpellInfo
local MSBTGetSpellCooldown = MikSBT.MSBTGetSpellCooldown

-------------------------------------------------------------------------------
-- Constants.
-------------------------------------------------------------------------------

-- Special flag to indicate the player.
local FLAG_YOU = 0xF0000000

-- Max group sizes.
local MAX_PARTY_MEMBERS = 5
local MAX_RAID_MEMBERS = 40


-------------------------------------------------------------------------------
-- Private variables.
-------------------------------------------------------------------------------

-- Prevent tainting global _.
local _

-- Holds dynamically created frame for receiving events.
local eventFrame

-- Holds the player's name, GUID, and class.
local playerName, playerGUID, playerClass

-- Events the triggers use.
local listenEvents = {}

-- Functions to handle combat log events and conditions.
local captureFuncs
local testFuncs
local eventConditionFuncs
local exceptionConditionFuncs

-- Holds triggers in a format optimized for searching.
local categorizedTriggers = {}
local triggerExceptions = {}
local parserEvent = {}
local lookupTable = {}

-- Information about triggers used for condition checking.
local lastPercentages = {}
local lastPowerTypes = {}
local firedTimes = {}
local triggersToFire = {}

-- Hold buffs and debuffs that should be suppressed since there is a trigger for them.
local triggerSuppressions = {}

-- Supported power types given a power token.
local powerTypes = {}


-------------------------------------------------------------------------------
-- Trigger utility functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Returns whether or not the passed spell name is unavailable.
-- ****************************************************************************
local function IsSkillUnavailable(skillName)
	-- Pass if there is no skill to check.
	if (not skillName or skillName == "") then return true end

	-- Pass if the skill isn't known.
	if (not MSBTGetSpellInfo(skillName)) then return true end

	-- Pass check if the skillName is cooling down (but ignore the global cooldown).
	local start, duration = MSBTGetSpellCooldown(skillName)
	if (start > 0 and duration > 1.5) then return true end
end


-- ****************************************************************************
-- Creates a map of test functions for supported test types.
-- ****************************************************************************
local function CreateTestFuncs()
	testFuncs = {
		eq = function(l, r) return l == r end,
		ne = function(l, r) return l ~= r end,
		like = function(l, r) return type(l)=="string" and type(r)=="string" and string_find(l, r) end,
		unlike = function(l, r) return type(l)=="string" and type(r)=="string" and not string_find(l, r) end,
		lt = function(l, r) return type(l)=="number" and type(r)=="number" and l < r end,
		gt = function(l, r) return type(l)=="number" and type(r)=="number" and l > r end,
	}
end


-- ****************************************************************************
-- Creates a map of capture functions for supported combat log events.
-- Also makes use of the ones already defined in the parser module.
-- ****************************************************************************
local function CreateCaptureFuncs()
	captureFuncs = {
		-- Leave out eventType because we really don't care about it for triggers.
		SPELL_AURA_BROKEN_SPELL = function (p, ...) p.skillID, p.skillName, p.skillSchool, p.extraSkillID, p.extraSkillName, p.extraSkillSchool, p.auraType = ... end,
		SPELL_AURA_REFRESH = function (p, ...) p.skillID, p.skillName, p.skillSchool, p.auraType = ... end,
		SPELL_CAST_SUCCESS = function (p, ...) p.skillID, p.skillName, p.skillSchool = ... end,
		SPELL_CAST_FAILED = function (p, ...) p.skillID, p.skillName, p.skillSchool, p.missType = ... end,
		SPELL_SUMMON = function (p, ...) p.skillID, p.skillName, p.skillSchool = ... end,
		SPELL_CREATE = function (p, ...) p.skillID, p.skillName, p.skillSchool = ... end,
		UNIT_DIED = function (p, ...) end,
		UNIT_DESTROYED = function (p, ...) end,
	}

	-- Make use of the parser module capture functions instead of redefining them.
	captureFuncs.__index = MSBTParser.captureFuncs
	setmetatable(captureFuncs, captureFuncs)
end


-- ****************************************************************************
-- Creates maps of functions for supported conditions.
-- ****************************************************************************
local function CreateConditionFuncs()
	-- Event conditions.
	eventConditionFuncs = {
		-- Source unit.
		sourceName = function (f, t, v) return f(t.sourceName, v) end,
		sourceAffiliation = function (f, t, v) if (v == FLAG_YOU) then return f(t.sourceUnit, "player") else return f(TestFlagsAny(t.sourceFlags, v), true) end end,
		sourceReaction = function (f, t, v) return f(TestFlagsAny(t.sourceFlags, v), true) end,
		sourceControl = function (f, t, v) return f(TestFlagsAny(t.sourceFlags, v), true) end,
		sourceUnitType = function (f, t, v) return f(TestFlagsAny(t.sourceFlags, v), true) end,	-- player, NPC, pet, guardian, object

		-- Recipient unit.
		recipientName = function (f, t, v) return f(t.recipientName, v) end,
		recipientAffiliation = function (f, t, v) if (v == FLAG_YOU) then return f(t.recipientUnit, "player") else return f(TestFlagsAny(t.recipientFlags, v), true) end end,
		recipientReaction = function (f, t, v) return f(TestFlagsAny(t.recipientFlags, v), true) end,
		recipientControl = function (f, t, v) return f(TestFlagsAny(t.recipientFlags, v), true) end,
		recipientUnitType = function (f, t, v) return f(TestFlagsAny(t.recipientFlags, v), true) end,

		-- Skill.
		skillID = function (f, t, v) return f(t.skillID, v) end,
		skillName = function (f, t, v) return f(t.skillName, v) end,
		skillSchool = function (f, t, v) return f(t.skillSchool, v) end,

		-- Extra skill.
		extraSkillID = function (f, t, v) return f(t.extraSkillID, v) end,
		extraSkillName = function (f, t, v) return f(t.extraSkillName, v) end,
		extraSkillSchool = function (f, t, v) return f(t.extraSkillSchool, v) end,

		-- Damage/heal.
		amount = function (f, t, v) return f(t.amount, v) end,
		overkillAmount = function (f, t, v) return f(t.overkillAmount, v) end,
		damageType = function (f, t, v) return f(t.damageType, v) end,
		resistAmount = function (f, t, v) return f(t.resistAmount, v) end,
		blockAmount = function (f, t, v) return f(t.blockAmount, v) end,
		absorbAmount = function (f, t, v) return f(t.absorbAmount, v) end,
		isCrit = function (f, t, v) return f(t.isCrit and true or false, v) end,
		isGlancing = function (f, t, v) return f(t.isGlancing and true or false, v) end,
		isCrushing = function (f, t, v) return f(t.isCrushing and true or false, v) end,

		-- Miss.
		missType = function (f, t, v) return f(t.missType, v) end,

		-- Environmental.
		hazardType = function (f, t, v) return f(t.hazardType, v) end,

		-- Power.
		powerType = function (f, t, v) return f(t.powerType, v) end,
		extraAmount = function (f, t, v) return f(t.extraAmount, v) end,

		-- Aura.
		auraType = function (f, t, v) return f(t.auraType, v) end,

		-- Health/power changes.
		threshold = function (f, t, v) if (type(v)=="number") then return f(t.currentPercentage, v/100) and not f(t.lastPercentage, v/100) end end,
		unitID = function (f, t, v) if ((v == "party" and string_find(t.unitID, "party%d+")) or (v == "raid" and (string_find(t.unitID, "raid%d+") or string_find(t.unitID, "party%d+")))) then v = t.unitID end return f(t.unitID, v) end,
		unitReaction = function (f, t, v) if (v == REACTION_HOSTILE) then return f(UnitIsFriend(t.unitID, "player"), false) else return f(UnitIsFriend(t.unitID, "player"), true) end end,
	}


	-- Exception conditions.
	exceptionConditionFuncs = {
		activeTalents = function (f, t, v) return f(GetActiveSpecGroup(), v) end,
		buffActive = function (f, t, v) return UnitBuff("player", v) and true or false end,
		buffInactive = function (f, t, v) return not UnitBuff("player", v) and true or false end,
		currentCP = function (f, t, v) return f(GetComboPoints("player"), v) end,
		currentPower = function (f, t, v) return f(UnitPower("player"), v) end,
		inCombat = function (f, t, v) return f(UnitAffectingCombat("player") == true and true or false, v) end,
		recentlyFired = function (f, t, v) return f(GetTime() - firedTimes[t], v) end,
		trivialTarget = function (f, t, v) return f(UnitIsTrivial("target") == true and true or false, v) end,
		unavailableSkill = function (f, t, v) return IsSkillUnavailable(v) and true or false end,
		warriorStance = function (f, t, v) if (playerClass == "WARRIOR") then return f(GetShapeshiftForm(true), v) end end,
		zoneName = function (f, t, v) return f(GetZoneText(), v) end,
		zoneType = function (f, t, v) local _, zoneType = IsInInstance() return f(zoneType, v) end,
	}
end


-- ****************************************************************************
-- Converts a string representation of a number, boolean, or nil to its
-- corresponding type.
-- ****************************************************************************
local function ConvertType(value)
	if (type(value) == "string") then
		if (value == "true") then return true end
		if (value == "false") then return false end
		if (tonumber(value)) then return tonumber(value) end
		if (value == "nil") then return nil end
	end

	return value
end


-- ****************************************************************************
-- Categorizes the passed trigger if it is not disabled and it applies to the
-- current player's class. Also tracks the events the trigger uses so the
-- only events that are received are those needed by the active triggers.
-- ****************************************************************************
local function CategorizeTrigger(triggerSettings)
	-- Don't register the trigger if it is disabled, not for the current class,
	-- or there aren't any main events.
	if (triggerSettings.disabled) then return end
	if (triggerSettings.classes and not string_find(triggerSettings.classes, playerClass, nil, 1)) then return end
	if (not triggerSettings.mainEvents) then return end

	-- Loop through the main events for the trigger.
	local eventConditions, conditions
	for mainEvent, conditionsString in string_gmatch(triggerSettings.mainEvents .. "&&", "(.-)%{(.-)%}&&") do
		-- Loop through the conditions for the event and populate the settings into a conditions table.
		conditions = {triggerSettings = triggerSettings}
		if (conditionsString and conditionsString ~= "") then
			for conditionEntry in string_gmatch(conditionsString .. ";;", "(.-);;") do
				conditions[#conditions+1] = ConvertType(conditionEntry)
			end
		end

		-- Check for special consolidated miss events.
		if (mainEvent == "GENERIC_MISSED") then
			listenEvents["COMBAT_LOG_EVENT_UNFILTERED"] = true

			-- Create a table to hold an array of the triggers for the main events if there isn't already one for it.
			if (not categorizedTriggers["SWING_MISSED"]) then categorizedTriggers["SWING_MISSED"] = {} end
			if (not categorizedTriggers["RANGE_MISSED"]) then categorizedTriggers["RANGE_MISSED"] = {} end
			if (not categorizedTriggers["SPELL_MISSED"]) then categorizedTriggers["SPELL_MISSED"] = {} end

			-- Add the conditions table categorized by main events.
			categorizedTriggers["SWING_MISSED"][#categorizedTriggers["SWING_MISSED"]+1] = conditions
			categorizedTriggers["RANGE_MISSED"][#categorizedTriggers["RANGE_MISSED"]+1] = conditions
			categorizedTriggers["SPELL_MISSED"][#categorizedTriggers["SPELL_MISSED"]+1] = conditions

		-- Consolidated damage.
		elseif (mainEvent == "GENERIC_DAMAGE") then
			listenEvents["COMBAT_LOG_EVENT_UNFILTERED"] = true

			-- Create a table to hold an array of the triggers for the main events if there isn't already one for it.
			if (not categorizedTriggers["SWING_DAMAGE"]) then categorizedTriggers["SWING_DAMAGE"] = {} end
			if (not categorizedTriggers["RANGE_DAMAGE"]) then categorizedTriggers["RANGE_DAMAGE"] = {} end
			if (not categorizedTriggers["SPELL_DAMAGE"]) then categorizedTriggers["SPELL_DAMAGE"] = {} end

			-- Add the conditions table categorized by main events.
			categorizedTriggers["SWING_DAMAGE"][#categorizedTriggers["SWING_DAMAGE"]+1] = conditions
			categorizedTriggers["RANGE_DAMAGE"][#categorizedTriggers["RANGE_DAMAGE"]+1] = conditions
			categorizedTriggers["SPELL_DAMAGE"][#categorizedTriggers["SPELL_DAMAGE"]+1] = conditions

		-- Consolidated aura application.
		elseif (mainEvent == "SPELL_AURA_APPLIED") then
			listenEvents["COMBAT_LOG_EVENT_UNFILTERED"] = true

			-- Create a table to hold an array of the triggers for the main events if there isn't already one for it.
			if (not categorizedTriggers["SPELL_AURA_APPLIED"]) then categorizedTriggers["SPELL_AURA_APPLIED"] = {} end
			if (not categorizedTriggers["SPELL_AURA_APPLIED_DOSE"]) then categorizedTriggers["SPELL_AURA_APPLIED_DOSE"] = {} end

			-- Add the conditions table categorized by main events.
			categorizedTriggers["SPELL_AURA_APPLIED"][#categorizedTriggers["SPELL_AURA_APPLIED"]+1] = conditions
			categorizedTriggers["SPELL_AURA_APPLIED_DOSE"][#categorizedTriggers["SPELL_AURA_APPLIED_DOSE"]+1] = conditions

			-- Add aura gains to the trigger suppression so the normal buff gain/fade events are ignored.
			local skillName, recipientAffiliation
			for x = 1, #conditions, 3 do
				if (conditions[x] == "skillName" and conditions[x+1] == "eq" and conditions[x+2]) then skillName = conditions[x+2] end
				if (conditions[x] == "recipientAffiliation" and conditions[x+1] == "eq" and conditions[x+2] == FLAG_YOU) then recipientAffiliation = FLAG_YOU end
				if (conditions[x] == "skillID" and conditions[x+1] == "eq" and conditions[x+2]) then skillName = MSBTGetSpellInfo(conditions[x+2]) or UNKNOWN end
			end

				if (skillName and recipientAffiliation) then triggerSuppressions[skillName] = true end

		-- Consolidated aura removal.
		elseif (mainEvent == "SPELL_AURA_REMOVED") then
			listenEvents["COMBAT_LOG_EVENT_UNFILTERED"] = true

			-- Create a table to hold an array of the triggers for the main events if there isn't already one for it.
			if (not categorizedTriggers["SPELL_AURA_REMOVED"]) then categorizedTriggers["SPELL_AURA_REMOVED"] = {} end
			if (not categorizedTriggers["SPELL_AURA_REMOVED_DOSE"]) then categorizedTriggers["SPELL_AURA_REMOVED_DOSE"] = {} end

			-- Add the conditions table categorized by main events.
			categorizedTriggers["SPELL_AURA_REMOVED"][#categorizedTriggers["SPELL_AURA_REMOVED"]+1] = conditions
			categorizedTriggers["SPELL_AURA_REMOVED_DOSE"][#categorizedTriggers["SPELL_AURA_REMOVED_DOSE"]+1] = conditions

		-- Other events.
		else
			-- Create a table to hold an array of the triggers for the main event if there isn't already one for it.
			if (not categorizedTriggers[mainEvent]) then categorizedTriggers[mainEvent] = {} end
			eventConditions = categorizedTriggers[mainEvent]

			-- Health events.
			if (mainEvent == "UNIT_HEALTH") then
				listenEvents[mainEvent] = true
				lastPercentages[mainEvent] = {}

				-- Categorize the change by used units for better performance. The unitID condition is required for
				-- health triggers.
				for x = 1, #conditions, 3 do
					if (conditions[x] == "unitID") then
						-- Expand the consolidated party unit id to individual ones.
						local conditionValue = conditions[x+2]
						if (conditionValue == "party") then
							for partyMember = 1, MAX_PARTY_MEMBERS do
								local unitID = "party" .. partyMember
								if (not eventConditions[unitID]) then eventConditions[unitID] = {} end
								eventConditions[unitID][#eventConditions[unitID]+1] = conditions
							end

						elseif (conditionValue == "raid") then
							for raidMember = 1, MAX_RAID_MEMBERS do
								local unitID = "raid" .. raidMember
								if (not eventConditions[unitID]) then eventConditions[unitID] = {} end
								eventConditions[unitID][#eventConditions[unitID]+1] = conditions
							end

						-- Specific unit.
						else
							if (not eventConditions[conditionValue]) then eventConditions[conditionValue] = {} end
							eventConditions[conditionValue][#eventConditions[conditionValue]+1] = conditions
						end
					end -- unitID?
				end -- Loop through conditions.

			-- Power events.
			elseif (mainEvent == "UNIT_POWER_UPDATE") then
				listenEvents[mainEvent] = true

				-- Detect power type. The powerType and unitID conditions are required for power triggers.
				local powerType
				for x = 1, #conditions, 3 do
					if (conditions[x] == "powerType") then powerType = conditions[x+2] break end
				end

				-- Ensure the power type is defined.
				if (powerType) then
					lastPercentages[powerType] = {}

					-- Categorize the change by used power types and units for better performance.
					-- The powerType and unitID conditions are required for power triggers.
					for x = 1, #conditions, 3 do
						if (conditions[x] == "unitID") then
							if (not eventConditions[powerType]) then eventConditions[powerType] = {} end
							local powerConditions = eventConditions[powerType]

							-- Expand the consolidated party unit id to individual ones.
							local conditionValue = conditions[x+2]
							if (conditionValue == "party") then
								for partyMember = 1, MAX_PARTY_MEMBERS do
									local unitID = "party" .. partyMember
									if (not powerConditions[unitID]) then powerConditions[unitID] = {} end
									powerConditions[unitID][#powerConditions[unitID]+1] = conditions
								end

							elseif (conditionValue == "raid") then
								for raidMember = 1, MAX_RAID_MEMBERS do
									local unitID = "raid" .. raidMember
									if (not powerConditions[unitID]) then powerConditions[unitID] = {} end
									powerConditions[unitID][#powerConditions[unitID]+1] = conditions
								end

							-- Specific unit.
							else

								if (not powerConditions[conditionValue]) then powerConditions[conditionValue] = {} end
								powerConditions[conditionValue][#powerConditions[conditionValue]+1] = conditions
							end
						end -- unitID?
					end -- Loop through conditions.
				end -- Power type?

			-- Skill cooldown events.
			elseif (mainEvent == "SKILL_COOLDOWN") then
				eventConditions[#eventConditions+1] = conditions
				MikSBT.Cooldowns.UpdateRegisteredEvents()

			-- Pet cooldown events.
			elseif (mainEvent == "PET_COOLDOWN") then
				eventConditions[#eventConditions+1] = conditions
				MikSBT.Cooldowns.UpdateRegisteredEvents()

			-- Item cooldown events.
			elseif (mainEvent == "ITEM_COOLDOWN") then
				eventConditions[#eventConditions+1] = conditions
				MikSBT.Cooldowns.UpdateRegisteredEvents()

			-- Combat log event.
			elseif (captureFuncs[mainEvent]) then
				listenEvents["COMBAT_LOG_EVENT_UNFILTERED"] = true
				eventConditions[#eventConditions+1] = conditions
			end
		end -- Specific events check.
	end -- Loop through conditions.


	-- Leave the function if there are no exceptions for the trigger.
	if (not triggerSettings.exceptions or triggerSettings.exceptions == "") then return end

	-- Loop through the conditions for the exceptions for the trigger.
	local exceptionConditions = {}
	for exceptionValue in string_gmatch(triggerSettings.exceptions .. ";;", "(.-);;") do
		exceptionConditions[#exceptionConditions+1] = ConvertType(exceptionValue)
	end

	-- Create an entry to track fired times for the trigger.
	for x = 1, #exceptionConditions, 3 do
		if (exceptionConditions[x] == "recentlyFired") then firedTimes[triggerSettings] = 0 end
	end

	-- Set the exceptions for the trigger.
	triggerExceptions[triggerSettings] = exceptionConditions
end


-- ****************************************************************************
-- Update the categorized triggers table that is used for optimized searching.
-- ****************************************************************************
local function UpdateTriggers()
	-- Unregister all of the events from the event frame.
	eventFrame:UnregisterAllEvents()

	-- Erase the listen events table.
	EraseTable(listenEvents)

	-- Loop through all of the categorized trigger arrays and erase them.
	for mainEvent in pairs(categorizedTriggers) do
		EraseTable(categorizedTriggers[mainEvent])
	end

	-- Update the registered cooldown event.
	MikSBT.Cooldowns.UpdateRegisteredEvents()

	-- Erase the trigger exceptions array.
	EraseTable(triggerExceptions)

	-- Categorize triggers from the current profile.
	local currentProfileTriggers = rawget(MSBTProfiles.currentProfile, "triggers")
	if (currentProfileTriggers) then
		for triggerKey, triggerSettings in pairs(currentProfileTriggers) do
			if (triggerSettings) then CategorizeTrigger(triggerSettings) end
		end
	end

	-- Categorize triggers available in the master profile that aren't in the current profile.
	for triggerKey, triggerSettings in pairs(MSBTProfiles.masterProfile.triggers) do
		if (not currentProfileTriggers or rawget(currentProfileTriggers, triggerKey) == nil) then
			CategorizeTrigger(triggerSettings)
		end
	end

	-- Register all of the events the triggers use.
	for event in pairs(listenEvents) do
		eventFrame:RegisterEvent(event)
	end
end


-- ****************************************************************************
-- Displays the passed trigger settings.
-- ****************************************************************************
local function DisplayTrigger(triggerSettings, sourceName, sourceClass, recipientName, recipientClass, skillName, extraSkillName, amount, effectTexture)
	-- Get a local reference to the current profile.
	local currentProfile = MSBTProfiles.currentProfile

	-- Get the trigger message and icon skill.
	local message = triggerSettings.message
	local iconSkill = triggerSettings.iconSkill

	-- Substitute source name.
	if (sourceName and string_find(message, "%n", 1, true)) then
		-- Strip realm from names.
		if (string_find(sourceName, "-", 1, true)) then sourceName = string_gsub(sourceName, "(.-)%-.*", "%1") end

		-- Color the name according to the class if there is one and it's enabled.
		if (sourceClass and not currentProfile.classColoringDisabled) then
			local classSettings = currentProfile[sourceClass]
			if (classSettings and not classSettings.disabled) then sourceName = string_format("|cFF%02x%02x%02x%s|r", classSettings.colorR * 255, classSettings.colorG * 255, classSettings.colorB * 255, sourceName) end
		end

		-- Substitute all %n event codes with the source name.
		message = string_gsub(message, "%%n", sourceName)
	end

	-- Substitute recipient name.
	if (recipientName and string_find(message, "%r", 1, true)) then
		-- Strip realm from names.
		if (string_find(recipientName, "-", 1, true)) then recipientName = string_gsub(recipientName, "(.-)%-.*", "%1") end

		-- Color the name according to the class if there is one and it's enabled.
		if (recipientClass and not currentProfile.classColoringDisabled) then
			local classSettings = currentProfile[recipientClass]
			if (classSettings and not classSettings.disabled) then recipientName = string_format("|cFF%02x%02x%02x%s|r", classSettings.colorR * 255, classSettings.colorG * 255, classSettings.colorB * 255, recipientName) end
		end

		-- Substitute all %r event codes with the recipient name.
		message = string_gsub(message, "%%r", recipientName)
	end

	-- Substitute skill name.
	if (skillName and string_find(message, "%s", 1, true)) then message = string_gsub(message, "%%s", skillName) end

	-- Substitute extra skill name.
	if (extraSkillName and string_find(message, "%e", 1, true)) then message = string_gsub(message, "%%e", extraSkillName) end

	-- Substitute amount.
	if (amount and string_find(message, "%a", 1, true)) then
		-- Shorten amount with SI suffixes or separate into digit groups depending on options.
		local formattedAmount = amount
		if (currentProfile.shortenNumbers) then
			formattedAmount = ShortenNumber(formattedAmount, currentProfile.shortenNumberPrecision)
		elseif (currentProfile.groupNumbers) then
			formattedAmount = FormatLargeNumber(formattedAmount)
		end
		message = string_gsub(message, "%%a", formattedAmount)
	end

	-- Override the texture if there is an icon skill for the trigger.
	if (iconSkill) then
		if (skillName and string_find(iconSkill, "%s", 1, true)) then iconSkill = string_gsub(iconSkill, "%%s", skillName) end
		if (extraSkillName and string_find(iconSkill, "%e", 1, true)) then iconSkill = string_gsub(iconSkill, "%%e", extraSkillName) end
		_, _, effectTexture = MSBTGetSpellInfo(iconSkill)
	end

	-- Display the trigger event.
	DisplayEvent(triggerSettings, message, effectTexture)
end


-------------------------------------------------------------------------------
-- Trigger handler functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Tests if any of the exceptions for the passed trigger settings are true.
-- ****************************************************************************
local function TestExceptions(triggerSettings)
	-- Trigger is not excluded if there are no exceptions.
	if (not triggerExceptions[triggerSettings]) then return end

	-- Loop through each exception triplet.
	local exceptionConditions = triggerExceptions[triggerSettings]
	for position = 1, #exceptionConditions, 3 do
		-- Test the exception and if it passes, don't waste time checking others.
		local conditionFunc = exceptionConditionFuncs[exceptionConditions[position]]
		local testFunc = testFuncs[exceptionConditions[position+1]]
		if (conditionFunc and testFunc and conditionFunc(testFunc, triggerSettings, exceptionConditions[position+2])) then return true end
	end -- Exceptions loop.

	-- Set the current time as the last time the trigger was fired if the the trigger
	-- has a recently fired exception.
	if (firedTimes[triggerSettings]) then firedTimes[triggerSettings] = GetTime() end
end


-- ****************************************************************************
-- Handles triggers for health and power events.
-- ****************************************************************************
local function HandleHealthAndPowerTriggers(unit, event, currentAmount, maxAmount, powerType)
	-- Ignore the event if there are no triggers to search for it.
	local eventTriggers = categorizedTriggers[event]
	if (powerType and eventTriggers) then eventTriggers = eventTriggers[powerType] end
	if (not eventTriggers or not eventTriggers[unit]) then return end

	-- Calculate current last percentages.
	local currentPercentage = currentAmount / maxAmount
	local lastEventPercentages = lastPercentages[powerType or event]
	local lastPercentage = lastEventPercentages[unit]

	-- Ignore thresholds on death.
	if (not lastPercentage) then lastEventPercentages[unit] = currentPercentage return end
	if (UnitIsDeadOrGhost(unit)) then lastEventPercentages[unit] = nil return end

	-- Populate the lookup table for conditions checking.
	lookupTable.amount = currentAmount
	lookupTable.currentPercentage = currentPercentage
	lookupTable.lastPercentage = lastPercentage
	lookupTable.unitID = unit
	lookupTable.powerType = powerType


	-- Erase the list of triggers to fire.
	for k in pairs(triggersToFire) do triggersToFire[k] = nil end

	-- Loop through the conditions list for the main event.
	for _, eventConditions in ipairs(eventTriggers[unit]) do
		-- Trigger fires by default.
		local doFire = true

		-- Don't bother checking conditions for a trigger that has already been fired.
		if (not triggersToFire[eventConditions.triggerSettings]) then
			-- Loop through each condition triplet.
			for position = 1, #eventConditions, 3 do
				-- Test the condition and if it fails, don't waste time checking other conditions.
				local conditionFunc = eventConditionFuncs[eventConditions[position]]
				local testFunc = testFuncs[eventConditions[position+1]]
				if (conditionFunc and testFunc and not conditionFunc(testFunc, lookupTable, eventConditions[position+2])) then doFire = false break end
			end -- Conditions loop.

			-- Set the trigger to be fired if none of the conditions failed.
			if (doFire) then triggersToFire[eventConditions.triggerSettings] = true end
		end
	end

	-- Get the texture for the event and display triggers that aren't excepted.
	if (next(triggersToFire)) then
		-- Display the fired triggers if none of the exceptions are true.
		local recipientName = UnitName(unit)
		local _, recipientClass = UnitClass(unit)
		local amount = currentAmount
		for triggerSettings in pairs(triggersToFire) do
			if (not TestExceptions(triggerSettings)) then DisplayTrigger(triggerSettings, nil, nil, recipientName, recipientClass, nil, nil, amount) end
		end
	end -- Triggers to fire?

	-- Update the last percentage for the unit.
	lastEventPercentages[unit] = currentPercentage
end


-- ****************************************************************************
-- Handles triggers for skill cooldowns.
-- ****************************************************************************
local function HandleCooldowns(cooldownType, cooldownID, cooldownName, effectTexture)
	-- Choose the correct cooldown event based on the cooldown type.
	local event = "SKILL_COOLDOWN"
	if (cooldownType == "pet") then
		event = "PET_COOLDOWN"
	elseif (cooldownType == "item") then
		event = "ITEM_COOLDOWN"
	end

	-- Ignore the event if there are no triggers to search for it.
	local eventTriggers = categorizedTriggers[event]
	if (not eventTriggers) then return end

	-- Populate the lookup table for conditions checking.
	if (cooldownType == "item") then
		lookupTable.itemID = cooldownID
		lookupTable.itemName = cooldownName
	else
		lookupTable.skillID = cooldownID
		lookupTable.skillName = cooldownName
	end

	-- Erase the list of triggers to fire.
	for k in pairs(triggersToFire) do triggersToFire[k] = nil end

	-- Loop through the conditions list for the main event.
	for _, eventConditions in ipairs(eventTriggers) do
		-- Trigger fires by default.
		local doFire = true

		-- Don't bother checking conditions for a trigger that has already been fired.
		if (not triggersToFire[eventConditions.triggerSettings]) then
			-- Loop through each condition triplet.
			for position = 1, #eventConditions, 3 do
				-- Test the condition and if it fails, don't waste time checking other conditions.
				local conditionFunc = eventConditionFuncs[eventConditions[position]]
				local testFunc = testFuncs[eventConditions[position+1]]
				if (conditionFunc and testFunc and not conditionFunc(testFunc, lookupTable, eventConditions[position+2])) then doFire = false break end
			end -- Conditions loop.

			-- Set the trigger to be fired if none of the conditions failed.
			if (doFire) then triggersToFire[eventConditions.triggerSettings] = true end
		end
	end

	-- Get the texture for the event and display triggers that aren't excepted.
	if (next(triggersToFire)) then
		-- Display the fired triggers if none of the exceptions are true.
		local recipientName = playerName
		for triggerSettings in pairs(triggersToFire) do
			if (not TestExceptions(triggerSettings)) then DisplayTrigger(triggerSettings, nil, nil, recipientName, playerClass, cooldownName, nil, nil, effectTexture) end
		end
	end -- Triggers to fire?
end


-- ****************************************************************************
-- Handles triggers for combat log events.
-- ****************************************************************************
local function HandleCombatLogTriggers(timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, recipientGUID, recipientName, recipientFlags, recipientRaidFlags, ...)
	-- Ignore the event if there are no triggers to search for it.
	if (not categorizedTriggers[event]) then return end

	-- Make sure the capture function for the event exists.
	local captureFunc = captureFuncs[event]
	if (not captureFunc) then return end


	-- Erase the parser event table.
	for k in pairs(parserEvent) do parserEvent[k] = nil end

	-- Populate fields that exist for all events.
	parserEvent.sourceGUID = sourceGUID
	parserEvent.sourceName = sourceName
	parserEvent.sourceFlags = sourceFlags
	parserEvent.recipientGUID = recipientGUID
	parserEvent.recipientName = recipientName
	parserEvent.recipientFlags = recipientFlags
	parserEvent.sourceUnit = unitMap[sourceGUID]
	parserEvent.recipientUnit = unitMap[recipientGUID]


	-- Map the local arguments into the parser event table.
	captureFunc(parserEvent, ...)


	-- Erase the list of triggers to fire.
	for k in pairs(triggersToFire) do triggersToFire[k] = nil end

	-- Loop through the conditions list for the main event.
	for _, eventConditions in ipairs(categorizedTriggers[event]) do
		-- Trigger fires by default.
		local doFire = true

		-- Don't bother checking conditions for a trigger that has already been fired.
		if (not triggersToFire[eventConditions.triggerSettings]) then
			-- Loop through each condition triplet.
			for position = 1, #eventConditions, 3 do
				-- Test the condition and if it fails, don't waste time checking other conditions.
				local conditionFunc = eventConditionFuncs[eventConditions[position]]
				local testFunc = testFuncs[eventConditions[position+1]]
				if (conditionFunc and testFunc and not conditionFunc(testFunc, parserEvent, eventConditions[position+2])) then doFire = false break end
			end -- Conditions loop.

			-- Set the trigger to be fired if none of the conditions failed.
			if (doFire) then triggersToFire[eventConditions.triggerSettings] = true end
		end
	end

	-- Get the texture for the event and display triggers that aren't excepted.
	if (next(triggersToFire)) then
		local effectTexture
		if (parserEvent.skillID or parserEvent.extraSkillID) then _, _, effectTexture = MSBTGetSpellInfo(parserEvent.extraSkillID or parserEvent.skillID) end

		-- Display the fired triggers if none of the exceptions are true.
		local sourceName = parserEvent.sourceName
		local recipientName = parserEvent.recipientName
		local sourceClass = classMap[sourceGUID]
		local recipientClass = classMap[recipientGUID]
		local skillName = parserEvent.skillName
		local extraSkillName = parserEvent.extraSkillName
		local amount = parserEvent.amount
		for triggerSettings in pairs(triggersToFire) do
			if (not TestExceptions(triggerSettings)) then DisplayTrigger(triggerSettings, sourceName, sourceClass, recipientName, recipientClass, skillName, extraSkillName, amount, effectTexture) end
		end
	end -- Triggers to fire?
end


-------------------------------------------------------------------------------
-- Initialization and event handlers.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Called when the registered events occur.
-- ****************************************************************************
local function OnEvent(this, event, arg1, arg2, ...)
	-- Health.
	if (event == "UNIT_HEALTH") then
		-- Ignore the event if there are no triggers to search for it.
		if (not categorizedTriggers[event] or not categorizedTriggers[event][arg1]) then return end
		HandleHealthAndPowerTriggers(arg1, event, UnitHealth(arg1), UnitHealthMax(arg1))

	-- Power.
	elseif (event == "UNIT_POWER_UPDATE") then
		-- Ignore the event if there are no triggers to search for it.
		if (not categorizedTriggers[event]) then return end
		local powerType = powerTypes[arg2]
		if (not powerType) then return end
		if (not categorizedTriggers[event][powerType] or not categorizedTriggers[event][powerType][arg1]) then return end
		HandleHealthAndPowerTriggers(arg1, event, UnitPower(arg1, powerType), UnitPowerMax(arg1, powerType), powerType)

	-- Combat log event.
	elseif (event == "COMBAT_LOG_EVENT_UNFILTERED") then
		HandleCombatLogTriggers(CombatLogGetCurrentEventInfo())

	end -- Event types.
end


-- ****************************************************************************
-- Enables the trigger parsing.
-- ****************************************************************************
local function Enable()
	-- Register events the triggers use.
	for event in pairs(listenEvents) do
		eventFrame:RegisterEvent(event)
	end
end


-- ****************************************************************************
-- Disables the trigger parsing.
-- ****************************************************************************
local function Disable()
	-- Unregister all of the events from the event frame.
	eventFrame:UnregisterAllEvents()
end


-------------------------------------------------------------------------------
-- Initialization.
-------------------------------------------------------------------------------

-- Get the player's name and class.
playerName = UnitName("player")
playerGUID = UnitGUID("player")
_, playerClass = UnitClass("player")

-- Create a frame to receive events.
eventFrame = CreateFrame("Frame")
eventFrame:Hide()
eventFrame:SetScript("OnEvent", OnEvent)

-- Create function maps.
CreateCaptureFuncs()
CreateTestFuncs()
CreateConditionFuncs()

-- Create the power types lookup map.
powerTypes["MANA"] = Enum.PowerType.Mana
powerTypes["RAGE"] = Enum.PowerType.Rage
powerTypes["FOCUS"] = Enum.PowerType.Focus
powerTypes["ENERGY"] = Enum.PowerType.Energy
powerTypes["COMBO_POINTS"] = Enum.PowerType.ComboPoints
powerTypes["RUNES"] = Enum.PowerType.Runes
powerTypes["RUNIC_POWER"] = Enum.PowerType.RunicPower
powerTypes["SOUL_SHARDS"] = Enum.PowerType.SoulShards
powerTypes["LUNAR_POWER"] = Enum.PowerType.LunarPower
powerTypes["HOLY_POWER"] = Enum.PowerType.HolyPower
powerTypes["ALTERNATE_POWER"] = Enum.PowerType.Alternate
powerTypes["MAELSTROM"] = Enum.PowerType.Maelstrom
powerTypes["CHI"] = Enum.PowerType.Chi
powerTypes["INSANITY"] = Enum.PowerType.Insanity
powerTypes["ARCANE_CHARGES"] = Enum.PowerType.ArcaneCharges
powerTypes["FURY"] = Enum.PowerType.Fury
powerTypes["PAIN"] = Enum.PowerType.Pain
powerTypes["ESSENCE"] = Enum.PowerType.Essence

-------------------------------------------------------------------------------
-- Module interface.
-------------------------------------------------------------------------------

-- Protected Variables.
module.triggerSuppressions		= triggerSuppressions
module.categorizedTriggers		= categorizedTriggers
module.powerTypes				= powerTypes

-- Protected Functions.
module.HandleCooldowns			= HandleCooldowns
module.HandleCombatLogTriggers	= HandleCombatLogTriggers
module.ConvertType				= ConvertType
module.UpdateTriggers			= UpdateTriggers
module.Enable					= Enable
module.Disable					= Disable
