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

-------------------------------------------------------------------------------------
-- Public Constants.
-------------------------------------------------------------------------------------

-- Display types for incoming, outgoing, and notification events.
MikSBT.DISPLAYTYPE_INCOMING			= 1;
MikSBT.DISPLAYTYPE_OUTGOING			= 2;
MikSBT.DISPLAYTYPE_NOTIFICATION		= 3;


-------------------------------------------------------------------------------------
-- Private constants.
-------------------------------------------------------------------------------------

-- How long to wait before showing events so that merges may happen.
local MERGE_DELAY_TIME = 0.3;

-- The overheal trailer to use.
local OVERHEAL_TRAILER = " <%d>";


-- The amount of time to delay between updating an animating object.
local ANIMATION_DELAY = 0.015;

-- Max number of combo points.
local MAX_COMBO_POINTS = 5;

-- Merge Categories.
local MERGE_CATEGORY_INCOMING = 1;
local MERGE_CATEGORY_OUTGOING = 2;

-- Amount of time to wait between updates.
local UPDATE_INTERVAL = 1;

-- Left, Center, Right Text Aligns.
local TEXT_ALIGN_MAP = {"BOTTOMLEFT", "BOTTOM", "BOTTOMRIGHT"};

-- None, Thin, Thick
local OUTLINE_MAP = {"", "OUTLINE", "THICKOUTLINE"};

-- Path to look for sound files.
local SOUND_PATH = "Interface\\AddOns\\MikScrollingBattleText\\Sounds\\";


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

-- Holds dynamically created frames for receiving events.
local eventFrame;
local animationFrame;

-- Holds the player's name.
local playerName;

-- Holds arrays of unmerged events, merged events to be displayed and the time since
-- the last merge for incoming and outgoing events.
local combatMergeData;

-- Holds whether or not a merge is still being processed.
local stillMerging = false;

-- Accumulates time between updates.
local timeSinceLastUpdate = 0;

-- Hold spells that should be recharacterized.
local recharacterizeMap;

-- Hold HoT and DoT abilities that are throttled.
local throttleData;

-- Holds a pool of dynamically created font strings.
local fontStringCache;

-- Holds information about animating display events.
local animationData;

-- Holds information about the registered animation styles.
local registeredAnimStyles = {};
local registeredStickyAnimStyles = {};

-- Holds information about registered fonts.
local registeredFonts = {};


-------------------------------------------------------------------------------------
-- Printing functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Prints out the passed message to the default chat frame.
-- **********************************************************************************
function MikSBT.Print(msg, r, g, b)
 -- Add the message to the default chat frame.
 DEFAULT_CHAT_FRAME:AddMessage("MSBT: " .. tostring(msg), r, g, b);
end


-------------------------------------------------------------------------------------
-- Core event handlers.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Called when the mod is loaded.
-- **********************************************************************************
function MikSBT.OnLoad()
 -- Create a frame to receive events.
 eventFrame = CreateFrame("FRAME");
 eventFrame:Hide();
 eventFrame:SetScript("OnEvent", MikSBT.OnEvent);
 eventFrame:SetScript("OnUpdate", MikSBT.OnUpdate);

 -- Create a frame for receiving animation updates.
 animationFrame = CreateFrame("FRAME", "MSBTAnimationFrame", UIParent);
 animationFrame:SetFrameStrata("HIGH");
 animationFrame:SetPoint("BOTTOM", UIParent, "CENTER");
 animationFrame:SetWidth(1);
 animationFrame:SetHeight(1);
 animationFrame:Hide();
 animationFrame:SetScript("OnUpdate", MikSBT.OnUpdateAnimationFrame);

 -- Register for the ADDON_LOADED and VARIABLES_LOADED event.
 eventFrame:RegisterEvent("ADDON_LOADED");
 eventFrame:RegisterEvent("VARIABLES_LOADED");
end


-- **********************************************************************************
-- Called when the events we registered for occur.
-- **********************************************************************************
function MikSBT.OnEvent()
 -- When an addon is loaded.
 if (event == "ADDON_LOADED") then
  -- Make sure it's this addon.
  if (arg1 == MikSBT.MOD_NAME) then

   -- Don't get notification for other addons being loaded.
   this:UnregisterEvent("ADDON_LOADED");

   -- Initialize the mod.
   MikSBT.Init();
  end

 -- Variables for all addons loaded.
 elseif (event == "VARIABLES_LOADED") then
  -- Enable or disable blizzard's combat text based on if the mod is disabled or not.
  -- Must do it once the variables are loaded because blizzard's code seems to override it
  -- after the ADDON_LOADED code runs.
  MSBTProfiles.DisableBlizzardCombatText(not MSBTProfiles.GetSavedVariables().UserDisabled)

 end
end


