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

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


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

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

-- Power types.
local POWERTYPE_MANA = 0;
local POWERTYPE_RAGE = 1;
local POWERTYPE_ENERGY = 3;


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

-- 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 = {};


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

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

local currentBuffs = MSBTParser.currentAuras.buffs;
local unitMap = MSBTParser.unitMap;

-- Get local references to certain functions for faster access.
local string_find = string.find;
local string_gsub = string.gsub;
local string_gmatch = string.gmatch;
local Print = MikSBT.Print;
local EraseTable = MikSBT.EraseTable;
local DisplayEvent = MikSBT.Animations.DisplayEvent;
local TestFlagsAny = MSBTParser.TestFlagsAny;

local REACTION_HOSTILE = MSBTParser.REACTION_HOSTILE;


-------------------------------------------------------------------------------
-- 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 GetSpellInfo(skillName)) then return true; end

 -- Pass check if the skillName is cooling down (but ignore the global cooldown).
 local start, duration = GetSpellCooldown(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_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,
  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"), nil) else return f(UnitIsFriend(t.unitID, "player"), 1); end end,
 };


 -- Exception conditions.
 exceptionConditionFuncs = {
  buffActive = function (f, t, v) return currentBuffs[v] and true or false; end,
  currentCP = function (f, t, v) return f(GetComboPoints(), v); end,
  currentPower = function (f, t, v) return f(UnitMana("player"), v); end,
  recentlyFired = function (f, t, v) return f(GetTime() - firedTimes[t], v); end,
  trivialTarget = function (f, t, v) return f(UnitIsTrivial("target") == 1 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
   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/Mana/Energy/Rage.
   if (mainEvent == "UNIT_HEALTH" or mainEvent == "UNIT_MANA" or mainEvent == "UNIT_ENERGY" or mainEvent == "UNIT_RAGE") then
    listenEvents[mainEvent] = true;
    lastPercentages[mainEvent] = {};

    -- Categorize the change by used unit types for better performance.  The unitID condition is required for
    -- health and power change triggers.
    for x = 1, #conditions, 3 do
     local conditionValue = conditions[x+2];

     -- Expand the consolidated party unit id to individual ones.
     if (conditions[x] == "unitID") then
      if (conditionValue == "party") then
       local unitID = "party" .. x;
       if (not eventConditions[unitID]) then eventConditions[unitID] = {}; end
       eventConditions[unitID][#eventConditions[unitID]+1] = conditions;   

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

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

   -- 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 enable state of cooldowns.
 MikSBT.Cooldowns.UpdateEnableState();

 -- 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, recipientName, skillName, extraSkillName, amount, effectTexture)
 -- Get the trigger message and icon skill.
 local message = triggerSettings.message;
 local iconSkill = triggerSettings.iconSkill;
 
 -- Substitute the codes if there are any. 
 if (sourceName) then message = string_gsub(message, "%%n", sourceName); end
 if (recipientName) then message = string_gsub(message, "%%r", recipientName); end
 if (skillName) then 
  message = string_gsub(message, "%%s", skillName);
  if (iconSkill) then iconSkill = string_gsub(iconSkill, "%%s", skillName); end
 end
 if (extraSkillName) then 
  message = string_gsub(message, "%%e", extraSkillName);
  if (iconSkill) then iconSkill = string_gsub(iconSkill, "%%e", extraSkillName); end
 end 
 if (amount) then message = string_gsub(message, "%%a", amount); end

 -- Override the texture if there is an icon skill for the trigger.
 if (iconSkill) then _, _, effectTexture = GetSpellInfo(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.
 if (not categorizedTriggers[event] or not categorizedTriggers[event][unit]) then return; end

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

 -- Ignore thresholds on death and power type changes. 
 if (not lastPercentage) then lastEventPercentages[unit] = currentPercentage; return; end
 if (powerType and powerType ~= UnitPowerType(unit) or 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;


 -- 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][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 amount = currentAmount;
  for triggerSettings in pairs(triggersToFire) do
   if (not TestExceptions(triggerSettings)) then DisplayTrigger(triggerSettings, nil, recipientName, 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(skillName, effectTexture)
 -- Ignore the event if there are no triggers to search for it.
 if (not categorizedTriggers["SKILL_COOLDOWN"]) then return; end
 
 -- Populate the lookup table for conditions checking.
 lookupTable.skillName = skillName;

 -- 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["SKILL_COOLDOWN"]) 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, recipientName, skillName, nil, nil, effectTexture); end
  end
 end -- Triggers to fire?
end


-- ****************************************************************************
-- Handles triggers for combat log events.
-- ****************************************************************************
local function HandleCombatLogTriggers(timestamp, event, sourceGUID, sourceName, sourceFlags, recipientGUID, recipientName, recipientFlags, ...)
 -- 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 = GetSpellInfo(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 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, recipientName, 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, ...)
 -- Health.
 if (event == "UNIT_HEALTH") then
  HandleHealthAndPowerTriggers(arg1, event, UnitHealth(arg1), UnitHealthMax(arg1));

 -- Mana.
 elseif (event == "UNIT_MANA") then
  HandleHealthAndPowerTriggers(arg1, event, UnitMana(arg1), UnitManaMax(arg1), POWERTYPE_MANA);

 -- Energy.
 elseif (event == "UNIT_ENERGY") then
  HandleHealthAndPowerTriggers(arg1, event, UnitMana(arg1), UnitManaMax(arg1), POWERTYPE_ENERGY);

 -- Rage.
 elseif (event == "UNIT_RAGE") then
  HandleHealthAndPowerTriggers(arg1, event, UnitMana(arg1), UnitManaMax(arg1), POWERTYPE_RAGE);

 -- Combat log event.
 elseif (event == "COMBAT_LOG_EVENT_UNFILTERED") then
  HandleCombatLogTriggers(arg1, ...);

 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


-- ****************************************************************************
-- Called when the module is loaded.
-- ****************************************************************************
local function OnLoad()
 -- 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();
end




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

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

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


-------------------------------------------------------------------------------
-- Load.
-------------------------------------------------------------------------------

OnLoad();