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

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


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

-- Local references to various functions for faster access.
local string_find = string.find
local string_gmatch = string.gmatch
local string_gsub = string.gsub
local string_len = string.len
local bit_band = bit.band
local bit_bor = bit.bor
local GetTime = GetTime
local UnitClass = UnitClass
local UnitGUID = UnitGUID
local Print = MikSBT.Print
local EraseTable = MikSBT.EraseTable


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

-- Bit flags.
local AFFILIATION_MINE		= 0x00000001
local AFFILIATION_PARTY		= 0X00000002
local AFFILIATION_RAID		= 0X00000004
local AFFILIATION_OUTSIDER	= 0X00000008
local REACTION_FRIENDLY		= 0x00000010
local REACTION_NEUTRAL		= 0x00000020
local REACTION_HOSTILE		= 0x00000040
local CONTROL_HUMAN			= 0x00000100
local CONTROL_SERVER		= 0x00000200
local UNITTYPE_PLAYER		= 0x00000400
local UNITTYPE_NPC			= 0x00000800
local UNITTYPE_PET			= 0x00001000
local UNITTYPE_GUARDIAN		= 0x00002000
local UNITTYPE_OBJECT		= 0x00004000
local TARGET_TARGET			= 0x00010000
local TARGET_FOCUS			= 0x00020000
local OBJECT_NONE			= 0x80000000

-- Value when there is no GUID.
local GUID_NONE				= "0x0000000000000000"

-- The maximum number of buffs and debuffs that can be on a unit.
local MAX_BUFFS = 16
local MAX_DEBUFFS = 40

-- Aura types.
local AURA_TYPE_BUFF = "BUFF"
local AURA_TYPE_DEBUFF = "DEBUFF"

-- Update timings.
local UNIT_MAP_UPDATE_DELAY = 0.2
local PET_UPDATE_DELAY = 1
local REFLECT_HOLD_TIME = 3
local CLASS_HOLD_TIME = 300

-- Commonly used flag combinations.
local FLAGS_ME			= bit_bor(AFFILIATION_MINE, REACTION_FRIENDLY, CONTROL_HUMAN, UNITTYPE_PLAYER)
local FLAGS_MINE		= bit_bor(AFFILIATION_MINE, REACTION_FRIENDLY, CONTROL_HUMAN)
local FLAGS_MY_GUARDIAN	= bit_bor(AFFILIATION_MINE, REACTION_FRIENDLY, CONTROL_HUMAN, UNITTYPE_GUARDIAN)


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

-- Dynamically created frames for receiving events and tooltip info.
local eventFrame

-- Name and GUID of the player.
local playerName
local playerGUID

-- Used for timing between updates.
local lastUnitMapUpdate = 0
local lastPetMapUpdate = 0

-- Whether or not values that need to be updated after a delay are stale.
local isUnitMapStale
local isPetMapStale

-- Map of guids to unit ids.
local unitMap = {}
local petMap = {}

-- Map of functions to call for supported combat log events.
local captureFuncs

-- Events to parse even if the source or recipient is not the player or pet.
local fullParseEvents

-- Information about global strings for CHAT_MSG_X events.
local searchMap
local searchCaptureFuncs
local rareWords = {}
local searchPatterns = {}
local captureOrders = {}

-- Captured and parsed event data.
local captureTable = {}
local parserEvent = {}

-- List of functions to call when an event occurs.
local handlers = {}

-- Holds information about reflected skills to track how much was reflected.
local reflectedSkills = {}
local reflectedTimes = {}

-- Holds information about guid to class mappings for known units.
local classMapCleanupTime = 0
local classMap = {}
local classTimes = {}
local arenaUnits = {}


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

-- ****************************************************************************
-- Registers a function to be called when an event occurs.
-- ****************************************************************************
local function RegisterHandler(handler)
 handlers[handler] = true
end

-- ****************************************************************************
-- Unregisters a previously registered function.
-- ****************************************************************************
local function UnregisterHandler(handler)
 handlers[handler] = nil
end


-- ****************************************************************************
-- Tests if any of the bits in the passed testFlags are set in the unit flags.
-- ****************************************************************************
local function TestFlagsAny(unitFlags, testFlags)
 if (bit_band(unitFlags, testFlags) > 0) then return true end 
end


-- ****************************************************************************
-- Tests if all of the passed testFlags are set in the unit flags.
-- ****************************************************************************
local function TestFlagsAll(unitFlags, testFlags)
 if (bit_band(unitFlags, testFlags) == testFlags) then return true end
end


-- ****************************************************************************
-- Sends the parser event to the registered handlers.
-- ****************************************************************************
local function SendParserEvent()
 for handler in pairs(handlers) do
  local success, ret = pcall(handler, parserEvent)
  if (not success) then geterrorhandler()(ret) end
 end