-- **********************************************************************************
-- Called when the core events frame is updated.
-- **********************************************************************************
function MikSBT.OnUpdate()
 -- Loop through throttle data.
 for mergeCategory, throttledAbilities in pairs(throttleData) do
  for _, throttledAbility in pairs(throttledAbilities) do

   -- Check if the ability is currently being throttled.
   if (throttledAbility.IsThrottled) then
    -- Add the throttled combat events to the correct merge category for potential merging if
    -- the ability has not been shown within the last few seconds set by the throttle time.
    if (GetTime() - throttledAbility.TimeLastShown >= MSBTProfiles.CurrentProfile.ThrottleTime) then

     -- Loop through all of the throttled events for the ability and add them to the correct
     -- merge category for potential merging then remove them from the throttled events list.
     local numEvents = table.getn(throttledAbility.ThrottledEvents);
     for x = numEvents, 1, -1 do
      table.insert(combatMergeData[mergeCategory].UnmergedEvents, table.remove(throttledAbility.ThrottledEvents));
     end

     -- Set the ability as no longer throttled and reset the time last shown.
     throttledAbility.IsThrottled = false;
     throttledAbility.TimeLastShown = GetTime();
    end -- Throttle time passed.
   end -- Ability not shown within the last few seconds.
  end -- Loop through abilities.
 end


 -- Loop through all of the merge data tables.
 for _, mergeData in pairs(combatMergeData) do
  -- Add the elapsed time since the last merge.
  mergeData.TimeSinceMerge = mergeData.TimeSinceMerge + arg1;

  -- Once enough time has passed and all previous merges are done.
  if ((mergeData.TimeSinceMerge >= MERGE_DELAY_TIME) and not stillMerging) then
   -- Set the still merging flag.
   stillMerging = true;

   -- Merge like events.
   MikSBT.MergeEvents(mergeData, #mergeData.UnmergedEvents);

   -- Add the merged events to the animation system.
   for i, combatEvent in ipairs(mergeData.MergedEvents) do
    MikSBT.AddAnimation(combatEvent);
   end

   -- Clear the merged events.
   for key in pairs(mergeData.MergedEvents) do
    mergeData.MergedEvents[key] = nil;
   end

   -- Reset the last merged time.
   mergeData.TimeSinceMerge = 0;

   -- Clear the still merging flag.
   stillMerging = false;
  end
 end
end


-- **********************************************************************************
-- Called when the mod's animation frame is updated.
-- **********************************************************************************
function MikSBT.OnUpdateAnimationFrame()
 -- Flag for whether or not all the animations are inactive.
 local allInactive = true;

 -- Variables to hold display event info.
 local numEvents, displayEvent;

 -- Loop through all of the animation arrays in the scroll areas.
 for _, animationArray in pairs(animationData) do
  -- Loop through all of the display events in the animation array.
  for _, displayEvents in pairs(animationArray) do
   numEvents = table.getn(displayEvents);

   -- Loop through all the display events for the scroll area.
   for i = 1, numEvents do
    displayEvent = displayEvents[i];
    displayEvent.ElapsedTime = displayEvent.ElapsedTime + arg1;
    displayEvent.TimeSinceLastUpdate = displayEvent.TimeSinceLastUpdate + arg1;

    -- Check if enough time has passed.
    if (displayEvent.TimeSinceLastUpdate >= ANIMATION_DELAY) then
     -- Call the correct animation function for the display event and check if the animation is complete.
     if (displayEvent.AnimationFunction(displayEvent)) then
      -- Hide the font string.
      displayEvent.FontString:SetAlpha(0);

      -- Cache the font string object.
      MikSBT.ReclaimFontString(displayEvent.FontString);

      -- Set the animation complete flag.
      displayEvent.AnimationComplete = true;
     else
      -- Move the font string and set its alpha.
      displayEvent.FontString:SetPoint(displayEvent.AnchorPoint, displayEvent.OffsetX + displayEvent.PositionX, displayEvent.OffsetY + displayEvent.PositionY);
      displayEvent.FontString:SetAlpha(displayEvent.Alpha);
     end

     -- Reset the last updated time.
     displayEvent.TimeSinceLastUpdate = 0;
    end

    -- Clear the all inactive flag
    allInactive = false;
   end

   -- Loop backwards through all the display events for the animation array and remove the ones
   -- that are complete.
   for i = numEvents, 1, -1 do
    displayEvent = displayEvents[i];
    if (displayEvent.AnimationComplete) then
     MSBTTableRecycler.ReclaimTable(displayEvent);
     table.remove(displayEvents, i);
    end
   end
  end
 end


 -- Check if none of the animations are active and the animation frame is visible.  If so, hide it so
 -- the OnUpdate events stop firing.  This is done to keep the number of OnUpdate events down to a minimum for
 -- better performance.
 if (allInactive and this:IsVisible()) then
  this:Hide();
 end
end


-- **********************************************************************************
-- Called when the mod is fully loaded (addon variables loaded).
-- **********************************************************************************
function MikSBT.Init()
 -- Register slash commands
 SLASH_MSBT1 = MikSBT.COMMAND;
 SlashCmdList["MSBT"] = MikSBT.CommandHandler;

 -- Get the name of the player.
 playerName = UnitName("player");

 -- Create table structure for merging events.
 combatMergeData = {
  [MERGE_CATEGORY_INCOMING] = {TimeSinceMerge = 0, UnmergedEvents={}, MergedEvents={}},
  [MERGE_CATEGORY_OUTGOING] = {TimeSinceMerge = 0, UnmergedEvents={}, MergedEvents={}},
 };

 -- Create tables to hold HoT and DoT abilities that are throttled.
 throttleData = {
  [MERGE_CATEGORY_INCOMING] = {},
  [MERGE_CATEGORY_OUTGOING] = {},
 };

 -- Create the recharacterize map.
 MikSBT.CreateRecharacterizeMap();

 -- Create a table to hold cached font strings.
 fontStringCache = {};

 -- Create a table structure to hold information about animating display events.
 animationData = {};


 -- Initialize the saved variables to make sure there is a profile to work with.
 MSBTProfiles.InitSavedVariables();

 -- Disable or enable the mod depending on the saved setting.
 MSBTProfiles.SetOptionUserDisabled(MSBTProfiles.GetSavedVariables().UserDisabled);


 -- Start receiving updates for the main event frame.
 eventFrame:Show();
end


-- **********************************************************************************
-- Handle parser events.
-- **********************************************************************************
function MikSBT.ParserEventsHandler(parserEvent)
 -- Hold the event type string, merge category, affected unit name, and effect name.
 local eventTypeString = "";
 local mergeCategory, affectedUnitName, effectName;

 -- Hold whether the event is a DoT or HoT.
 local isDoT, isHoT;

 -- Check if the event is incoming, outgoing from the player, or outgoing from the player's pet and
 -- set the appropriate merge category.
 if (parserEvent.RecipientName == playerName) then
  mergeCategory = MERGE_CATEGORY_INCOMING;
  affectedUnitName = parserEvent.SourceName;
  eventTypeString = "INCOMING_";
 elseif (parserEvent.SourceName == playerName) then
  mergeCategory = MERGE_CATEGORY_OUTGOING;
  affectedUnitName = parserEvent.RecipientName;
  eventTypeString = "OUTGOING_";
 else
  mergeCategory = MERGE_CATEGORY_OUTGOING;
  affectedUnitName = parserEvent.RecipientName;
  eventTypeString = "PET_";
 end


 -- Check if it's a damage event.
 if (parserEvent.EventType == MSBTParser.EVENTTYPE_DAMAGE) then
  -- Append the spell prefix if there is an ability name.
  if (parserEvent.AbilityName) then
   eventTypeString = eventTypeString .. "SPELL_";
   effectName = parserEvent.AbilityName;
  end

  -- Append the correct damage event type for whether or not it's a DoT.
  isDoT = parserEvent.IsDoT or recharacterizeMap.DoTs[parserEvent.AbilityName];
  if (isDoT) then
   eventTypeString = eventTypeString .. "DOT";
  else
   eventTypeString = eventTypeString .. "DAMAGE";
  end


 -- Miss.
 elseif (parserEvent.EventType == MSBTParser.EVENTTYPE_MISS) then
  -- Append the spell prefix if there is an ability name.
  if (parserEvent.AbilityName) then
   eventTypeString = eventTypeString .. "SPELL_";
   effectName = parserEvent.AbilityName;
  end

  -- Append the correct miss type.
  if (parserEvent.MissType == MSBTParser.MISSTYPE_MISS) then
   eventTypeString = eventTypeString .. "MISS";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_DODGE) then
   eventTypeString = eventTypeString .. "DODGE";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_PARRY) then
   eventTypeString = eventTypeString .. "PARRY";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_BLOCK) then
   eventTypeString = eventTypeString .. "BLOCK";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_RESIST) then
   eventTypeString = eventTypeString .. "RESIST";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_ABSORB) then
   eventTypeString = eventTypeString .. "ABSORB";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_IMMUNE) then
   eventTypeString = eventTypeString .. "IMMUNE";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_EVADE) then
   eventTypeString = eventTypeString .. "EVADE";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_REFLECT) then
   eventTypeString = eventTypeString .. "REFLECT";
  elseif (parserEvent.MissType == MSBTParser.MISSTYPE_INTERRUPT) then
   eventTypeString = eventTypeString .. "INTERRUPT";
  end


 -- Heal.
 elseif (parserEvent.EventType == MSBTParser.EVENTTYPE_HEAL) then
  -- Set the effect name.
  effectName = parserEvent.AbilityName;

  -- Append the correct heal event type for whether or not it's a HoT.
  isHoT = parserEvent.IsHoT or recharacterizeMap.HoTs[parserEvent.AbilityName];
  if (isHoT) then
   eventTypeString = eventTypeString .. "HOT";
  else
   eventTypeString = eventTypeString .. "HEAL";
  end


 -- Notification.
 elseif (parserEvent.EventType == MSBTParser.EVENTTYPE_NOTIFICATION  and parserEvent.RecipientName == playerName) then
  mergeCategory = nil;
  eventTypeString = "NOTIFICATION_";

  -- Set the effect name.
  effectName = parserEvent.EffectName;


  -- Append the correct notification type.
  if (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_DEBUFF) then
   eventTypeString = eventTypeString .. "DEBUFF";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_BUFF) then
   eventTypeString = eventTypeString .. "BUFF";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_ITEM_BUFF) then
   eventTypeString = eventTypeString .. "ITEM_BUFF";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_DEBUFF_FADE) then
   eventTypeString = eventTypeString .. "DEBUFF_FADE";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_BUFF_FADE) then
   eventTypeString = eventTypeString .. "BUFF_FADE";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_ITEM_BUFF_FADE) then
   eventTypeString = eventTypeString .. "ITEM_BUFF_FADE";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_COMBAT_ENTER) then
   eventTypeString = eventTypeString .. "COMBAT_ENTER";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_COMBAT_LEAVE) then
   eventTypeString = eventTypeString .. "COMBAT_LEAVE";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_POWER_GAIN) then
   eventTypeString = eventTypeString .. "POWER_GAIN";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_POWER_LOSS) then
   eventTypeString = eventTypeString .. "POWER_LOSS";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_CP_GAIN) then
   -- Check if we are at max combo points.
   if (parserEvent.Amount == MAX_COMBO_POINTS) then
    eventTypeString = eventTypeString .. "CP_FULL";
   else
    eventTypeString = eventTypeString .. "CP_GAIN";
   end
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_HONOR_GAIN) then
   eventTypeString = eventTypeString .. "HONOR_GAIN";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_REP_GAIN) then
   eventTypeString = eventTypeString .. "REP_GAIN";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_REP_LOSS) then
   eventTypeString = eventTypeString .. "REP_LOSS";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_SKILL_GAIN) then
   eventTypeString = eventTypeString .. "SKILL_GAIN";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_EXPERIENCE_GAIN) then
   eventTypeString = eventTypeString .. "EXPERIENCE_GAIN";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_PC_KILLING_BLOW) then
   eventTypeString = eventTypeString .. "PC_KILLING_BLOW";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_NPC_KILLING_BLOW) then
   eventTypeString = eventTypeString .. "NPC_KILLING_BLOW";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_SOUL_SHARD_CREATED) then
   eventTypeString = eventTypeString .. "SOUL_SHARD_CREATED";
  elseif (parserEvent.NotificationType == MSBTParser.NOTIFICATIONTYPE_EXTRA_ATTACK) then
   eventTypeString = eventTypeString .. "EXTRA_ATTACK";
  end


 -- Environmental.
 elseif (parserEvent.EventType == MSBTParser.EVENTTYPE_ENVIRONMENTAL) then
  eventTypeString = "INCOMING_ENVIRONMENTAL";

  -- Set the correct effect name.
  if (parserEvent.HazardType == MSBTParser.HAZARDTYPE_DROWNING) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_DROWNING;
  elseif  (parserEvent.HazardType == MSBTParser.HAZARDTYPE_FALLING) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_FALLING;
  elseif  (parserEvent.HazardType == MSBTParser.HAZARDTYPE_FATIGUE) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_FATIGUE;
  elseif  (parserEvent.HazardType == MSBTParser.HAZARDTYPE_FIRE) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_FIRE;
  elseif  (parserEvent.HazardType == MSBTParser.HAZARDTYPE_LAVA) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_LAVA;
  elseif  (parserEvent.HazardType == MSBTParser.HAZARDTYPE_SLIME) then
   effectName = MSBTLocale.MSG_ENVIRONMENTAL_SLIME;
  end
 end


 -- Check if there is profile data for the event type.
 local eventSettings = MSBTProfiles.CurrentProfile.EventSettings[eventTypeString];
 if (eventSettings) then

  -- Get the scroll area settings and check if the event and its scroll area is enabled.
  local scrollAreaSettings = MSBTProfiles.CurrentProfile.ScrollAreaSettings[eventSettings.ScrollAreaIndex or MSBTDefaults.EventSettings.ScrollAreaIndex];
  if (not eventSettings.Disabled and not scrollAreaSettings.Disabled) then
   -- Get a recycled table from the table recycler.
   local combatEvent = MSBTTableRecycler.AcquireTable();

   -- Setup the combat event.
   combatEvent.EventType = eventTypeString;
   combatEvent.EventSettings = MSBTProfiles.CurrentProfile.EventSettings[eventTypeString];
   combatEvent.ScrollAreaSettings = scrollAreaSettings;
   combatEvent.UnformattedText = combatEvent.EventSettings.Message;
   combatEvent.IsCrit = parserEvent.IsCrit;
   combatEvent.Amount = parserEvent.Amount;
   combatEvent.EffectName = effectName;
   combatEvent.Name = affectedUnitName;
   combatEvent.DamageType = parserEvent.DamageType;
   combatEvent.PartialEffectText = MikSBT.GetFormattedPartialEffectText(parserEvent);
   combatEvent.OverhealAmount = parserEvent.OverhealAmount;

   -- Throttle DoTs and HoTs, if enabled.
   if (not MSBTProfiles.CurrentProfile.ThrottlingDisabled and (isDoT or isHoT) and parserEvent.AbilityName) then

    -- Create a table for the throttled ability if there is not one for it.
    local throttledAbility = throttleData[mergeCategory][parserEvent.AbilityName];
    if (not throttledAbility) then
     throttledAbility = {};
     throttledAbility.TimeLastShown = 0;
     throttledAbility.ThrottledEvents = {};
     throttleData[mergeCategory][parserEvent.AbilityName] = throttledAbility;
    end

    -- Add the event to the throttle list and set the throttling flag for the ability.
    table.insert(throttledAbility.ThrottledEvents, combatEvent);
    throttledAbility.IsThrottled = true;
    return;
   end

 
   -- Add the event to the correct merge category for potential merging.
   if (mergeCategory) then
    table.insert(combatMergeData[mergeCategory].UnmergedEvents, combatEvent);
   -- Add the event the animation system.
   else
    MikSBT.AddAnimation(combatEvent);
   end
  end
 end -- Profile data exists for the event type.
