local ADDON_NAME, Obituary = ...
LibStub("AceAddon-3.0"):NewAddon(Obituary, ADDON_NAME, "AceEvent-3.0", "AceTimer-3.0", "AceHook-3.0")
_G.Obituary = Obituary

local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local Stack = Obituary.Stack
local CombatLogHandler = CreateFrame("Frame")

local db

local _G = _G
local wipe, type, pairs, ipairs, tostring, select = wipe, type, pairs, ipairs, tostring, select
local format, strsub, strlen, strsplit = string.format, string.sub, string.len, strsplit
local floor, max, min = math.floor, math.max, math.min
local bit_bor, bit_band = bit.bor, bit.band

local UnitExists = UnitExists
local UnitGUID = UnitGUID
local UnitIsFeignDeath = UnitIsFeignDeath
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local GetSpellLink = GetSpellLink
local GetSpellInfo = GetSpellInfo
local UnitBuff = UnitBuff
local IsInGuild = IsInGuild
local GetNumSubgroupMembers = GetNumSubgroupMembers
local GetNumGroupMembers = GetNumGroupMembers
local UnitClass = UnitClass
local UnitIsCharmed = UnitIsCharmed
local GetInstanceInfo = GetInstanceInfo
local GetTime = GetTime
local IsShiftKeyDown = IsShiftKeyDown

local COMBATLOG_OBJECT_AFFILIATION_OUTSIDER = COMBATLOG_OBJECT_AFFILIATION_OUTSIDER
local COMBATLOG_OBJECT_TYPE_PLAYER = COMBATLOG_OBJECT_TYPE_PLAYER
local COMBATLOG_OBJECT_TYPE_PET = COMBATLOG_OBJECT_TYPE_PET
local COMBATLOG_OBJECT_TYPE_NPC = COMBATLOG_OBJECT_TYPE_NPC
local COMBATLOG_OBJECT_REACTION_HOSTILE = COMBATLOG_OBJECT_REACTION_HOSTILE
local COMBATLOG_OBJECT_REACTION_FRIENDLY = COMBATLOG_OBJECT_REACTION_FRIENDLY
local COMBATLOG_OBJECT_REACTION_NEUTRAL = COMBATLOG_OBJECT_REACTION_NEUTRAL
local COMBATLOG_OBJECT_RAIDTARGET_MASK = COMBATLOG_OBJECT_RAIDTARGET_MASK
local COMBATLOG_OBJECT_RAIDTARGET1 = COMBATLOG_OBJECT_RAIDTARGET1
local TEXT_MODE_A_STRING_SOURCE_ICON = TEXT_MODE_A_STRING_SOURCE_ICON
local TEXT_MODE_A_STRING_POSSESSIVE = TEXT_MODE_A_STRING_POSSESSIVE

local UNKNOWN = UNKNOWN
local ACTION_SWING = ACTION_SWING
local ACTION_RANGED = ACTION_RANGED
local ACTION_SWING_MISSED = ACTION_SWING_MISSED

local AURA_TYPE_BUFF = "BUFF"
local AURA_TYPE_DEBUFF = "DEBUFF"

local EVENT_TYPE_DAMAGE = 1
local EVENT_TYPE_HEAL = 2
local EVENT_TYPE_AURA_APPLIED = 3
local EVENT_TYPE_AURA_REMOVED = 4

local THROTTLE_THRESHOLD = 3
local THROTTLE_TIME_WINDOW = 3
local THROTTLE_TIMEOUT = 10


local buffCache = {
	[5384] = 360, -- Feign Death
	[2825] = 40,  -- Bloodlust
	[32182] = 40, -- Heroism
	[80353] = 40, -- Time Warp
	[90355] = 40, -- Ancient Hysteria

	--stfu
	[48107] = false, -- Mage Heating Up
	[48108] = false, -- Mage Pyroblast!
}
Obituary.buffCache = buffCache

local nameToGUID = {}
Obituary.nameToGUID = nameToGUID

local deathEventHistory = {}
function Obituary:PurgeDeathEventHistories()
	wipe(deathEventHistory)
end
Obituary.deathEventHistory = deathEventHistory

local deathReport = {}
function Obituary:PurgeDeathReports()
	wipe(deathReport)
	wipe(nameToGUID)
end
Obituary.deathReport = deathReport

function Obituary:GetDeathReports(guid)
	return guid and deathReport[guid]