end


-- ****************************************************************************
-- Compares two global strings so the most specific one comes first.  This
-- prevents incorrectly capturing information for certain events.
-- ****************************************************************************
local function GlobalStringCompareFunc(globalStringNameOne, globalStringNameTwo)
 -- Get the global string for the passed names.
 local globalStringOne = _G[globalStringNameOne]
 local globalStringTwo = _G[globalStringNameTwo]

 local gsOneStripped = string_gsub(globalStringOne, "%%%d?%$?[sd]", "")
 local gsTwoStripped = string_gsub(globalStringTwo, "%%%d?%$?[sd]", "")

 -- Check if the stripped global strings are the same length.
 if (string_len(gsOneStripped) == string_len(gsTwoStripped)) then
  -- Count the number of captures in each string.
  local numCapturesOne = 0
  for _ in string_gmatch(globalStringOne, "%%%d?%$?[sd]") do
   numCapturesOne = numCapturesOne + 1
  end

  local numCapturesTwo = 0
  for _ in string_gmatch(globalStringTwo, "%%%d?%$?[sd]") do
   numCapturesTwo = numCapturesTwo + 1
  end
  
  -- Return the global string with the least captures.
  return numCapturesOne < numCapturesTwo

 else
  -- Return the longest global string.
  return string_len(gsOneStripped) > string_len(gsTwoStripped)
 end
end


-- ****************************************************************************
-- Converts the passed global string into a lua search pattern with a capture
-- order table and stores the results so any requests to convert the same
-- global string will just return the cached one.
-- ****************************************************************************
local function ConvertGlobalString(globalStringName)
 -- Don't do anything if the passed global string does not exist.
 local globalString = _G[globalStringName]
 if (globalString == nil) then return end

 -- Return the cached conversion if it has already been converted.
 if (searchPatterns[globalStringName]) then
  return searchPatterns[globalStringName], captureOrders[globalStringName]
 end

 -- Hold the capture order.
 local captureOrder
 local numCaptures = 0

 -- Escape lua magic chars.
 local searchPattern = string.gsub(globalString, "([%^%(%)%.%[%]%*%+%-%?])", "%%%1")

 -- Loop through each capture and setup the capture order.
 for captureIndex in string_gmatch(searchPattern, "%%(%d)%$[sd]") do
  if (not captureOrder) then captureOrder = {} end
  numCaptures = numCaptures + 1
  captureOrder[tonumber(captureIndex)] = numCaptures
 end
 
 -- Convert %1$s / %s to (.+) and %1$d / %d to (%d+).
 searchPattern = string.gsub(searchPattern, "%%%d?%$?s", "(.+)")
 searchPattern = string.gsub(searchPattern, "%%%d?%$?d", "(%%d+)")

 -- Escape any remaining $ chars.
 searchPattern = string.gsub(searchPattern, "%$", "%%$")
 
 -- Cache the converted pattern and capture order.
 searchPatterns[globalStringName] = searchPattern
 captureOrders[globalStringName] = captureOrder

 -- Return the converted global string.
 return searchPattern, captureOrder
end


-- ****************************************************************************
-- Fills in the capture table with the captured data if a match is found.
-- ****************************************************************************
local function CaptureData(matchStart, matchEnd, c1, c2, c3, c4, c5, c6, c7, c8, c9)
 -- Check if a match was found.
 if (matchStart) then
  captureTable[1] = c1
  captureTable[2] = c2
  captureTable[3] = c3
  captureTable[4] = c4
  captureTable[5] = c5
  captureTable[6] = c6
  captureTable[7] = c7
  captureTable[8] = c8
  captureTable[9] = c9

  -- Return the last position of the match.
  return matchEnd
 end

 -- Don't return anything since no match was found.
 return nil
end


-- ****************************************************************************
-- Reorders the capture table according to the passed capture order.
-- ****************************************************************************
local function ReorderCaptures(capOrder)
 local t, o = captureTable, capOrder
 
 t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8], t[9] = 
 t[o[1] or 1], t[o[2] or 2], t[o[3] or 3], t[o[4] or 4], t[o[5] or 5],
 t[o[6] or 6], t[o[7] or 7], t[o[8] or 8], t[o[9] or 9]
end