end


-- **********************************************************************************
-- Handle trigger events.
-- **********************************************************************************
function MikSBT.TriggerHandler(triggerEvent)
 -- Check if there is profile data for the trigger.
 if (MSBTProfiles.CurrentProfile.Triggers[triggerEvent.TriggerKey]) then
  -- Get the event settings and scroll area settings for the trigger.
  local eventSettings = MSBTProfiles.CurrentProfile.Triggers[triggerEvent.TriggerKey].EventSettings;
  local scrollAreaSettings = MSBTProfiles.CurrentProfile.ScrollAreaSettings[eventSettings.ScrollAreaIndex or MSBTDefaults.EventSettings.ScrollAreaIndex];

  -- Check if the trigger is enabled.
  if (not eventSettings.Disabled) then
   -- Play the associated sound file if there is one.
   if (eventSettings.SoundFile and not MSBTProfiles.CurrentProfile.SoundsDisabled) then
    PlaySound("GAMEHIGHLIGHTFRIENDLYUNIT"); -- TODO: Remove when Blizzard fixes PlaySoundFile()
    PlaySoundFile(SOUND_PATH .. eventSettings.SoundFile);
   end

   -- Check if the trigger's scroll area is enabled.
   if (not scrollAreaSettings.Disabled) then
    -- Get a recycled table from the table recycler.
    local combatEvent = MSBTTableRecycler.AcquireTable();

    -- Set the event settings, scroll area settings, and format the output text.
    combatEvent.EventSettings = eventSettings;
    combatEvent.ScrollAreaSettings = scrollAreaSettings;
    MikSBT.FormatTriggerText(combatEvent, triggerEvent);

    -- Add the event the animation system.
    MikSBT.AddAnimation(combatEvent);
   end
  end

 -- No profile data.
 else
  -- Print an error message.
  MikSBT.Print("No profile data found for trigger key " .. triggerEvent.TriggerKey, 1, 0, 0);
 end