end

local ChatFrameList = {}
for i = 1, NUM_CHAT_WINDOWS do
	ChatFrameList[i] = _G["ChatFrame" .. i]
end

-- Filter "Player has died." when death links are enabled
do
	local pattern = "^"..ERR_PLAYER_DIED_S:gsub("%%s", "(.-)")
	ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", function(self, event, msg)
		return db.detailed and msg:find(pattern)
	end)
end

-- Check which ChatFrames would show "Player has died." and print to each
local function sysprint(msg)
	local info = ChatTypeInfo["SYSTEM"]
	for _, frame in ipairs(ChatFrameList) do
		for _, msgType in ipairs(frame.messageTypeList) do
			if msgType == "SYSTEM" then
				frame:AddMessage(msg, info.r, info.g, info.b, info.id)
				break
			end
		end
	end
end


-- Pet ownership shenanigans (code adapted from Skada, because I'm terribly lazy)
local pets = {}
do
	local function checkPet(unit, pet)
		local petGUID = UnitGUID(pet)
		local unitGUID = UnitGUID(unit)

		-- Add to pets if it does not already exist.
		if petGUID and unitGUID and not pets[petGUID] then
			pets[petGUID] = unitGUID
		end
	end

	function Obituary:CheckPets()
		if not db.enableForPets then return end

		if IsInRaid() then
			for i = 1, GetNumGroupMembers() do
				local pet = format("raid%dpet", i)
				if UnitExists(pet) then
					checkPet("raid"..i, pet)
				end
			end
		elseif IsInGroup() then
			for i = 1, GetNumSubgroupMembers() do
				local pet = format("party%dpet", i)
				if UnitExists(pet) then
					checkPet("party"..i, pet)
				end
			end
		end

		if UnitExists("pet") then
			checkPet("player", "pet")
		end
	end
end


do --CLEU handler
	local feignBuff = GetSpellInfo(5384)
	local redemptionDeaths = {}

	local combatLogMap = {
		["SPELL_DAMAGE"] = "SPELL_DAMAGE",
		["SPELL_MISSED"] = "SPELL_MISSED",

		["SPELL_PERIODIC_DAMAGE"] = "SPELL_DAMAGE",
		["SPELL_PERIODIC_MISSED"] = "SPELL_MISSED",

		["DAMAGE_SHIELD"] = "SPELL_DAMAGE",
		["DAMAGE_SPLIT"] = "SPELL_DAMAGE",
		["DAMAGE_SHIELD_MISSED"] = "SPELL_MISSED",

		["RANGE_DAMAGE"] = "SPELL_DAMAGE",
		["RANGE_MISSED"] = "RANGE_MISSED",

		["SWING_DAMAGE"] = "SWING_DAMAGE",
		["SWING_MISSED"] = "SWING_MISSED",

		["ENVIRONMENTAL_DAMAGE"] = "ENVIRONMENTAL_DAMAGE",

		["SPELL_HEAL"] = "SPELL_HEAL",
		["SPELL_PERIODIC_HEAL"] = "SPELL_PERIODIC_HEAL",

		["SPELL_AURA_APPLIED"] = "SPELL_AURA_APPLIED",
		["SPELL_AURA_APPLIED_DOSE"] = "SPELL_AURA_APPLIED",
		["SPELL_AURA_AURA_REFRESH"] = "SPELL_AURA_APPLIED",
		["SPELL_AURA_REMOVED"] = "SPELL_AURA_REMOVED",
		["SPELL_AURA_REMOVED_DOSE"] = "SPELL_AURA_REMOVED",
		["SPELL_AURA_BROKEN"] = "SPELL_AURA_REMOVED",

		["SPELL_AURA_BROKEN_SPELL"] = "SPELL_AURA_REMOVED",
		["SPELL_DISPEL"] = "SPELL_DISPEL",
		["SPELL_STOLEN"] = "SPELL_STOLEN",

		["SPELL_INSTAKILL"] = "SPELL_INSTAKILL",
	}

	local function checkAffiliation(flags, guid)
		if bit_band(flags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0 then -- players
			if bit_band(flags, COMBATLOG_OBJECT_AFFILIATION_OUTSIDER) ~= 0 then -- not in group
				return db.enableForPlayersNotInGroup
			--elseif bit_band(flags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 then
			--	return db.enableForPlayer
			else
				return db.enableForPlayersInGroup
			end
		else -- not a player
			if bit_band(flags, COMBATLOG_OBJECT_TYPE_PET) ~= 0 then -- pet
				if db.enableForPets then
					return db.enableForPlayersInGroup and pets[guid] or db.enableForPlayersNotInGroup
				end
				return false
			elseif bit_band(flags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 then -- npc
				return db.enableForNPCs
			end
		end
		return false --COMBATLOG_OBJECT_TYPE_GUARDIAN, COMBATLOG_OBJECT_TYPE_OBJECT and 0 get this far
	end

	CombatLogHandler:SetScript("OnEvent", function(_, _, timestamp, event, hideCaster, srcGUID, srcName, srcFlags, srcRaidFlags, dstGUID, dstName, dstFlags, dstRaidFlags, ...)
		if event == "UNIT_DIED" then
			if redemptionDeaths[dstGUID] then
				redemptionDeaths[dstGUID] = nil
			elseif deathEventHistory[dstGUID] and not UnitIsFeignDeath(dstName) and not UnitBuff(dstName, feignBuff) then
				-- mobs that were mc'd then released don't need death logs
				if checkAffiliation(dstFlags, dstGUID) then
					local maxHealth = UnitHealthMax(dstName)
					Stack.push(deathEventHistory[dstGUID], {
						type = 0,
						timestamp = timestamp,
						srcName = dstName,
						curHealth = maxHealth > 0 and UnitHealth(dstName) or nil,
						maxHealth = maxHealth > 0 and maxHealth or nil,
					})
					Obituary:ReportDeath(timestamp, dstGUID, dstName, dstFlags, dstRaidFlags)
				else
					deathEventHistory[dstGUID] = nil
				end
			end
			return
		end

		local e = combatLogMap[event]
		if e and (deathEventHistory[dstGUID] or (not UnitIsCharmed(dstGUID) and checkAffiliation(dstFlags, dstGUID))) then
			local spellId, spellName, spellSchool, auraType
			local amount, overkill, resisted, blocked, absorbed, isCrit, isGlancing, isCrush
			local missType, amountMissed
			local eventType

			if e == "SPELL_AURA_APPLIED" then
				spellId, spellName, spellSchool, auraType, amount = ...
				eventType = EVENT_TYPE_AURA_APPLIED

				-- report the death of a holy priest when they gain spirit of redemption and ignore the death event 15s later
				if spellId == 27827 then
					redemptionDeaths[dstGUID] = true
					local maxHealth = UnitHealthMax(dstName)
					Stack.push(deathEventHistory[dstGUID], {
						type = 0,
						timestamp = timestamp,
						srcName = dstName,
						curHealth = maxHealth > 0 and UnitHealth(dstName) or nil,
						maxHealth = maxHealth > 0 and maxHealth or nil,
					})
					-- get the killing event (SPELL_MISS)
					Obituary:ScheduleTimer("ReportDeath", 0.1, timestamp, dstGUID, dstName, dstFlags, dstRaidFlags)
				end
				-- don't need to show the player's name for aura gains/loses in the log
				if srcGUID == dstGUID then
					hideCaster = true
				end

			elseif e == "SPELL_AURA_REMOVED" then
				spellId, spellName, spellSchool, auraType, amount = ...
				eventType = EVENT_TYPE_AURA_REMOVED

				-- don't need to show the player's name for aura gains/loses in the log
				if srcGUID == dstGUID then
					hideCaster = true
				end

			elseif e == "SWING_DAMAGE" then
				spellName = ACTION_SWING
				amount, overkill, _, resisted, blocked, absorbed, isCrit, isGlancing, isCrush = ...
				eventType = EVENT_TYPE_DAMAGE

			elseif e == "SPELL_DAMAGE" then
				spellId, spellName, spellSchool, amount, overkill, _, resisted, blocked, absorbed, isCrit, isGlancing, isCrush = ...
				eventType = EVENT_TYPE_DAMAGE

			elseif e == "SPELL_HEAL" then
				spellId, spellName, spellSchool, amount, overkill, absorbed, isCrit = ...
				eventType = EVENT_TYPE_HEAL

			elseif e == "SPELL_PERIODIC_HEAL" then
				spellId, spellName, spellSchool, amount, overkill, absorbed, isCrit = ...
				eventType = EVENT_TYPE_HEAL

			elseif e == "RANGE_DAMAGE" then
				--spellId, spellName, spellSchool = ...
				spellName = ACTION_RANGED
				amount, overkill, _, resisted, blocked, absorbed, isCrit, isGlancing, isCrush = select(4, ...)
				eventType = EVENT_TYPE_DAMAGE

			elseif e == "SPELL_MISSED" then
				spellId, spellName, spellSchool, missType, _, amountMissed = ...

				-- mark full absorbs as damage for dead holy priests
				if redemptionDeaths[dstGUID] then
					eventType = EVENT_TYPE_DAMAGE
				end

			elseif e == "SWING_MISSED" then
				spellName = ACTION_SWING
				missType, _, amountMissed = ...

			elseif e == "RANGE_MISSED" then
				--spellId, spellName, spellSchool = ...
				spellName = ACTION_RANGED
				missType, _, amountMissed = select(4, ...)

			elseif e == "ENVIRONMENTAL_DAMAGE" then
				spellName, amount, overkill, _, resisted, blocked, absorbed, isCrit, isGlancing, isCrush = ...
				--spellName = _G["ACTION_ENVIRONMENTAL_DAMAGE_"..spellName]
				srcFlags = bit_bor(srcFlags, COMBATLOG_OBJECT_REACTION_NEUTRAL) -- for coloring (yellow)
				eventType = EVENT_TYPE_DAMAGE

			elseif e == "SPELL_DISPEL" then
				_, _, _, spellId, spellName, spellSchool, auraType = ...
				eventType = EVENT_TYPE_AURA_REMOVED

			elseif e == "SPELL_STOLEN" then
				_, _, _, spellId, spellName, spellSchool, auraType = ...
				eventType = EVENT_TYPE_AURA_APPLIED

			elseif e == "SPELL_INSTAKILL" then
				spellId, spellName, spellSchool = ...
				amount = -1
				eventType = EVENT_TYPE_DAMAGE

			else
				print("Got \""..event.."\" as \""..e.."\" but don't know what to do with it!", ...)
				return

			end

			-- Only care about hits if all we're showing is the summary
			if eventType ~= EVENT_TYPE_DAMAGE and not db.detailed then return end

			if type(amount) ~= "number" then amount = 0 end

			-- Do filtering
			local totalAmount = amount + (overkill or 0) + (resisted or 0) + (blocked or 0) + (absorbed or 0)
			if eventType == EVENT_TYPE_DAMAGE and totalAmount <= db.filterDamage then return end
			if eventType == EVENT_TYPE_HEAL and totalAmount <= db.filterHeal then return end
			if auraType == AURA_TYPE_BUFF then
				if not db.showBuff or buffCache[spellId] == false then return end
				if eventType == EVENT_TYPE_AURA_APPLIED then
					if not buffCache[spellId] then
						local _, _, _, _, _, duration = UnitBuff(dstName, spellName)
						if not duration or duration < 5 or duration > 30 then return end
						buffCache[spellId] = duration
					end
				elseif not buffCache[spellId] then
					return
				end
			elseif auraType == AURA_TYPE_DEBUFF and not db.showDebuff then return end

			-- If the unit is in our group, we can show health values in its report
			local curHealth, maxHealth = UnitHealth(dstName), UnitHealthMax(dstName)
			if maxHealth == 0 then
				curHealth = nil
				maxHealth = nil
			end

			-- Ensure that the table is properly initialized
			if not deathEventHistory[dstGUID] then
				deathEventHistory[dstGUID] = Stack.new(db.detailed and db.detailedReportEvents or 10)
			end

			if srcName == "" and not hideCaster then
				srcName = UNKNOWN
			end

			-- Store it!
			Stack.push(deathEventHistory[dstGUID], {
				type = eventType,
				timestamp = timestamp,
				hideCaster = hideCaster,
				srcName = srcName or L["Environment"],
				srcFlags = srcFlags,
				srcRaidFlags = srcRaidFlags,
				spellId = spellId,
				spellName = spellName,
				auraType = auraType,
				amount = amount,
				overkill = overkill,
				resisted = resisted,
				blocked = blocked,
				absorbed = absorbed,
				isCrit = isCrit,
				isGlancing = isGlancing,
				isCrush = isCrush,
				miss = missType,
				missAmount = amountMissed,
				curHealth = curHealth,
				maxHealth = maxHealth,
			})
		end
	end)
end


local function GetRaidTargetIcon(flags, localOutput)
	local raidTarget = bit_band(flags, COMBATLOG_OBJECT_RAIDTARGET_MASK)
	if raidTarget ~= 0 then
		for i = 1, 8 do
			local mask = COMBATLOG_OBJECT_RAIDTARGET1 * (2 ^ (i - 1))
			if raidTarget == mask then
				if localOutput then
					return format(TEXT_MODE_A_STRING_SOURCE_ICON, mask, _G["COMBATLOG_ICON_RAIDTARGET"..i])
				else
					return format("{rt%d}", i)
				end
			end
		end
	end
	return ""
end

local function GetColoredName(name, flags)
	if bit_band(flags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0 then -- player
		local class = select(2, UnitClass(name))
		local classColor = class and RAID_CLASS_COLORS[class]
		if classColor then
			return format("|cff%02x%02x%02x%s|r", classColor.r * 255, classColor.g * 255, classColor.b * 255, name)
		end
		return format("|cff888888%s|r", name) -- grey ...maybe I should store some more meta data about entries?

	else -- npc
		if bit_band(flags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0 then
			return format("|cffff0000%s|r", name) -- red
		elseif bit_band(flags, COMBATLOG_OBJECT_REACTION_FRIENDLY) ~= 0 then
			return format("|cff00ff00%s|r", name) -- green
		elseif bit_band(flags, COMBATLOG_OBJECT_REACTION_NEUTRAL) ~= 0 then
			return format("|cffffff00%s|r", name) -- yellow
		end
	end

	return format("|cffffffff%s|r", name) -- white
end

local function DecorateName(name, flags, raidFlags, localOutput)
	if localOutput then
		name = GetColoredName(name, flags)
	end
	return GetRaidTargetIcon(raidFlags, localOutput) .. name
end

local defaultChannels = Obituary.defaultChannels
local function SendMessage(msg, channel)
	if type(channel) == "table" then -- channel is a chat frame
		channel:AddMessage(msg)

	elseif defaultChannels[channel] then -- channel is a default chat type (such as "RAID", "GUILD", etc.)
		-- fail if not in a guild if trying to send to guild
		if (channel == "GUILD" or channel == "OFFICER") and not IsInGuild() then return end
		-- fall back to party chat if we're not in a raid if trying to send to raid
		if channel == "RAID" and not IsInRaid() then channel = "PARTY" end
		-- fail if we're not in a party if trying to send to party
		if channel == "PARTY" and not IsInGroup() then return end
		-- switch to instance chat if in a LFG instance
		if (channel == "RAID" or channel == "PARTY") and IsPartyLFG() then return end --channel = "INSTANCE_CHAT"

		--SendChatMessage(msg, channel)
		ChatThrottleLib:SendChatMessage("NORMAL", "obit", msg, channel)

	else -- channel is a custom channel
		--SendChatMessage(msg, "CHANNEL", nil, channel)
		ChatThrottleLib:SendChatMessage("NORMAL", "obit", msg, "CHANNEL", nil, channel)

	end
end


local function SummarizeDeathReport(timestamp, report, name, flags, raidFlags)
	-- Not sure how we got here without some sort of valid report, but if we don't..
	if not report then return end

	-- We're only interested in recent damage events
	local damageEvents = Stack.new(db.summaryReportEvents)
	for event in Stack.next(report) do
		if event.type == EVENT_TYPE_DAMAGE and (timestamp-event.timestamp < 6) then
			Stack.push(damageEvents, event)
		end
	end

	local localOutput = db.summaryOutput == "ChatFrame"
	local outputChannel = localOutput and ChatFrameList[db.summaryChatFrame] or db.summaryOutput

	-- We don't have any damage events to report
	if Stack.len(damageEvents) == 0 then
		SendMessage(format("%s died mysteriously.", DecorateName(name, flags, raidFlags, localOutput)), outputChannel)
		return
	end

	--Haven't gotten around to rewriting this, works well enough but its a nightmare to localize
	local reportHeader = format("%s was killed by ", DecorateName(name, flags, raidFlags, localOutput))
	local lastDamageSource
	local reportString = ""
	local sourceString = ""

	-- Iterate through the event history, generating the report:
	--		Yumiko was killed by Baradin Fox ([Snarl] for 3698, Melee for 7098 *crit*)
	--		Yumiko was killed by Ghastly Scavenger ([Throw] for 5397), Baradin Fox (Melee for 4175)
	for event in Stack.next(damageEvents) do
		local srcName = DecorateName(event.srcName, event.srcFlags, event.srcRaidFlags, localOutput)

		if lastDamageSource ~= srcName then
			if reportString ~= "" then
				reportString = format("%s), ", strsub(reportString, 1, -3))
			end
			lastDamageSource = srcName
			srcName = nil
			sourceString = format("%s (", lastDamageSource)
		end

		-- Format the event
		sourceString = format("%s%s%s%s%s, ",
			sourceString,
			event.spellId and GetSpellLink(event.spellId) or event.spellName or "",
			event.amount > 0 and format(" for %d", event.amount) or "",
			event.isCrit and " "..L["crit"] or "", -- use custom crit/crush text for more pizazz
			event.isCrush and " "..L["crush"] or ""
		)

		-- If the sum length of the report header, last damage source, report string,
		-- and source string exceed the max message length (255-ChatThrottleLib.MSG_OVERHEAD),
		-- send the current report
		if not localOutput and strlen(reportHeader) + strlen(lastDamageSource) + strlen(reportString) + strlen(sourceString) >= 215 then
			if srcName then
				reportString = format("%s%s)", reportHeader, strsub(reportString, 1, -3))
				sourceString = format("%s (%s", lastDamageSource, sourceString)
			else
				reportString = format("%s%s", reportHeader, strsub(reportString, 1, -3))
			end
			SendMessage(reportString, outputChannel)
			reportString = ""
		end
		reportString = reportString .. sourceString
		sourceString = ""
	end
	reportString = format("%s%s)", reportHeader, strsub(reportString, 1, -3))
	SendMessage(reportString, outputChannel)

	Stack.release(damageEvents)
end


do --Obituary:ReportDeath
	local recentDeaths = 0
	local recentDeathsLast = 0
	local recentDeathsCooldown = nil

	function Obituary:ReportDeath(timestamp, guid, name, flags, raidFlags)
		-- If we don't have any events stored (?!), there is nothing to report
		if not deathEventHistory[guid] or Stack.len(deathEventHistory[guid]) == 0 then return end

		--so i have an easy way to iterate by player
		nameToGUID[name] = guid

		-- Iterate through the event history, storing the events that haven't expired
		local lastDamageSource
		local report = Stack.new()
		for event in Stack.next(deathEventHistory[guid]) do
			local tdiff = timestamp - event.timestamp
			if tdiff <= db.eventTTL then
				Stack.push(report, event)
				--save the last thing that and isn't a heal as our cause of death
				--and filter out things that happened longer than 2s before the death
				if tdiff < 2 and event.type == EVENT_TYPE_DAMAGE then
					lastDamageSource = event.srcName
				end
			end
		end

		-- Output the death chat link and save the trimmed death log
		if db.detailed then
			-- Ensure that deathReport[guid] is initialized
			if not deathReport[guid] then
				deathReport[guid] = {}
			end
			deathReport[guid][tostring(timestamp)] = report

			-- Prune logs
			local temp = {}
			for ts in next, deathReport[guid] do
				temp[#temp+1] = ts
			end
			table.sort(temp)
			for i=#temp-db.detailedReportsPerPlayer, 1, -1 do
				deathReport[guid][tostring(temp[i])] = nil
			end

			-- lastDamageSource == nil really only happens when something is wonky, like phasing eating the combat log
			-- or because apparently no insta-kill effects actually use SPELL_INSTAKILL
			local msg = format("|Hobituary:%s:%s:%s:%s:%s|h[%s]|h", timestamp, guid, name, flags, raidFlags, L["%s was killed by %s."]:format(name, lastDamageSource or UNKNOWN))
			sysprint(msg)
		end

		-- Are we supposed to print a summarized death report?
		if db.summary then
			if db.throttle then
				-- don't show death summaries for x seconds if you get more than y deaths in z seconds to prevent wipe spam
				local now = GetTime()
				if recentDeathsCooldown and recentDeathsCooldown < now then
					recentDeathsCooldown = nil
					recentDeathsLast = 0
				end

				if not recentDeathsCooldown then
					if recentDeaths <= THROTTLE_THRESHOLD or
						now - recentDeathsLast > THROTTLE_TIME_WINDOW then
						if now - recentDeathsLast > THROTTLE_TIME_WINDOW then
							recentDeaths = 0
							recentDeathsLast = now
						end
						SummarizeDeathReport(timestamp, report, name, flags, raidFlags)
						recentDeaths = recentDeaths + 1
					else
						recentDeathsCooldown = now + THROTTLE_TIMEOUT
					end
				end
			else
				SummarizeDeathReport(timestamp, report, name, flags, raidFlags)
			end
		end

		-- Wipe the full event history for this unit since we no longer need it
		Stack.wipe(deathEventHistory[guid])

		--Release the report table back into the pool
		if not db.detailed then
			Stack.release(report)
		end

		if bit_band(flags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0 then
			self:SendMessage("Obituary_PlayerDeath", timestamp, guid, name, flags, raidFlags)
		end
	end
end

local function NumberShort(value)
	local fmt
	if value >= 10000000 or value <= -10000000 then
		fmt = "%.1fm"
		value = value / 1000000
	elseif value >= 1000000 or value <= -1000000 then
		fmt = "%.2fm"
		value = value / 1000000
	elseif value >= 100000 or value <= -100000 then
		fmt = "%.0fk"
		value = value / 1000
	elseif value >= 10000 or value <= -10000 then
		fmt = "%.1fk"
		value = value / 1000
	else
		fmt = "%d"
		value = floor(value + 0.5)
	end
	return fmt:format(value)
end

local function DeathEventString(event, timeOfDeath, localOutput)
	local health = event.maxHealth and format("%s/%s", NumberShort(event.maxHealth), NumberShort(event.curHealth))

	if event.type == 0 then
		return format("† %s dies †", event.srcName), format("-%.02fs", timeOfDeath - event.timestamp), health
	end

	local source
	if not event.hideCaster then
		--just set srcName to "" when hideCaster? leaving it like this for now so I don't have to muck around with summary
		source = format(TEXT_MODE_A_STRING_POSSESSIVE, DecorateName(event.srcName, event.srcFlags, event.srcRaidFlags, localOutput)).." "
	else
		source = ""
	end

	local spellLink = event.spellId and GetSpellLink(event.spellId)
	local spell = spellLink or event.spellName
	local eventType, missType, auraType = event.type, event.miss, event.auraType

	local eventString = ""
	-- it's always funny how in the process of "simplifying" something, I tend to make it do more
	if event.amount == 0 and missType then
		eventString = format("%s%s %s", source, spell, ACTION_SWING_MISSED)

	elseif eventType == EVENT_TYPE_AURA_APPLIED then
		local auraText = localOutput and format("|cff%s++|r", auraType == AURA_TYPE_BUFF and "00ff00" or "ff0000") or "++"
		if event.amount > 1 then
			eventString = format("%s %s%s (%d)", auraText, source, spell, event.amount)
		else
			eventString = format("%s %s%s", auraText, source, spell)
		end

	elseif eventType == EVENT_TYPE_AURA_REMOVED then
		local auraText = localOutput and format("|cff%s--|r", auraType == AURA_TYPE_BUFF and "00ff00" or "ff0000") or "--"
		if event.amount > 1 then
			eventString = format("%s %s%s (%d)", auraText, source, spell, event.amount)
		else
			eventString = format("%s %s%s", auraText, source, spell)
		end

	elseif event.amount ~= -1 then
		local amount = event.amount
		if event.overkill and event.overkill > 0 then
			amount = amount - event.overkill
		end
		if localOutput then
			amount = format("|cff%s%d|r", eventType == EVENT_TYPE_HEAL and "00ff00" or "ff0000", amount)
		end
		eventString = format("%s%s for %s", source, spell, amount)
	end

	local result
	if missType then
		if missType == "RESIST" or missType == "BLOCK" or missType == "ABSORB" then
			result = _G["TEXT_MODE_A_STRING_RESULT_"..missType]:format(event.missAmount)
		else
			if event.spellName == ACTION_RANGED then
				result = _G["ACTION_RANGE_MISSED_"..missType]
			else
				result = _G["ACTION_SWING_MISSED_"..missType]
			end
		end
	elseif not auraType then
		local overkill = eventType == EVENT_TYPE_DAMAGE and event.overkill
		local overhealing = eventType == EVENT_TYPE_HEAL and event.overkill
		result = CombatLog_String_DamageResultString(event.resisted, event.blocked, event.absorbed, event.isCrit, event.isGlancing, event.isCrush, overhealing, "A", event.spellId, overkill)

	end

	if result then
		eventString = eventString.." "..result
	end

	local timeBeforeDeath = format("-%.02fs", timeOfDeath - event.timestamp)

	return eventString, timeBeforeDeath, health, spellLink, (event.overkill ~= nil)
end


function Obituary:OnInitialize()
	self.db = LibStub("AceDB-3.0"):New("ObituaryDB", Obituary.defaultDB, true)
	db = self.db.profile

	local function OnProfileChanged()
		db = self.db.profile
		self:CheckEnable()
	end
	self.db.RegisterCallback(self, "OnProfileChanged", OnProfileChanged)
	self.db.RegisterCallback(self, "OnProfileCopied", OnProfileChanged)
	self.db.RegisterCallback(self, "OnProfileReset", OnProfileChanged)

	self:RawHook("SetItemRef", "ParseLink", true)
end

function Obituary:OnEnable()
	self:RegisterEvent("PLAYER_ENTERING_WORLD", "CheckEnable")
	self:RegisterEvent("ZONE_CHANGED_NEW_AREA", "CheckEnable")

	self:RegisterEvent("GROUP_ROSTER_UPDATE", "CheckPets")
	self:RegisterEvent("UNIT_PET", "CheckPets")

	self:CheckEnable()
end

function Obituary:CheckEnable()
	CombatLogHandler:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")

	local enable = nil

	local instanceType = select(2, GetInstanceInfo())
	if db.enableForPlayersInGroup or db.enableForPlayersNotInGroup or db.enableForPets or db.enableForNPCs then
		if instanceType == "pvp" then
			enable = db.enableForBattleground
		elseif instanceType == "arena" then
			enable = db.enableForArena
		elseif instanceType == "party" then
			enable = db.enableForPartyInstance
		elseif instanceType == "raid" then
			enable = db.enableForRaidInstance
		elseif instanceType == "none" then
			enable = db.enableForWorld
		end
	end

	if enable then
		CombatLogHandler:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
		self:CheckPets()
	end
end

function Obituary:ParseLink(link, text, button)
	if strsub(link, 1, 8) ~= "obituary" then
		return self.hooks["SetItemRef"](link, text, button)
	end

	local _, timestamp, guid, name, flags, raidFlags = strsplit(":", link)
	local tooltip = Obituary.Tooltip
	local report = self:GetDeathReports(guid) --deathReport[guid]

	-- We don't have information stored for this death
	if not report or not report[timestamp] or Stack.len(report[timestamp]) == 0 then
		tooltip:SetText(L["Death report for %s"]:format(name))
		tooltip:AddSeparator()
		tooltip:AddLine(L["There is no information for this death, or the information is no longer available."])
		tooltip:Show()
		return
	end

	-- Send the death report to the specified channel
	if IsShiftKeyDown() then
		local localOutput = db.detailedOutput == "ChatFrame"
		local outputChannel = localOutput and ChatFrameList[db.detailedChatFrame] or db.detailedOutput
		SendMessage(L["Death report for %s"]:format(DecorateName(name, flags, raidFlags, localOutput)), outputChannel)

		-- Get the last N events for output (excluding SPELL_AURA_REMOVED)
		local output = Stack.new(min(db.detailedReportEvents, db.detailedReportEventsChat))
		for event in Stack.next(report[timestamp]) do
			if event.type ~= EVENT_TYPE_AURA_REMOVED then
				Stack.push(output, event)
			end
		end

		-- Create the bulk of the report information
		for event in Stack.next(output) do
			local eventString, time, health = DeathEventString(event, timestamp, localOutput)
			if eventString then
				if health then
					SendMessage(format("%s: %s %s", time, health, eventString), outputChannel)
				else
					SendMessage(format("%s: %s", time, eventString), outputChannel)
				end
			end
		end

		Stack.release(output)

	-- Display the death report in a tooltip
	else
		tooltip:SetText(L["Death report for %s"]:format(DecorateName(name, flags, raidFlags, true)))
		tooltip:AddSeparator()

		-- Create the bulk of the report information
		for event in Stack.next(report[timestamp]) do
			local eventString, time, health, spell = DeathEventString(event, timestamp, true)
			if eventString then
				tooltip:AddLine(health, eventString, time, spell)
			end
		end

		tooltip:Show()
	end
end