-- ****************************************************************************
-- Parses the CHAT_MSG_X search style events.
-- ****************************************************************************
local function ParseSearchMessage(event, combatMessage)
 -- Leave if there is no map of global strings to search for the event.
 if (not searchMap[event]) then return end

 -- Loop through all of the global strings to search for the event.
 for _, globalStringName in pairs(searchMap[event]) do
  -- Make sure the capture func for the global string exists.
  local captureFunc = searchCaptureFuncs[globalStringName]
  if (captureFunc) then
   -- First, check if there is a rare word for the global string and it is in the combat
   -- message since a plain text search is faster than doing a full regular expression search.
   if (not rareWords[globalStringName] or string_find(combatMessage, rareWords[globalStringName], 1, true)) then
    -- Get capture data.
    local matchEnd = CaptureData(string_find(combatMessage, searchPatterns[globalStringName]))
  

    -- Check if a match was found. 
    if (matchEnd) then
     -- Check if there is a capture order for the global string and reorder the data accordingly.
     if (captureOrders[globalStringName]) then ReorderCaptures(captureOrders[globalStringName]) end

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

     -- Populate fields that exist for all events.
     parserEvent.sourceGUID = GUID_NONE
     parserEvent.sourceFlags = OBJECT_NONE
     parserEvent.recipientGUID = playerGUID
     parserEvent.recipientName = playerName
     parserEvent.recipientFlags = FLAGS_ME
     parserEvent.recipientUnit = "player"
	 
     -- Map the captured arguments into the parser event table.
     captureFunc(parserEvent, captureTable)

     -- Send the event.
     SendParserEvent()
     return
    end -- Match found.
   end -- Fast plain search.
  end -- Capture func is valid.
 end -- Loop through global strings to search. 
end


-- ****************************************************************************
-- Parses the parameter style events going to the combat log.
-- ****************************************************************************
local function ParseLogMessage(timestamp, event, sourceGUID, sourceName, sourceFlags, recipientGUID, recipientName, recipientFlags, ...)
 -- Make sure the capture function for the event exists.
 local captureFunc = captureFuncs[event]
 if (not captureFunc) then return end

 -- Look for spells the player reflected and make the damage belong to the player.
 if (sourceGUID == recipientGUID and reflectedTimes[recipientGUID] and event == "SPELL_DAMAGE") then
  local skillID = ...
  if (skillID == reflectedSkills[recipientGUID]) then
   -- Clear the reflected skill entries.
   reflectedTimes[recipientGUID] = nil
   reflectedSkills[recipientGUID] = nil

   -- Change the source to the player.
   sourceGUID = playerGUID
   sourceName = playerName
   sourceFlags = FLAGS_ME
  end
 end

 -- Attempt to figure out the source and recipient unitIDs.
 local sourceUnit = unitMap[sourceGUID] or petMap[sourceGUID]
 local recipientUnit = unitMap[recipientGUID] or petMap[recipientGUID]

 -- Treat guardians that are flagged as belonging to the player as their pet and vehicles and other objects as the player.
 if (not sourceUnit and TestFlagsAll(sourceFlags, FLAGS_MINE)) then sourceUnit = TestFlagsAll(sourceFlags, FLAGS_MY_GUARDIAN) and "pet" or "player" end
 if (not recipientUnit and TestFlagsAll(recipientFlags, FLAGS_MINE)) then recipientUnit = TestFlagsAll(recipientFlags, FLAGS_MY_GUARDIAN) and "pet" or "player" end
 
  -- Ignore the event if it is not one that should be fully parsed and it doesn't pertain to the player
 -- or pet.  This is done to avoid wasting time parsing events that won't be used like damage that other
 -- players are doing.
 if (not fullParseEvents[event] and sourceUnit ~= "player" and sourceUnit ~= "pet" and
     recipientUnit ~= "player" and recipientUnit ~= "pet") 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.sourceUnit = sourceUnit
 parserEvent.recipientGUID = recipientGUID
 parserEvent.recipientName = recipientName
 parserEvent.recipientFlags = recipientFlags 
 parserEvent.recipientUnit = recipientUnit
 
 -- Map the local arguments into the parser event table.
 captureFunc(parserEvent, ...)

 -- Track reflected skills.
 if (parserEvent.eventType == "miss" and parserEvent.missType == "REFLECT" and recipientUnit == "player") then
  -- Clean up old entries.
  for guid, reflectTime in pairs(reflectedTimes) do
   if (timestamp - reflectTime > REFLECT_HOLD_TIME) then
    reflectedTimes[guid] = nil
    reflectedSkills[guid] = nil
   end
  end

  -- Save the time of the reflect and the reflected skillID.
  reflectedTimes[sourceGUID] = timestamp
  reflectedSkills[sourceGUID] = parserEvent.skillID
 end

 -- Send the event.
 SendParserEvent()
end


-------------------------------------------------------------------------------
-- Startup utility functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Creates a list of events that will be fully parsed even if they event
-- doesn't pertain to the player or player's pet.
-- ****************************************************************************
local function CreateFullParseList()
 fullParseEvents = {
  SPELL_AURA_APPLIED = true,
  SPELL_AURA_REMOVED = true,
  SPELL_AURA_APPLIED_DOSE = true,
  SPELL_AURA_REMOVED_DOSE = true,
  SPELL_CAST_START = true,
 }