end


-------------------------------------------------------------------------------------
-- Utility functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Create a map of spells to be recharacterized.
-- **********************************************************************************
function MikSBT.CreateRecharacterizeMap()
 -- Create a table for recharacterization.
 recharacterizeMap = {};

 -- Create an entry for heals over time.
 recharacterizeMap.HoTs = {};
 recharacterizeMap.HoTs[MSBTLocale.SPELL_DRAIN_LIFE]		= true;
 recharacterizeMap.HoTs[MSBTLocale.SPELL_SHADOWMEND]		= true;
 recharacterizeMap.HoTs[MSBTLocale.SPELL_SIPHON_LIFE]		= true;
 recharacterizeMap.HoTs[MSBTLocale.SPELL_VAMPIRIC_EMBRACE]	= true;

 -- Create an entry for damage over time.
 recharacterizeMap.DoTs = {};
 recharacterizeMap.DoTs[MSBTLocale.SPELL_REFLECTIVE_SHIELD]	= true;
end



-- **********************************************************************************
-- Merges like combat events.
-- **********************************************************************************
function MikSBT.MergeEvents(mergeData, numEvents)
 -- Holds and unmerged event and whether or not to merge it.
 local unmergedEvent;
 local doMerge = false;

 -- Don't attempt to merge any more events than were available when the function was called since
 -- more events may get added while the merge is taking place.
 for x = 1, numEvents do
  -- Get the unmerged event.
  unmergedEvent = mergeData.UnmergedEvents[x];

  -- Loop through all of the events in the merged events array.
  for _, mergedEvent in ipairs(mergeData.MergedEvents) do
   -- Check if the event types match.
   if (unmergedEvent.EventType == mergedEvent.EventType) then
    -- Check if there is no ability/spell name. 
    if (unmergedEvent.EffectName == nil) then
     -- Set the merge flag if the affected unit name is the same.
     if ((unmergedEvent.Name == mergedEvent.Name) and unmergedEvent.Name) then
      doMerge = true;
     end

    -- The ability/spell names match.
    elseif (unmergedEvent.EffectName == mergedEvent.EffectName) then
     -- Change the name to the multiple targets string if the names don't match.
     if (unmergedEvent.Name ~= mergedEvent.Name) then
      mergedEvent.Name = MSBTLocale.MSG_MULTIPLE_TARGETS;
     end

     -- Set the merge flag.
     doMerge = true;
    end
   end -- Event types match.


   -- Check if the event should be merged.
   if (doMerge) then
    -- Clear partial effects.
    mergedEvent.PartialEffectText = nil;

    -- Set the event merged flag for the event being merged.
    unmergedEvent.EventMerged = true;

    -- If there is an amount, total it.
    if (unmergedEvent.Amount ~= nil) then
     mergedEvent.Amount = (mergedEvent.Amount or 0) + unmergedEvent.Amount;
    end

    -- Check if there is an overheal amount, total it.
    if (unmergedEvent.OverhealAmount ~= nil) then
     mergedEvent.OverhealAmount = (mergedEvent.OverhealAmount or 0) + unmergedEvent.OverhealAmount;
    end
   
    -- Increment the number of merged events.
    mergedEvent.NumMerged = mergedEvent.NumMerged + 1;

    -- Check if the event being merged is a crit and increment the number of crits.
    if (unmergedEvent.IsCrit) then
     mergedEvent.NumCrits = mergedEvent.NumCrits + 1;
    end

    -- Make sure the merged event is not flagged as a crit.
    mergedEvent.IsCrit = false;

    -- Break out of the merged events loop since the event has been merged.
    break;
   end -- Do Merge.
  end -- Loop through all merged events.


  -- If the event wasn't merged add it to the end of the merged events array.
  if (not doMerge) then
   local mergedEvent = unmergedEvent;
   mergedEvent.NumMerged = 0;

   -- Check if the event is a crit and set the number of crits appropriately.
   if (mergedEvent.IsCrit) then
    mergedEvent.NumCrits = 1;
   else
    mergedEvent.NumCrits = 0;
   end

   -- Add the event to the end of the merged events array.
   table.insert(mergeData.MergedEvents, mergedEvent);
  end

  -- Reset the event merged flag.
  doMerge = false;
 end


 -- Loop through each event in the merged events array.
 for _, mergedEvent in ipairs(mergeData.MergedEvents) do
  -- Check if there were any events merged.
  if (mergedEvent.NumMerged > 0) then
   -- Create trailer text with the number of merged events.
   local trailerText = " [" .. mergedEvent.NumMerged + 1 .. " " .. MSBTLocale.MSG_HITS;

   -- Check if there were any crits merged.
   if (mergedEvent.NumCrits > 1) then
    trailerText = trailerText .. ", " .. mergedEvent.NumCrits .. " " .. MSBTLocale.MSG_CRITS;
   elseif (mergedEvent.NumCrits == 1) then
    trailerText = trailerText .. ", " .. mergedEvent.NumCrits .. " " .. MSBTLocale.MSG_CRIT;
   end

   -- Close the merge string.
   trailerText = trailerText .. "]";

   -- Set the event's merge trailer text field.
   mergedEvent.MergeTrailerText = trailerText;
  end
 end


 -- Remove the processed events from unmerged events queue.
 for x = 1, numEvents do
  -- Check if the event was merged into a different event.
  if (mergeData.UnmergedEvents[1].EventMerged) then
   -- Reclaim the combat event table to the events recycler.
   MSBTTableRecycler.ReclaimTable(mergeData.UnmergedEvents[1]);
  end

  -- Remove the event from the unmerged events array.
  table.remove(mergeData.UnmergedEvents, 1);
 end
end


-- **********************************************************************************
-- This function formats partial effect text using the values in the passed
-- parser event's fields.
-- **********************************************************************************
function MikSBT.GetFormattedPartialEffectText(parserEvent)
 -- Get the partial effect settings.
 local partialEffectSettings = MSBTProfiles.CurrentProfile.PartialEffectSettings;

 local partialEffectText = "";

 -- Partial Absorb
 if (parserEvent.AbsorbAmount and not partialEffectSettings.Absorb.Disabled and ABSORB_TRAILER) then
  local trailer = string.gsub(ABSORB_TRAILER, "%%d", parserEvent.AbsorbAmount);
  local color = partialEffectSettings.Absorb.Color;
  partialEffectText = string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), trailer);

 -- Partial Block
 elseif (parserEvent.BlockAmount and not partialEffectSettings.Block.Disabled and BLOCK_TRAILER) then
  local trailer = string.gsub(BLOCK_TRAILER, "%%d", parserEvent.BlockAmount);
  local color = partialEffectSettings.Block.Color;
  partialEffectText = string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), trailer);

 -- Partial Resist
 elseif (parserEvent.ResistAmount and not partialEffectSettings.Resist.Disabled and RESIST_TRAILER) then
  local trailer = string.gsub(RESIST_TRAILER, "%%d", parserEvent.ResistAmount);
  local color = partialEffectSettings.Resist.Color;
  partialEffectText = string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), trailer);

 -- Vulnerability
 elseif (parserEvent.VulnerabilityAmount and not partialEffectSettings.Vulnerability.Disabled) then
  local trailer = string.gsub(MSBTLocale.MSG_VULNERABLE_TRAILER, "%%d", parserEvent.VulnerabilityAmount);
  local color = partialEffectSettings.Vulnerability.Color;
  partialEffectText = string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), trailer);
 end


 -- Glancing hit
 if (parserEvent.IsGlancing and not partialEffectSettings.Glancing.Disabled and GLANCING_TRAILER) then
  local color = partialEffectSettings.Glancing.Color;
  partialEffectText = partialEffectText .. string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), GLANCING_TRAILER);

 -- Crushing blow
 elseif (parserEvent.IsCrushing and not partialEffectSettings.Crushing.Disabled and CRUSHING_TRAILER) then
  local color = partialEffectSettings.Crushing.Color;
  partialEffectText = partialEffectText .. string.format("|cFF%02x%02x%02x%s|r", ceil(color.r * 255), ceil(color.g * 255), ceil(color.b * 255), CRUSHING_TRAILER);
 end

 -- Strip out the color codes if coloring is disabled.
 if (partialEffectSettings.ColoringDisabled and partialEffectText) then
  partialEffectText = string.gsub(partialEffectText, "|c%x%x%x%x%x%x%x%x(.-)|r", "%1")
 end

 return partialEffectText; 