end


-- ****************************************************************************
-- Creates a map of global strings to search for CHAT_MSG_X events.
-- ****************************************************************************
local function CreateSearchMap()
 searchMap = {
  -- Honor Gains.
  CHAT_MSG_COMBAT_HONOR_GAIN = {"COMBATLOG_HONORGAIN", "COMBATLOG_HONORAWARD"},

  -- Reputation Gains/Losses.
  CHAT_MSG_COMBAT_FACTION_CHANGE = {"FACTION_STANDING_INCREASED", "FACTION_STANDING_DECREASED"},

  -- Skill Gains.
  CHAT_MSG_SKILL = {"SKILL_RANK_UP"},

  -- Experience Gains.
  CHAT_MSG_COMBAT_XP_GAIN = {"COMBATLOG_XPGAIN_FIRSTPERSON", "COMBATLOG_XPGAIN_FIRSTPERSON_UNNAMED"},

  -- Looted Items.
  CHAT_MSG_LOOT = {
   "LOOT_ITEM_CREATED_SELF_MULTIPLE", "LOOT_ITEM_CREATED_SELF", "LOOT_ITEM_PUSHED_SELF_MULTIPLE",
   "LOOT_ITEM_PUSHED_SELF", "LOOT_ITEM_SELF_MULTIPLE", "LOOT_ITEM_SELF"
  },
  
  -- Money.
  CHAT_MSG_MONEY = {"YOU_LOOT_MONEY", "LOOT_MONEY_SPLIT"},
 }


 -- Loop through each of the events.
 for event, map in pairs(searchMap) do
  -- Remove invalid global strings.
  for i = #map, 1, -1 do
   if (not _G[map[i]]) then table.remove(map, i) end
  end

  -- Sort the global strings from most to least specific.
  table.sort(map, GlobalStringCompareFunc)
 end
end


-- ****************************************************************************
-- Creates a map of capture functions for supported global strings.
-- ****************************************************************************
local function CreateSearchCaptureFuncs()
 searchCaptureFuncs = {
  -- Honor events.
  COMBATLOG_HONORAWARD = function (p, c) p.eventType, p.amount = "honor", c[1] end,
  COMBATLOG_HONORGAIN = function (p, c) p.eventType, p.sourceName, p.sourceRank, p.amount = "honor", c[1], c[2], c[3] end,

  -- Experience events.
  COMBATLOG_XPGAIN_FIRSTPERSON = function (p, c) p.eventType, p.sourceName, p.amount = "experience", c[1], c[2] end,
  COMBATLOG_XPGAIN_FIRSTPERSON_UNNAMED = function (p, c) p.eventType, p.amount = "experience", c[1] end,

  -- Reputation events.
  FACTION_STANDING_DECREASED = function (p, c) p.eventType, p.isLoss, p.factionName, p.amount = "reputation", true, c[1], c[2] end,
  FACTION_STANDING_INCREASED = function (p, c) p.eventType, p.factionName, p.amount = "reputation", c[1], c[2] end,

  -- Proficiency events.
  SKILL_RANK_UP = function (p, c) p.eventType, p.skillName, p.amount = "proficiency", c[1], c[2] end,

  -- Loot events.
  LOOT_ITEM_SELF = function (p, c) p.eventType, p.itemLink, p.amount = "loot", c[1], c[2] end,
  LOOT_ITEM_CREATED_SELF = function (p, c) p.eventType, p.isCreate, p.itemLink, p.amount = "loot", true, c[1], c[2] end,
  LOOT_MONEY_SPLIT = function (p, c) p.eventType, p.isMoney, p.moneyString = "loot", true, c[1] end,
 }

 searchCaptureFuncs["LOOT_ITEM_SELF_MULTIPLE"] = searchCaptureFuncs["LOOT_ITEM_SELF"]
 searchCaptureFuncs["LOOT_ITEM_CREATED_SELF_MULTIPLE"] = searchCaptureFuncs["LOOT_ITEM_CREATED_SELF"]
 searchCaptureFuncs["LOOT_ITEM_PUSHED_SELF"] = searchCaptureFuncs["LOOT_ITEM_CREATED_SELF"]
 searchCaptureFuncs["LOOT_ITEM_PUSHED_SELF_MULTIPLE"] = searchCaptureFuncs["LOOT_ITEM_CREATED_SELF"]
 searchCaptureFuncs["YOU_LOOT_MONEY"] = searchCaptureFuncs["LOOT_MONEY_SPLIT"]

 -- Print an error message for each global string that isn't found and remove it from the map.
 for globalStringName in pairs(searchCaptureFuncs) do
  if (not _G[globalStringName]) then
   Print("Unable to find global string: " .. globalStringName, 1, 0, 0)
   searchCaptureFuncs[globalStringName] = nil
  end
 end