end


-- **********************************************************************************
-- This function substitutes the codes in the unformatted text of the passed
-- combat event with the values in its fields.
-- **********************************************************************************
function MikSBT.FormatEventText(combatEvent)
 -- Check if there is no unformatted text and bail.
 if (not combatEvent.UnformattedText) then
  return;
 end

 -- Save the unformatted text into a local variable.
 local outputString = combatEvent.UnformattedText;


 -- Substitute amount.
 if (combatEvent.Amount ~= nil) then
  local amount = combatEvent.Amount;

  -- Get the hex color for the damage type if there is one and coloring is enabled.
  local damageColorCode;
  if (combatEvent.DamageType ~= nil and not MSBTProfiles.CurrentProfile.DamageColorSettings.ColoringDisabled) then
   local damageColor;

   if (combatEvent.DamageType == MSBTParser.DAMAGETYPE_PHYSICAL) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Physical.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_HOLY) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Holy.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_FIRE) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Fire.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_NATURE) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Nature.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_FROST) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Frost.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_SHADOW) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Shadow.Color;
   elseif (combatEvent.DamageType == MSBTParser.DAMAGETYPE_ARCANE) then
    damageColor = MSBTProfiles.CurrentProfile.DamageColorSettings.Arcane.Color;
   end

   -- Set the damage color code if the color data is present for the damage type.
   if (damageColor) then
    damageColorCode = string.format("|cFF%02x%02x%02x", ceil(damageColor.r * 255), ceil(damageColor.g * 255), ceil(damageColor.b * 255)); 
   end
  end


  -- Check if there is overheal information and displaying it is enabled.
  if (combatEvent.OverhealAmount ~= nil and not MSBTProfiles.CurrentProfile.PartialEffectSettings.Overheal.Disabled) then
   local overhealAmount, overhealColorCode;

   -- Get the overheal amount and color it with the correct color if coloring is enabled.
   overhealAmount = combatEvent.OverhealAmount;
   if (not MSBTProfiles.CurrentProfile.PartialEffectSettings.ColoringDisabled) then
    local overhealColor = MSBTProfiles.CurrentProfile.PartialEffectSettings.Overheal.Color;
    overhealColorCode = string.format("|cFF%02x%02x%02x", ceil(overhealColor.r * 255), ceil(overhealColor.g * 255), ceil(overhealColor.b * 255)); 
   end

   -- Color the overheal amount if there is a color code.
   if (overhealColorCode) then
    overhealAmount = overhealColorCode .. overhealAmount .. "|r";
   end
   
   -- Append the overheal amount to the actual amount healed.
   amount = (combatEvent.Amount - combatEvent.OverhealAmount) .. string.gsub(OVERHEAL_TRAILER, "%%d", overhealAmount);
  end

  -- Color the amount according to the damage type, if any.
  if (damageColorCode) then
   amount = damageColorCode .. amount .. "|r";
  end

  outputString = string.gsub(outputString, "%%a", amount);
 end


 -- Substitute enemy/player names.
 if (combatEvent.Name) then
  outputString = string.gsub(outputString, "%%n", combatEvent.Name);
 end


 -- Substitute effect names.
 if (combatEvent.EffectName) then
  outputString = string.gsub(outputString, "%%s", combatEvent.EffectName);
 end


 -- Substitute damage types.
 if (combatEvent.DamageType ~= nil) then
  outputString = string.gsub(outputString, "%%t", MSBTParser.GetDamageTypeString(combatEvent.DamageType));
 end


 -- Check if there is partial effect text and append it.
 if (combatEvent.PartialEffectText) then
  outputString = outputString .. combatEvent.PartialEffectText;
 end


 -- Check if there is merge trailer text and append it.
 if (combatEvent.MergeTrailerText) then
  outputString = outputString .. combatEvent.MergeTrailerText;
 end

 -- Set the event's output text.
 combatEvent.Text = outputString;
end


-- **********************************************************************************
-- This function formats the output message for the passed trigger event with all
-- %1-9 codes substituted with captured data.
-- **********************************************************************************
function MikSBT.FormatTriggerText(combatEvent, triggerEvent)
 local outputString = combatEvent.EventSettings.Message;

 -- Check if there is no text and bail.
 if (outputString == nil) then
  return;
 end

 -- Loop through all of the captures in the trigger event.
 for i = 1, triggerEvent.NumCaptures do
  outputString = string.gsub(outputString , "%%" .. i, triggerEvent["CapturedData" .. i]);
 end

 -- Set the event's output text.
 combatEvent.Text = outputString;
end


-- **********************************************************************************
-- This function returns the registered fonts.
-- **********************************************************************************
function MikSBT.GetRegisteredFonts()
 return registeredFonts;
end


-- **********************************************************************************
-- This function returns the registered animation styles.
-- **********************************************************************************
function MikSBT.GetRegisteredAnimationStyles()
 return registeredAnimStyles;
end


-- **********************************************************************************
-- This function returns the registered sticky animation styles.
-- **********************************************************************************
function MikSBT.GetRegisteredStickyAnimationStyles()
 return registeredStickyAnimStyles;
end


-------------------------------------------------------------------------------------
-- Font functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- This function gets the appropriate font size for an event.
-- **********************************************************************************
function MikSBT.GetFontSize(eventSettings, scrollAreaSettings, isCrit)
 -- Holds the various font sizes for the event.
 local eventNormalFontSize, eventCritFontSize = 0;
 local scrollAreaNormalFontSize = scrollAreaSettings.NormalFontSize or MSBTDefaults.ScrollAreaSettings.NormalFontSize;
 local scrollAreaCritFontSize = scrollAreaSettings.CritFontSize or MSBTDefaults.ScrollAreaSettings.CritFontSize;
 local masterNormalFontSize = MSBTProfiles.CurrentProfile.MasterFontSettings.NormalFontSize;
 local masterCritFontSize = MSBTProfiles.CurrentProfile.MasterFontSettings.CritFontSize;

 -- Make sure the passed event settings are valid and get the event font sizes.
 if (eventSettings) then
  eventNormalFontSize = eventSettings.NormalFontSize or MSBTDefaults.EventSettings.NormalFontSize;
  eventCritFontSize = eventSettings.CritFontSize or MSBTDefaults.EventSettings.CritFontSize;
 end


 -- Declare variable to hold the font size to use.
 local fontSize;

 -- Check if the event is a crit.
 if (isCrit) then
  -- Get the crit font size for the event.
  fontSize = eventCritFontSize;

  -- Check if the font size is inherited from the scroll area font size.
  if (fontSize == 0) then
   fontSize = scrollAreaCritFontSize;
  end

  -- Check if the crit font size for the scroll area is inherited from the master font settings.
  if (fontSize == 0) then
   fontSize = masterCritFontSize;
  end

 -- Event is not a crit.
 else
  -- Get the normal font size for the event.
  fontSize = eventNormalFontSize;

  -- Check if the font size is inherited from the scroll area font size.
  if (fontSize == 0) then
   fontSize = scrollAreaNormalFontSize;
  end

  -- Check if the normal font size for the scroll area is inherited from the master font settings.
  if (fontSize == 0) then
   fontSize = masterNormalFontSize;
  end
 end

 -- Return the font size.
 return fontSize;