end


-- ****************************************************************************
-- Finds the rarest word for each global string.
-- ****************************************************************************
local function FindRareWords()
 -- Hold the number of times each word appears in all the global strings.
 local wordCounts = {}

 -- Loop through all of the supported global strings.
 for globalStringName in pairs(searchCaptureFuncs) do
  -- Strip out all of the formatting codes.
  local strippedGS = string.gsub(_G[globalStringName], "%%%d?%$?[sd]", "")

  -- Count how many times each word appears in the global string.
  for word in string_gmatch(strippedGS, "%w+") do
   wordCounts[word] = (wordCounts[word] or 0) + 1
  end
 end


 -- Loop through all of the supported global strings.
 for globalStringName in pairs(searchCaptureFuncs) do
  local leastSeen, rarestWord

  -- Strip out all of the formatting codes.
  local strippedGS = string.gsub(_G[globalStringName], "%%%d?%$?[sd]", "")

  -- Find the rarest word in the global string.
  for word in string_gmatch(strippedGS, "%w+") do
   if (not leastSeen or wordCounts[word] < leastSeen) then
    leastSeen = wordCounts[word]
    rarestWord = word
   end
  end

  -- Set the rarest word.
  rareWords[globalStringName] = rarestWord
 end
end


-- ****************************************************************************
-- Validates rare words to make sure there are no oddities caused by various
-- languages. 
-- ****************************************************************************
local function ValidateRareWords()
 -- Loop through all of the global strings there is a rare word entry for.
 for globalStringName, rareWord in pairs(rareWords) do
  -- Remove the entry if the rare word isn't found in the associated global string.
  if (not string_find(_G[globalStringName], rareWord, 1, true)) then
   rareWords[globalStringName] = nil
  end
 end
end


-- ****************************************************************************
-- Converts all of the supported global strings.
-- ****************************************************************************
local function ConvertGlobalStrings()
 -- Loop through all of the supported global strings.
 for globalStringName in pairs(searchCaptureFuncs) do
  -- Get the global string converted to a lua search pattern and prepend an anchor to
  -- speed up searching.
  searchPatterns[globalStringName] = "^" .. ConvertGlobalString(globalStringName)
 end
end


-- ****************************************************************************
-- Creates a map of capture functions for each supported combat log event.
-- ****************************************************************************
local function CreateCaptureFuncs()
 captureFuncs = {
  -- Damage events.
  SWING_DAMAGE = function (p, ...) p.eventType, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", ... end,
  RANGE_DAMAGE = function (p, ...) p.eventType, p.isRange, p.skillID, p.skillName, p.skillSchool, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", true, ... end,
  SPELL_DAMAGE = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", ... end,
  SPELL_PERIODIC_DAMAGE = function (p, ...) p.eventType, p.isDoT, p.skillID, p.skillName, p.skillSchool, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", true, ... end,
  SPELL_BUILDING_DAMAGE = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", ... end,
  DAMAGE_SHIELD = function (p, ...) p.eventType, p.isDamageShield, p.skillID, p.skillName, p.skillSchool, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "damage", true, ... end,

  -- Miss events.
  SWING_MISSED = function (p, ...) p.eventType, p.missType, p.amount = "miss", ... end,
  RANGE_MISSED = function (p, ...) p.eventType, p.isRange, p.skillID, p.skillName, p.skillSchool, p.missType, p.amount = "miss", true, ... end,
  SPELL_MISSED = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.missType, p.amount = "miss", ... end,
  DAMAGE_SHIELD_MISSED = function (p, ...) p.eventType, p.isDamageShield, p.skillID, p.skillName, p.skillSchool, p.missType, p.amount = "miss", true, ... end,
  SPELL_DISPEL_FAILED = function (p, ...) p.eventType, p.missType, p.skillID, p.skillName, p.skillSchool, p.extraSkillID, p.extraSkillName, p.extraSkillSchool = "miss", "RESIST", ... end,

  -- Heal events.
  SPELL_HEAL = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.amount, p.overhealAmount, p.absorbAmount, p.isCrit = "heal", ... end,
  SPELL_PERIODIC_HEAL = function (p, ...) p.eventType, p.isHoT, p.skillID, p.skillName, p.skillSchool, p.amount, p.overhealAmount, p.absorbAmount, p.isCrit = "heal", true, ... end,
  
  -- Environmental events.
  ENVIRONMENTAL_DAMAGE = function (p, ...) p.eventType, p.hazardType, p.amount, p.overkillAmount, p.damageType, p.resistAmount, p.blockAmount, p.absorbAmount, p.isCrit, p.isGlancing, p.isCrushing = "environmental", ... end,

  -- Power events.
  SPELL_ENERGIZE = function (p, ...) p.eventType, p.isGain, p.skillID, p.skillName, p.skillSchool, p.amount, p.powerType = "power", true, ... end,
  SPELL_DRAIN = function (p, ...) p.eventType, p.isDrain, p.skillID, p.skillName, p.skillSchool, p.amount, p.powerType, p.extraAmount = "power", true, ... end,
  SPELL_LEECH = function (p, ...) p.eventType, p.isLeech, p.skillID, p.skillName, p.skillSchool, p.amount, p.powerType, p.extraAmount = "power", true, ... end,

  -- Interrupt events.
  SPELL_INTERRUPT = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.extraSkillID, p.extraSkillName, p.extraSkillSchool = "interrupt", ... end,
  
  -- Aura events.
  SPELL_AURA_APPLIED = function (p, ...) p.eventType, p.amount, p.skillID, p.skillName, p.skillSchool, p.auraType = "aura", 1, ... end,
  SPELL_AURA_APPLIED_DOSE = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.auraType, p.amount = "aura", ... end,
  SPELL_AURA_REMOVED = function (p, ...) p.eventType, p.isFade, p.amount, p.skillID, p.skillName, p.skillSchool, p.auraType = "aura", true, 1, ... end,
  SPELL_AURA_REMOVED_DOSE = function (p, ...) p.eventType, p.isFade, p.skillID, p.skillName, p.skillSchool, p.auraType, p.amount = "aura", true, ... end,

  -- Enchant events.
  ENCHANT_APPLIED = function (p, ...) p.eventType, p.skillName, p.itemID, p.itemName = "enchant", ... end,
  ENCHANT_REMOVED = function (p, ...) p.eventType, p.isFade, p.skillName, p.itemID, p.itemName = "enchant", true, ... end,
  
  -- Dispel events.
  SPELL_DISPEL = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.extraSkillID, p.extraSkillName, p.extraSkillSchool, p.auraType = "dispel", ... end,

  -- Cast events.
  SPELL_CAST_START = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool = "cast", ... end,

  -- Kill events.
  PARTY_KILL = function (p, ...) p.eventType = "kill" end,
  
  -- Extra Attack events.
  SPELL_EXTRA_ATTACKS = function (p, ...) p.eventType, p.skillID, p.skillName, p.skillSchool, p.amount = "extraattacks", ... end,
 }

 captureFuncs["DAMAGE_SPLIT"] = captureFuncs["SPELL_DAMAGE"]
 captureFuncs["SPELL_PERIODIC_MISSED"] = captureFuncs["SPELL_MISSED"]
 captureFuncs["SPELL_PERIODIC_ENERGIZE"] = captureFuncs["SPELL_ENERGIZE"]
 captureFuncs["SPELL_PERIODIC_DRAIN"] = captureFuncs["SPELL_DRAIN"]
 captureFuncs["SPELL_PERIODIC_LEECH"] = captureFuncs["SPELL_LEECH"]
 captureFuncs["SPELL_STOLEN"] = captureFuncs["SPELL_DISPEL"]

 -- Expose the capture functions.
 module.captureFuncs = captureFuncs
end