end


-- **********************************************************************************
-- This function gets the appropriate outline index for an event.
-- **********************************************************************************
function MikSBT.GetOutlineIndex(eventSettings, scrollAreaSettings, isCrit)
 -- Holds the various outline indices for the event.
 local eventNormalOutlineIndex, eventCritOutlineIndex = 0;
 local scrollAreaNormalOutlineIndex = scrollAreaSettings.NormalOutlineIndex or MSBTDefaults.ScrollAreaSettings.NormalOutlineIndex;
 local scrollAreaCritOutlineIndex = scrollAreaSettings.CritOutlineIndex or MSBTDefaults.ScrollAreaSettings.CritOutlineIndex;
 local masterNormalOutlineIndex = MSBTProfiles.CurrentProfile.MasterFontSettings.NormalOutlineIndex;
 local masterCritOutlineIndex = MSBTProfiles.CurrentProfile.MasterFontSettings.CritOutlineIndex;

 -- Make sure the passed event settings are valid and get the event font outline indices.
 if (eventSettings) then
  eventNormalOutlineIndex = eventSettings.NormalOutlineIndex or MSBTDefaults.EventSettings.NormalOutlineIndex;
  eventCritOutlineIndex = eventSettings.CritOutlineIndex or MSBTDefaults.EventSettings.CritOutlineIndex;
 end


 -- Declare variable to hold the outline index to use.
 local outlineIndex;

 -- Check if the event is a crit.
 if (isCrit) then
  -- Get the crit outline index for the event.
  outlineIndex = eventCritOutlineIndex;

  -- Check if the outline index is inherited from the scroll area outline index.
  if (outlineIndex == 0) then
   outlineIndex = scrollAreaCritOutlineIndex;
  end

  -- Check if the crit outline index for the scroll area is inherited from the master font settings.
  if (outlineIndex == 0) then
   outlineIndex = masterCritOutlineIndex;
  end

 -- Event is not a crit.
 else
  -- Get the normal outline index for the event.
  outlineIndex = eventNormalOutlineIndex;

  -- Check if the outline index is inherited from the scroll area outline index.
  if (outlineIndex == 0) then
   outlineIndex = scrollAreaNormalOutlineIndex;
  end

  -- Check if the normal outline index for the scroll area is inherited from the master font settings.
  if (outlineIndex == 0) then
   outlineIndex = masterNormalOutlineIndex;
  end
 end

 -- Return the outline index.
 return outlineIndex;
end


-- **********************************************************************************
-- This function gets the appropriate font path for an event.
-- **********************************************************************************
function MikSBT.GetFontPath(eventSettings, scrollAreaSettings, isCrit)
 -- Holds the various font names for the event.
 local eventNormalFontName, eventCritFontName = 0;
 local scrollAreaNormalFontName = scrollAreaSettings.NormalFontName or MSBTDefaults.ScrollAreaSettings.NormalFontName;
 local scrollAreaCritFontName = scrollAreaSettings.CritFontName or MSBTDefaults.ScrollAreaSettings.CritFontName;
 local masterNormalFontName = MSBTProfiles.CurrentProfile.MasterFontSettings.NormalFontName;
 local masterCritFontName = MSBTProfiles.CurrentProfile.MasterFontSettings.CritFontName;

 -- Make sure the passed event settings are valid and get the event font names.
 if (eventSettings) then
  eventNormalFontName = eventSettings.NormalFontName or MSBTDefaults.EventSettings.NormalFontName;
  eventCritFontName = eventSettings.CritFontName or MSBTDefaults.EventSettings.CritFontName;
 end


 -- Declare variable to hold the font name to use.
 local fontName;

 -- Check if the event is a crit.
 if (isCrit) then
  -- Get the crit font name for the event.
  fontName = eventCritFontName;

  -- Check if the font name is inherited from the scroll area font name.
  if (fontName == 0) then
   fontName = scrollAreaCritFontName;
  end

  -- Check if the crit font name for the scroll area is inherited from the master font settings.
  if (fontName == 0) then
   fontName = masterCritFontName;
  end

 -- Event is not a crit.
 else
  -- Get the normal font name for the event.
  fontName = eventNormalFontName;

  -- Check if the font name is inherited from the scroll area font name.
  if (fontName == 0) then
   fontName = scrollAreaNormalFontName;
  end

  -- Check if the normal font name for the scroll area is inherited from the master font settings.
  if (fontName == 0) then
   fontName = masterNormalFontName;
  end
 end


 -- Get the correct font path.
 return registeredFonts[fontName] or registeredFonts["Default"];
end


-- **********************************************************************************
-- Gets a font string to use by pulling one from cache or creating a new one.
-- **********************************************************************************
function MikSBT.AcquireFontString()
 -- Return a cached font string if there are any.
 if (table.getn(fontStringCache) > 0) then
  return table.remove(fontStringCache);

 -- No cached font strings, so create a new one and return it.
 else
  return animationFrame:CreateFontString(nil, "ARTWORK", MasterFont);
 end
end


-- **********************************************************************************
-- Reclaims a font string to cache.
-- **********************************************************************************
function MikSBT.ReclaimFontString(fontString)
 -- Cache the font string. 
 table.insert(fontStringCache, fontString);
end


-------------------------------------------------------------------------------------
-- Animation functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Gets the appropriate initialize and scroll functions for the passed scroll area
-- settings.
-- **********************************************************************************
function MikSBT.GetScrollFunctions(scrollAreaSettings, isSticky)
 local animationStyleSettings;
 if (isSticky) then
  animationStyleSettings = registeredStickyAnimStyles[scrollAreaSettings.StickyAnimationStyle] or
                           registeredStickyAnimStyles[MSBTDefaults.ScrollAreaSettings.StickyAnimationStyle];
 else
  animationStyleSettings = registeredAnimStyles[scrollAreaSettings.AnimationStyle] or
                           registeredAnimStyles[MSBTDefaults.ScrollAreaSettings.AnimationStyle];
 end

 return animationStyleSettings.InitFunc,
        (scrollAreaSettings.Reversed and animationStyleSettings.AnimateReverseFunc) or animationStyleSettings.AnimateFunc;
end


-- **********************************************************************************
-- Add the event to the appropriate scroll area's animation display array.
-- **********************************************************************************
function MikSBT.AddAnimation(combatEvent)
 -- Format the event's output text.
 MikSBT.FormatEventText(combatEvent);

 -- Check if the text is suppressed.
 if (MSBTSuppressions.IsSuppressed(combatEvent.Text)) then
  -- Reclaim the combat event table to the events recycler and leave the function.
  MSBTTableRecycler.ReclaimTable(combatEvent);
  return;
 end


 -- Create arrays to hold normal and sticky display events for the event's scroll area
 -- if there isn't one already.
 if (not animationData[combatEvent.ScrollAreaSettings]) then
  animationData[combatEvent.ScrollAreaSettings] = {Normal = {}, Sticky = {}};
 end


 -- Hold font info.
 local fontPath, fontOutline, colorR, colorG, colorB;

 -- Get a recycled table from the table recycler.
 local displayEvent = MSBTTableRecycler.AcquireTable();


 -- Get scroll area settings.
 displayEvent.OffsetX = combatEvent.ScrollAreaSettings.OffsetX or MSBTDefaults.ScrollAreaSettings.OffsetX;
 displayEvent.OffsetY = combatEvent.ScrollAreaSettings.OffsetY or MSBTDefaults.ScrollAreaSettings.OffsetY;
 displayEvent.AnchorPoint = TEXT_ALIGN_MAP[combatEvent.ScrollAreaSettings.TextAlignIndex or MSBTDefaults.ScrollAreaSettings.TextAlignIndex];
 displayEvent.ScrollHeight = combatEvent.ScrollAreaSettings.ScrollHeight or MSBTDefaults.ScrollAreaSettings.ScrollHeight;

 -- Check if the combat event has event settings associated with it.
 if (combatEvent.EventSettings) then
  displayEvent.FontSize = MikSBT.GetFontSize(combatEvent.EventSettings, combatEvent.ScrollAreaSettings, combatEvent.IsCrit);
  fontPath = MikSBT.GetFontPath(combatEvent.EventSettings, combatEvent.ScrollAreaSettings, combatEvent.IsCrit);
  fontOutline = OUTLINE_MAP[MikSBT.GetOutlineIndex(combatEvent.EventSettings, combatEvent.ScrollAreaSettings, combatEvent.IsCrit)];
  colorR = (combatEvent.EventSettings.Color and combatEvent.EventSettings.Color.r) or MSBTDefaults.EventSettings.Color.r;
  colorG = (combatEvent.EventSettings.Color and combatEvent.EventSettings.Color.g) or MSBTDefaults.EventSettings.Color.g;
  colorB = (combatEvent.EventSettings.Color and combatEvent.EventSettings.Color.b) or MSBTDefaults.EventSettings.Color.b;

 -- No event type associated with the event so get the display info from the combat event itself.
 else
  displayEvent.FontSize = combatEvent.FontSize;
  fontPath = combatEvent.FontPath;
  fontOutline = combatEvent.FontOutline;
  colorR = combatEvent.ColorR;
  colorG = combatEvent.ColorG;
  colorB = combatEvent.ColorB;
 end

 -- Animation speed value.
 displayEvent.AnimationSpeed = (MSBTProfiles.CurrentProfile.AnimationSpeed or 100) / 100;

 -- Default starting alpha and x/y positions.
 displayEvent.Alpha = 0;
 displayEvent.PositionX = 0;
 displayEvent.PositionY = 0;

 -- Acquire a font string from cache.
 displayEvent.FontString = MikSBT.AcquireFontString();


 -- Set font string properties.
 displayEvent.FontString:ClearAllPoints();
 displayEvent.FontString:SetFont(fontPath, displayEvent.FontSize, fontOutline);
 displayEvent.FontString:SetTextColor(colorR, colorG, colorB);
 displayEvent.FontString:SetText(combatEvent.Text);
 displayEvent.FontString:SetAlpha(displayEvent.Alpha);

 -- Set timing properties.
 displayEvent.ElapsedTime = 0;
 displayEvent.TimeSinceLastUpdate = 0;

 
 -- Get whether or not the event is to be displayed sticky.
 local isSticky = combatEvent.IsSticky or (combatEvent.IsCrit and not MSBTProfiles.CurrentProfile.StickyCritsDisabled);
 if (combatEvent.EventSettings and combatEvent.EventSettings.AlwaysSticky) then
  isSticky = true;
 end


 -- Change the draw layer of the font string so that stickies are on top and
 -- get the correct animation array.
 local animationArray;
 if (isSticky) then
  displayEvent.FontString:SetDrawLayer("OVERLAY");
  animationArray = animationData[combatEvent.ScrollAreaSettings].Sticky;
 else
  displayEvent.FontString:SetDrawLayer("ARTWORK");
  animationArray = animationData[combatEvent.ScrollAreaSettings].Normal;
 end


 -- Get the correct initialize and animate functions for the scroll area.
 local initFunc;
 initFunc, displayEvent.AnimationFunction = MikSBT.GetScrollFunctions(combatEvent.ScrollAreaSettings, isSticky);

 -- Call init func to initialize the new display event and to move any overlapping events.
 initFunc(displayEvent, animationArray, combatEvent.ScrollAreaSettings.Reversed);

 -- Move the font string to the initial starting location.
 displayEvent.FontString:SetPoint(displayEvent.AnchorPoint, displayEvent.OffsetX + displayEvent.PositionX, displayEvent.OffsetY + displayEvent.PositionY);


 -- Add the display event to the appropriate scroll area's array.
 table.insert(animationArray, displayEvent);


 -- Check if the animation frame is not visible and make it visible so the OnUpdate events start firing.
 -- This is done to keep the number of OnUpdate events down to a minimum for better performance.
 if (not animationFrame:IsVisible()) then
  animationFrame:Show();
 end


 -- Reclaim the combat event table to the events recycler.
 MSBTTableRecycler.ReclaimTable(combatEvent);
end


-------------------------------------------------------------------------------------
-- Command handler functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Returns the current and remaining parameters from the passed string.
-- **********************************************************************************
function MikSBT.GetNextParameter(paramString)
 local remainingParams;
 local currentParam = paramString;

 -- Look for a space.
 local idx = string.find(paramString, " ");

 if (idx) then
  -- Get the current and remaing parameters.
  currentParam = string.sub(paramString, 1, idx-1);
  remainingParams = string.sub(paramString, idx+1);
 end

 -- Return the current parameter and the remaining ones.
 return currentParam, remainingParams;
end


-- **********************************************************************************
-- Called to handle commands.
-- **********************************************************************************
function MikSBT.CommandHandler(params)
 -- Get the parameter.
 local currentParam;
 local remainingParams;
 currentParam, remainingParams = MikSBT.GetNextParameter(params);

 -- Flag for whether or not to show usage info.
 local showUsage = true;

 -- Make sure there is a current parameter and lower case it.
 if (currentParam) then
  currentParam = string.lower(currentParam);
 end

 -- Look for the recognized parameters.
 if (currentParam == "") then
  -- Check if the load on demand options are not loaded.
  if (not IsAddOnLoaded(MikSBT.MOD_NAME .. "Options")) then
   -- Load the on demand options.
   UIParentLoadAddOn(MikSBT.MOD_NAME .. "Options");
  end

  -- Make sure the on demand options actually loaded.
  if (IsAddOnLoaded(MikSBT.MOD_NAME .. "Options")) then
   -- Make sure the scroll areas config frame is not shown.
   if (not MSBTScrollAreasConfigFrame:IsShown()) then
    -- Show the options interface.
    MSBTFrameOptions:Show();
   end
  end

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_RESET) then
  -- Reset the current profile.
  MSBTProfiles.ResetProfile(MSBTProfiles.GetSavedVariablesPerChar().CurrentProfileName, true);

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_DISABLE) then
  -- Stop receiving updates.
  eventFrame:Hide();

  -- Set the user disabled option.
  MSBTProfiles.SetOptionUserDisabled(true);

  -- Output an informative message.
  MikSBT.Print(MSBTLocale.MSG_DISABLE, 1,1,1);

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_ENABLE) then
  -- Start receiving updates.
  eventFrame:Show();

  -- Unset the user disabled option.
  MSBTProfiles.SetOptionUserDisabled(false);

  -- Output an informative message.
  MikSBT.Print(MSBTLocale.MSG_ENABLE, 1,1,1);

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_SHOWVER) then
  -- Output the current version number.
  MikSBT.Print(MikSBT.VERSION_STRING, 1,1,1);

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_STATS) then
  -- Print out stats about table recycling.
  MSBTTableRecycler.PrintStats();

  -- Don't show the usage info.
  showUsage = false;

 elseif (currentParam == MSBTLocale.COMMAND_SEARCH) then
  -- Check if there is any filter text
  if (remainingParams and remainingParams ~= "") then
   -- Turn on event searching mode.
   MSBTTriggers.EnableEventSearching(remainingParams);
   MikSBT.Print(MSBTLocale.MSG_SEARCH_ENABLE .. remainingParams, 1, 1, 1);
  else
   -- Turn off event searching mode.
   MSBTTriggers.DisableEventSearching();
   MikSBT.Print(MSBTLocale.MSG_SEARCH_DISABLE, 1, 1, 1);
  end

  -- Don't show the usage info.
  showUsage = false;

 end

 -- Check if the usage information should be shown.
 if (showUsage) then
  MikSBT.PrintUsage();
 end