-------------------------------------------------------------------------------
-- Event handlers.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Called when there is information that needs to be obtained after a delay.
-- ****************************************************************************
local function OnUpdateDelayedInfo(this, elapsed)
 -- Check if the unit map needs to be updated after a delay.
 if (isUnitMapStale) then
  -- Increment the amount of time passed since the last update.
  lastUnitMapUpdate = lastUnitMapUpdate + elapsed

  -- Check if it's time for an update.
  if (lastUnitMapUpdate >= UNIT_MAP_UPDATE_DELAY) then
   -- Update the player GUID if it isn't known yet and verify it's now known.
   if (not playerGUID) then playerGUID = UnitGUID("player") end
   if (playerGUID) then
    -- Erase the unit map table and mark all old units for cleanup from the class map.
    local now = GetTime()
    for guid in pairs(unitMap) do
     unitMap[guid] = nil
     classTimes[guid] = now + CLASS_HOLD_TIME
    end

    -- Check if there are raid members.
    local numRaidMembers = GetNumRaidMembers()
    if (numRaidMembers > 0) then
     -- Loop through all of the raid members and add them and their class to the maps.
     for i = 1, numRaidMembers do
      local unitID = "raid" .. i
      local guid = UnitGUID(unitID)
      unitMap[guid] = unitID
      if (not classMap[guid]) then _, classMap[guid] = UnitClass(unitID) end
      classTimes[guid] = nil
     end -- Loop through raid members

    -- There are not any raid members so look for party members.
    else
     -- Loop through all of the party members and add their class to the maps.
     for i = 1, GetNumPartyMembers() do
      local unitID = "party" .. i
      local guid = UnitGUID(unitID)
      unitMap[guid] = unitID
      if (not classMap[guid]) then _, classMap[guid] = UnitClass(unitID) end
      classTimes[guid] = nil
     end -- Loop through party members
    end

    -- Add the player and player's class to the maps.
    unitMap[playerGUID] = "player"
    if (not classMap[playerGUID]) then _, classMap[playerGUID] = UnitClass("player") end
    classTimes[playerGUID] = nil
   
    -- Clear the unit map stale flag.
    isUnitMapStale = false
   end

   -- Reset the time since last update.
   lastUnitMapUpdate = 0
  end
 end -- Unit map is stale.

 -- Check if the pet map needs to be updated after a delay.
 if (isPetMapStale) then
  -- Increment the amount of time passed since the last update.
  lastPetMapUpdate = lastPetMapUpdate + elapsed
  
  -- Check if it's time for an update.
  if (lastPetMapUpdate >= PET_UPDATE_DELAY) then
   -- Verify the player's pet is not in an unknown state if there is one.
   local petName = UnitName("pet")
   if (not petName or petName ~= UNKNOWN) then
    -- Erase the pet map table and mark all old units for cleanup from the class map.
    local now = GetTime()
    for guid in pairs(petMap) do
     petMap[guid] = nil
     classTimes[guid] = now + CLASS_HOLD_TIME
    end

    -- Check if there are raid members.
    local numRaidMembers = GetNumRaidMembers()
    if (numRaidMembers > 0) then
     -- Loop through all of the raid members and add their pets and pet's class to the maps.
     for i = 1, numRaidMembers do
      local unitID = "raidpet" .. i
      if (UnitExists(unitID)) then
       local guid = UnitGUID(unitID)
       petMap[guid] = unitID
       if (not classMap[guid]) then _, classMap[guid] = UnitClass(unitID) end
       classTimes[guid] = nil
      end
     end -- Loop through raid members

    -- There are not any raid members so look for party members.
    else
     -- Loop through all of the party members and add their pets and pet's class to the maps.
     for i = 1, GetNumPartyMembers() do
      local unitID = "partypet" .. i
      if (UnitExists(unitID)) then
       local guid = UnitGUID(unitID)
       petMap[guid] = unitID
       if (not classMap[guid]) then _, classMap[guid] = UnitClass(unitID) end
       classTimes[guid] = nil
      end
     end -- Loop through party members
    end

    -- Add the player's pet and its class if there is one.  Treat vehicles as the player instead of a pet.
    if (petName) then
     local unitID = "pet"
     local guid = UnitGUID(unitID)
     if (guid == UnitGUID("vehicle")) then unitID = "player" end
     petMap[guid] = unitID
     if (not classMap[guid]) then _, classMap[guid] = UnitClass(unitID) end
     classTimes[guid] = nil
    end

    -- Clear the pet map stale flag.
    isPetMapStale = false
   end -- Pet in known state.

   -- Reset the time since last update.
   lastPetMapUpdate = 0
  end
 end -- Pet map is stale.

 -- Stop receiving updates if no more data needs to be updated.
 if (not isUnitMapStale and not isPetMapStale) then this:Hide() end
end


-- ****************************************************************************
-- Called when the events the parser registered for occur.
-- ****************************************************************************
local function OnEvent(this, event, arg1, arg2, ...)
 -- Combat log events.
 if (event == "COMBAT_LOG_EVENT_UNFILTERED") then
  ParseLogMessage(arg1, arg2, ...)

 -- Mouseover changes.
 elseif (event == "UPDATE_MOUSEOVER_UNIT") then
  -- Map the GUID for the moused over unit to a class.
  local mouseoverGUID = UnitGUID("mouseover")
  if (not mouseoverGUID) then return end

  -- Ignore the GUID if its class is already known and there is no cleanup time for it.
  if (classMap[mouseoverGUID] and not classTimes[mouseoverGUID]) then return end

  -- Update the cleanup time for the GUID and map it to a class if it's not already known.
  classTimes[mouseoverGUID] = GetTime() + CLASS_HOLD_TIME
  if (not classMap[mouseoverGUID]) then _, classMap[mouseoverGUID] = UnitClass("mouseover") end

 -- Target changes.
 elseif (event == "PLAYER_TARGET_CHANGED") then
  -- Map the GUID for the target unit to a class.
  local targetGUID = UnitGUID("target")
  if (not targetGUID) then return end

  -- Ignore the GUID if its class is already known and there is no cleanup time for it.
  if (classMap[targetGUID] and not classTimes[targetGUID]) then return end

  -- Update the cleanup time for the GUID and map it to a class if it's not already known.
  local now = GetTime()
  classTimes[targetGUID] = now + CLASS_HOLD_TIME
  if (not classMap[targetGUID]) then _, classMap[targetGUID] = UnitClass("target") end

  -- Loop through all of the recent guid to class mappings and remove the old ones if enough time has passed.
  if (now >= classMapCleanupTime) then
   for guid, cleanupTime in pairs(classTimes) do
    if (now >= cleanupTime) then classMap[guid] = nil classTimes[guid] = nil end
   end

   classMapCleanupTime = now + CLASS_HOLD_TIME
  end -- Time to clean up class map.

 -- Party/Raid changes.
 elseif (event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE") then
  -- Set the unit map stale flag and schedule the unit map to be updated after a short delay.
  isUnitMapStale = true
  eventFrame:Show()

 -- Pet changes.
 elseif (event == "UNIT_PET") then
  isPetMapStale = true
  eventFrame:Show()

 -- Arena opponent changes.
 elseif (event == "ARENA_OPPONENT_UPDATE") then
  -- Map the unit id and GUID for an arena unit to a class when it's seen.
  if (arg2 == "seen") then
   local arenaGUID = UnitGUID(arg1)
   if (not arenaGUID) then return end
   arenaUnits[arg1] = arenaGUID
   _, classMap[arenaGUID] = UnitClass(arg1)

  -- Remove the mappings for an arena unit when it's cleared.
  elseif (arg2 == "cleared") then
   local arenaGUID = arenaUnits[arg1]
   if (not arenaGUID) then return end
   arenaUnits[arg1] = nil
   classMap[arenaGUID] = nil
  end

 -- Chat message combat events.
 else
  ParseSearchMessage(event, arg1)
 end
end


-- ****************************************************************************
-- Enables parsing.
-- ****************************************************************************
local function Enable()
 -- Register for parameter style events going to the combat log.
 eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
 
 -- Register CHAT_MSG_X search style events.
 for event in pairs(searchMap) do
  eventFrame:RegisterEvent(event)
 end

 -- Register additional events for unit and class map processing.
 eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
 eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
 eventFrame:RegisterEvent("UNIT_PET") 
 eventFrame:RegisterEvent("ARENA_OPPONENT_UPDATE")
 eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
 eventFrame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")

 -- Update the unit map and current pet information.
 isUnitMapStale = true
 isPetNameStale = true

 -- Start receiving updates.
 eventFrame:Show()
end


-- ****************************************************************************
-- Disables the parsing.
-- ****************************************************************************
local function Disable()
 -- Stop receiving updates.
 eventFrame:Hide()
 eventFrame:UnregisterAllEvents()
 
 -- Erase the reflected skill tables.
 EraseTable(reflectedTimes)
 EraseTable(reflectedSkills)
end


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

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

-- Get the name, GUID, and class of the player.
playerName = UnitName("player")
playerGUID = UnitGUID("player")
 
-- Create various maps.
CreateSearchMap()
CreateSearchCaptureFuncs()
CreateCaptureFuncs()
 
-- Create the list of events that should be fully parsed.
CreateFullParseList()

-- Find the rarest word for each supported global string.
FindRareWords()
ValidateRareWords()

-- Convert the supported global strings into lua search patterns.
ConvertGlobalStrings()




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

-- Protected Constants.
module.AFFILIATION_MINE		= AFFILIATION_MINE
module.AFFILIATION_PARTY	= AFFILIATION_PARTY
module.AFFILIATION_RAID		= AFFILIATION_RAID
module.AFFILIATION_OUTSIDER	= AFFILIATION_OUTSIDER
module.REACTION_FRIENDLY	= REACTION_FRIENDLY
module.REACTION_NEUTRAL		= REACTION_NEUTRAL
module.REACTION_HOSTILE		= REACTION_HOSTILE
module.CONTROL_HUMAN		= CONTROL_HUMAN
module.CONTROL_SERVER		= CONTROL_SERVER
module.UNITTYPE_PLAYER		= UNITTYPE_PLAYER
module.UNITTYPE_NPC			= UNITTYPE_NPC
module.UNITTYPE_PET			= UNITTYPE_PET
module.UNITTYPE_GUARDIAN	= UNITTYPE_GUARDIAN
module.UNITTYPE_OBJECT		= UNITTYPE_OBJECT
module.TARGET_TARGET		= TARGET_TARGET
module.TARGET_FOCUS			= TARGET_FOCUS
module.OBJECT_NONE			= OBJECT_NONE

-- Protected Variables.
module.unitMap = unitMap
module.classMap = classMap

-- Protected Functions.
module.RegisterHandler				= RegisterHandler
module.UnregisterHandler			= UnregisterHandler
module.TestFlagsAny					= TestFlagsAny
module.TestFlagsAll					= TestFlagsAll
module.Enable						= Enable
module.Disable						= Disable