end


-- **********************************************************************************
-- Prints out the usage information.
-- **********************************************************************************
function MikSBT.PrintUsage()
 -- Loop through all of the entries in the command usage list.
 for _, msg in pairs(MSBTLocale.COMMAND_USAGE) do
  MikSBT.Print(msg, 1, 1, 1);
 end
end




-------------------------------------------------------------------------------------
-- Public Access functions.
-------------------------------------------------------------------------------------

-- **********************************************************************************
-- Displays the passed message in the passed scroll area with the passed parameters.
-- This function is for easy displaying of messages from external sources.  See
-- the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.DisplayMessage(message, scrollArea, isSticky, colorR, colorG, colorB, fontSize, fontName, outlineIndex)
 -- Check if no message was passed or the mod is disabled and do nothing.
 if (not message or MSBTProfiles.GetSavedVariables().UserDisabled) then
  return;
 end


 -- Get the notification scroll area settings.
 local scrollAreaSettings = MSBTProfiles.CurrentProfile.ScrollAreaSettings[MikSBT.DISPLAYTYPE_NOTIFICATION];

 -- Check if the passed scroll area is a valid index.
 if (MSBTProfiles.CurrentProfile.ScrollAreaSettings[scrollArea]) then
  scrollAreaSettings = MSBTProfiles.CurrentProfile.ScrollAreaSettings[scrollArea];

 -- Passed scroll area is not a valid index so search for a valid name.
 else
  -- Loop through all of the scroll areas to see if the passed scroll area matches one of the names.
  for _, saSettings in pairs(MSBTProfiles.CurrentProfile.ScrollAreaSettings) do
   if (scrollArea == saSettings.Name) then
    scrollAreaSettings = saSettings;
   end
  end
 end
 
 -- Leave the function if the scroll area is disabled.
 if (scrollAreaSettings.Disabled) then
  return;
 end


 -- Check if the red color value is invalid.
 if (colorR == nil or colorR < 0 or colorR > 255) then
  -- Set the red color value to the default.
  colorR = 255;
 end

 -- Check if the green color value is invalid.
 if (colorG == nil or colorG < 0 or  colorG > 255) then
  -- Set the green color value to the default.
  colorG = 255;
 end

 -- Check if the blue color value is invalid.
 if (colorB == nil or colorB < 0 or  colorB > 255) then
  -- Set the blue color value to the default.
  colorB = 255;
 end

 -- Check if the font size is invalid.
 if (fontSize == nil or fontSize < 12 or fontSize > 32) then
  fontSize = MikSBT.GetFontSize(nil, scrollAreaSettings, false);
 end

 local fontPath = registeredFonts[fontName];
 -- Get the font path from the scroll area if there is no font registered for the passed name.
 if (not fontPath) then
  fontPath = MikSBT.GetFontPath(nil, scrollAreaSettings, false);
 end

 local fontOutline = OUTLINE_MAP[outlineIndex];
 -- Get the outline from the scroll area if the passed index is invalid.
 if (not fontOutline) then
  fontOutline = OUTLINE_MAP[MikSBT.GetOutlineIndex(nil, scrollAreaSettings, false)];
 end


 -- Get a table from the table recycler object.
 local combatEvent = MSBTTableRecycler.AcquireTable();

 -- Populate the combat event data.
 combatEvent.ScrollAreaSettings = scrollAreaSettings;
 combatEvent.IsSticky = isSticky;
 combatEvent.Text = message;
 combatEvent.FontSize = fontSize;
 combatEvent.FontPath = fontPath;
 combatEvent.FontOutline = fontOutline;
 combatEvent.ColorR = colorR / 255;
 combatEvent.ColorG = colorG / 255;
 combatEvent.ColorB = colorB / 255;


 -- Add the event to the animation system.
 MikSBT.AddAnimation(combatEvent);
end


-- **********************************************************************************
-- Registers a font.
-- See the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.RegisterFont(fontName, fontPath)
 -- Add the font to the registered fonts array if it doesn't already exist.
 if (not registeredFonts[fontName] and fontPath) then
  registeredFonts[fontName] = fontPath;
 end
end


-- **********************************************************************************
-- Registers an animation style for non sticky events.
-- See the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.RegisterAnimationStyle(styleID, initFunc, animateFunc, animateReverseFunc, localizedName)
 -- Make sure there isn't already an animation style with the same name and the passed functions are valid.
 if (not registeredAnimStyles[styleID] and initFunc and animateFunc and animateReverseFunc) then
  -- Create new animation style.
  local animationStyleSettings = {};
  animationStyleSettings.Name = localizedName;
  animationStyleSettings.InitFunc = initFunc;
  animationStyleSettings.AnimateFunc = animateFunc;
  animationStyleSettings.AnimateReverseFunc = animateReverseFunc;

  -- Add the new style to the registered animation styles table.
  registeredAnimStyles[styleID] = animationStyleSettings;
 end
end


-- **********************************************************************************
-- Registers an animation style for sticky events.
-- See the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.RegisterStickyAnimationStyle(styleID, initFunc, animateFunc, animateReverseFunc, localizedName)
 -- Make sure there isn't already an animation style with the same name and the passed functions are valid.
 if (not registeredStickyAnimStyles[styleID] and initFunc and animateFunc and animateReverseFunc) then
  -- Create new animation style.
  local animationStyleSettings = {};
  animationStyleSettings.Name = localizedName;
  animationStyleSettings.InitFunc = initFunc;
  animationStyleSettings.AnimateFunc = animateFunc;
  animationStyleSettings.AnimateReverseFunc = animateReverseFunc;

  -- Add the new style to the registered stick animation styles table.
  registeredStickyAnimStyles[styleID] = animationStyleSettings;
 end
end


-- **********************************************************************************
-- Returns a list of registered fonts.
-- See the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.GetRegisteredFontList()
 -- Create a new table to hold the list.
 local fontList = {};

 -- Loop through all of the registered fonts and add them to the list.
 for fontName in pairs(registeredFonts) do
  table.insert(fontList, fontName);
 end

 -- Return the list.
 return fontList;
end


-- **********************************************************************************
-- Returns a list of available scroll areas.
-- See the included API.html file for usage info.
-- **********************************************************************************
function MikSBT.GetScrollAreaList()
 -- Create a new table to hold the list.
 local scrollAreaList = {};

 -- Loop through all of the scroll areas and add them to the list.
 for _, scrollAreaSettings in ipairs(MSBTProfiles.CurrentProfile.ScrollAreaSettings) do
  table.insert(scrollAreaList, scrollAreaSettings.Name);
 end

 -- Return the list.
 return scrollAreaList;
end


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

-- Load the Mod.
MikSBT.OnLoad();