
-- Add the module to the tree
local mod = klhtm
local me = {}
mod.combat = me

--[[
KTM_Combat.lua

The combat module parses combat log events for damage and abilities done.
]]

-- These are the events we would like to be notified of
me.myevents = { "CHAT_MSG_SPELL_FAILED_LOCALPLAYER", "CHAT_MSG_COMBAT_FRIENDLY_DEATH"}

-- these are kept for debug purposes. We need two because next attack abilities are split into two.
me.lastattack = nil
me.secondlastattack = nil

--[[ 
This is a record of attacks in the last second while out of combat. We keep this because when you
go into combat by initiating an attack, the +combat event can come after the actual attack. 
]]
me.recentattacks = { } 

me.onupdate = function()

	local timenow = GetTime()
	local key
	local value

	for key, value in me.recentattacks do
		if value[1] < timenow - 1 then
			me.recentattacks[key] = nil
		end
	end

end
	

-- This is a method level temporary variable. Declared at file level because it is a list,
-- and we don't want to keep paging heap memory every time he is created.
me.event = 
{
	["hits"] = 0,
	["damage"] = 0,
	["rage"] = 0,
	["threat"] = 0,
	["name"] = 0,
}

--[[ 
Special onevent() method that will be called by Core.lua:onevent()
]]
me.onevent = function()

	if me.oneventinternal() then
		KLHTM_RequestRedraw("self")
	end
	
end

-- Returns non-nil if the event causes our threat to change
me.oneventinternal = function()
	
	local ability
	local target
	local amount
	local damagetype
	
	if event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" then
		if string.find(arg1, mod.string.get("spell", "sunder")) then
			me.retractsundercast()
			return true 
		else
			return
		end
	
	elseif event == "CHAT_MSG_COMBAT_FRIENDLY_DEATH" then
		
		if arg1 == UNITDIESSELF then -- UNITDIESSELF = "You die."
			-- death is a threat wipe
			mod.table.resetraidthreat()
			return true
		end
		
	end
		
end

--[[
mod.combat.specialattack(abilityid, target, damage, iscrit, spellschool)
This handles any attack from a spell that has special threat properties, i.e. all the spells in mod.data.spells .
<abilityid> is the internal identifier for these abilities. i.e. Sunder Armor has <abilityid> = "sunder". This is locale
independent.
<damage>, <iscrit>, and <spellschool> are optional. <spellschool> is localised, and will have the value either nil
	or "" or one of SPELL_SCHOOL1_CAP, SPELL_SCHOOL2_CAP, etc.
<iscrit> is only accepted if it has the boolean value true.
]]
me.specialattack = function(abilityid, target, damage, iscrit, spellschool)

	-- 1) check the attack is directed at the master target. If not, ignore.
	if mod.boss.targetismaster(target) == nil then
		return
	end
	
	-- 2) get the player's global threat modifiers (defensive stance, blessing of salvation, etc)
	local threatmodifier = mod.my.globalthreat.value
	
	--[[
	Now, most attacks can be handled gracefully by the table. However, for abilities that modify your autoattack, 
	we would prefer to decouple the ability from the autoattack, so we have to handle these cases individually.
	]]
	
	-- reset me.event
	me.event.hits = 1
	me.event.rage = 0
	me.event.damage = damage
	
	-- 3) Handle Autoattack modifying abilities separately
	if abilityid == "whitedamage" then

		-- shaman special: check for rockbiter
		if mod.my.mods.shaman.rockbiter > 0 then
			
			-- make a separate event for the rockbiter
			local weaponspeed = UnitAttackSpeed("player")
			local rockbiterdps = mod.data.rockbiter[mod.my.mods.shaman.rockbiter]

			-- note: we are sending the threat value of rockbiter as the damage argument
			me.specialattack("rockbiter", target, rockbiterdps * weaponspeed, nil, nil)
			
			-- the above call to me.specialattack will have overwritten some parts of me.event. Set them back!
			me.event.damage = damage
		end
		
		-- normal behaviour
		me.event.threat = damage * threatmodifier
	
	-- Special case: Rockbiter Weapon
	elseif abilityid == "rockbiter" then
		
		-- this will only come from a "whitedamage" call to this method (see above)
		me.event.threat = me.event.damage * threatmodifier
		me.event.damage = 0

	-- Special case: Heroic Strike
	elseif abilityid == "heroicstrike" then
		
		local preimpaledamage = damage
		if iscrit == true then
			preimpaledamage = damage / (1 + mod.my.mods.warrior.impale)
		end
		
		local addeddamage = mod.my.ability("heroicstrike", "nextattack")
		local myaveragedamage = me.averagemainhanddamage()
		local whitedamage = preimpaledamage * (myaveragedamage / (addeddamage + myaveragedamage))
		
		-- Now make a separate method call for the autoattack component
		me.specialattack("whitedamage", target, whitedamage, nil, nil)
		
		-- The above method will have overwritten some parts of me.event, so change them back
		me.event.damage = damage - whitedamage
		me.event.threat = (me.event.damage + mod.my.ability("heroicstrike", "threat")) * threatmodifier
		me.event.rage = mod.my.ability("heroicstrike", "rage") + whitedamage / (UnitLevel("player") / 2)
	
	-- Special Case: Maul
	elseif abilityid == "maul" then
		
		-- same as heroic strike, but a bit different
		local presavagefurydamage = damage / (1 + mod.my.mods.druid.savagefury)		
		local addeddamage = mod.my.ability("maul", "nextattack")
		local myaveragedamage = me.averagemainhanddamage()
		local whitedamage = presavagefurydamage * (myaveragedamage / (addeddamage + myaveragedamage))
		
		-- Now make a separate method call for the autoattack component
		me.specialattack("whitedamage", target, whitedamage, nil, nil)
		
		-- The above method will have overwritten some parts of me.event, so change them back
		me.event.damage = damage - whitedamage
		me.event.threat = threatmodifier * (damage * mod.my.ability("maul", "multiplier") - whitedamage)
		me.event.rage = mod.my.ability("maul", "rage") + whitedamage / (UnitLevel("player") / 2)
		
	-- Default Case: all other abilities
	else
		
		-- 1) Check for rage
		me.event.rage = mod.my.ability(abilityid, "rage")

		local multiplier = mod.my.ability(abilityid, "multiplier")
		
		-- 2) Check for multiplier
		if multiplier then
			me.event.threat = me.event.damage * multiplier
			
		else
			me.event.threat = me.event.damage + mod.my.ability(abilityid, "threat")
		end
		
		-- 3) Multiply by global modifiers
		me.event.threat = me.event.threat * threatmodifier
	
	end
	
	-- Paladin righteous fury (can affect holy shield)
	if mod.my.class == "paladin" then
		
		-- righteous fury
		if spellschool == SPELL_SCHOOL1_CAP then -- holy
			me.event.threat = me.event.threat * mod.my.mods.paladin.righteousfury
		end
		
	-- warlock Nemesis 8/8 (can affect searing pain)
	elseif mod.my.class == "warlock" then
			
		-- Nemesis 8/8
		if (mod.my.mods.warlock.nemesis == true) and (mod.data.spellmatchesset("Warlock Destruction", abilityid) == true) then
			me.event.threat = me.event.threat * 0.8
		end
	end
		
	-- check for >= 0 threat
	if me.event.threat + mod.table.getraidthreat() < 0 then
		me.event.threat = - mod.table.getraidthreat()
	end
	
	-- relocalise.
	if abilityid == "whitedamage" then
		me.event.name = mod.string.get("threatsource", "whitedamage")
		
	else
		me.event.name = mod.string.get("spell", abilityid)
	end
	
	-- Add to data
	me.addattacktodata(me.event.name, me.event)
	me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
	
end

-- to work out which part of a next attack ability was from white damage
-- used by nextattack abilities like maul and heroic strike.
me.averagemainhanddamage = function()

	local min, max = UnitDamage("player")
	return (min + max) / 2

end

--[[
me.normalattack(spellname, damage, target, iscrit, spellschool)
Handles a damage-causing ability with no special threat properties. We often have to make modifiers for gear or talents here.
<spellname> is the name of the ability.
<spellid> is the internal name for "special" spells or abilities, i.e. those we have to look out for because there are specific set bonuses or talents that affect them.
<damage> is the damage done.
<target> is the name of the mob you hit.
<iscrit> is a boolean, whether the hit was a critical one. Triggers iff it is the boolean value true.
<spellschool> is a string, one of SPELL_SCHOOL1_CAP, SPELL_SCHOOL2_CAP, etc, or possibly nil or "".
]]
me.normalattack = function(spellname, spellid, damage, isdot, target, iscrit, spellschool)
	
	-- check the attack is directed at the master target. If not, ignore.
	if mod.boss.targetismaster(target) == nil then
		return
	end

	-- threatmodifier includes global things, like defensive stance, tranquil air totem, rogue passive modifier, etc.
	local threatmodifier = mod.my.globalthreat.value
	
	-- Special threat mod: priest silent resolve (spells only)
	if mod.my.class == "priest" then
		threatmodifier = threatmodifier * (1.0 +  mod.my.mods.priest.silentresolve)
	end 
	
	-- Special threat modifiers for mages
	if mod.my.class == "mage" then
		
		if spellschool == SPELL_SCHOOL6_CAP then -- arcane
			threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.arcanethreat)
		
		elseif spellschool == SPELL_SCHOOL4_CAP then -- frost
			threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.frostthreat)
		
		elseif spellschool == SPELL_SCHOOL2_CAP then -- fire
			threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.firethreat)
		end
	end
	
	-- Default values for me.event:
	me.event.hits = 1
	me.event.rage = 0
	me.event.damage = damage
	me.event.threat = damage * threatmodifier

	-- now get the name:
	if spellid == "dot" then
		
		me.event.hits = 0
		me.event.name = mod.string.get("threatsource", "dot")
		
	elseif spellid == "damageshield" then
		me.event.name = mod.string.get("threatsource", "damageshield")
		
	else
		me.event.name = mod.string.get("threatsource", "special")
		me.event.threat = damage * threatmodifier
	end
	
	-- Now apply class-specific filters
	
	-- warlock
	if mod.my.class == "warlock" then
			
		-- Nemesis 8/8
		if (mod.my.mods.warlock.nemesis == true) and (mod.data.spellmatchesset("Warlock Destruction", spellid) == true) then
			me.event.threat = me.event.threat * 0.8
		end
	
	-- Priest
	elseif mod.my.class == "priest" then
			
		-- shadow affinity
		if spellschool == SPELL_SCHOOL5_CAP then
			me.event.threat = me.event.threat * mod.my.mods.priest.shadowaffinity
		end
		
		-- holy nova: no threat
		if spellid == "holynova" then
			me.event.threat = 0
		end
		
	-- Mage
	elseif mod.my.class == "mage" then
			
		-- netherwind
		if mod.my.mods.mage.netherwind == true then
			
			if (spellid == "frostbolt") or (spellid == "scorch") or (spellid == "fireball") then
				-- note that this won't trigger off the dot part of fireball, because then spellid will be "dot"
				me.event.threat = math.max(0, (me.event.threat - 100))
				
			elseif spellid == "arcanemissiles" then
				me.event.threat = math.max(0, (me.event.threat - 20))
			end
		end
		
	-- Paladin
	elseif mod.my.class == "paladin" then
		
		-- righteous fury
		if spellschool == SPELL_SCHOOL1_CAP then -- holy
			me.event.threat = me.event.threat * mod.my.mods.paladin.righteousfury
		end
	end

	-- special: blood siphon no threat vs Hakkar. This may or may not be correct.
	if spellid == "bloodsiphon" then
		me.event.threat = 0
	end

	-- now add me.event to individual and totals
	me.addattacktodata(me.event.name, me.event)
	me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
	
end

--[[ 
me.registertaunt()
Called when you succesfully casts Taunt or Growl on a mob.
<target> is the name of the mob you have taunted.
]]
me.taunt = function(target)
	
	if mod.out.checktrace("info", me, "taunt") then
		mod.out.printtrace(string.format("Taunting %s!", target))
	end
	
	--[[
	OK, new idea. If targettarget has greater threat than you, assume they are the aggro target.
	]]
	
	if mod.boss.mastertarget and (target ~= mod.boss.mastertarget) then
		-- you taunted a mob that is not the master target
		return
	end
	
	-- here, you either taunted the master target, or there is no master target
	-- check if you are still targetting that mob (likely, if you just taunted it)
	if UnitName("target") == target then
	
		-- Check for tt
		local targettarget = UnitName("targettarget")
		
		if mod.table.raiddata[targettarget] and (mod.table.raiddata[targettarget] > mod.table.getraidthreat()) then
			-- your current target is targetting another player, and they have more threat than you
			
			local gain = mod.table.raiddata[targettarget] - mod.table.getraidthreat()
				
			me.event.hits = 1
			me.event.damage = 0
			me.event.rage = 0
			me.event.threat = gain
			me.event.name = mod.string.get("spell", "taunt")

			me.addattacktodata(mod.string.get("spell", "taunt"), me.event)
			me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
			
			if mod.out.checktrace("info", me, "taunt") then
				mod.out.printtrace(string.format("You taunt %s from %s, gaining %d threat.", target, targettarget, gain))
			end
			
			return
		end
	end
	
	-- If that didn't work, use the old code:	
	
	if target == mod.boss.mastertarget then
		local previoustargetthreat = mod.table.raiddata[mod.boss.mttruetarget]
		
		if (previoustargetthreat ~= nil) and (previoustargetthreat > mod.table.getraidthreat()) then
			local tauntgain = previoustargetthreat - mod.table.getraidthreat()
			
			me.event.hits = 1
			me.event.damage = 0
			me.event.rage = 0
			me.event.threat = tauntgain
			me.event.name = mod.string.get("spell", "taunt")
			
			me.addattacktodata(mod.string.get("spell", "taunt"), me.event)
			me.addattacktodata(mod.string.get("threatsource", "total"), me.event)

			if mod.out.checktrace("info", me, "taunt") then
				mod.out.printtrace(string.format("You taunt %s from %s, gaining %d threat.", target, mod.boss.mttruetarget or "<nil>", tauntgain))
			end
		
		else
			if mod.out.checktrace("info", me, "taunt") then
				mod.out.printtrace(string.format("You taunt %s from %s, but he had a lower threat, so you gain no threat.", target, mod.boss.mttruetarget or "<nil>"))
			end
		end
	end
	
end

--[[
me.possibleoverheal(spellname, spellid, amount, target)
Works out the threat from a heal.
<spellname> is the localised name of the spell
<spellid> is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "".
<amount> is the healed amount only (no overheal)
<target> is the name of the target
Called when you heal someone. Deducts the overhealing from the total, then calls the Heal method
]]
me.possibleoverheal = function(spellname, spellid, amount, target)
	
	-- we can check the target's health, which will be the health before the heal. Then we work out what the 
	-- heal did, and we can calculate overheal.
	
	local unit = mod.unit.findunitidfromname(target)
	
	if unit == nil then
		if mod.out.checktrace("info", me, "healtarget") then
			mod.out.printtrace(string.format("Could not find a UnitID for the name %s.", target))
		end
		-- (and assume there was no overheal)
	else
		local hpvoid = UnitHealthMax(unit) - UnitHealth(unit)
		amount = math.min(amount, hpvoid)
	end
	
	me.registerheal(spellname, spellid, amount, target)
	
end

--[[ 
me.registerheal(spellname, spellid, amount, target)
Works out the threat from a heal.
<spellname> is the localised name of the spell
<spellid> is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "".
<amount> is the healed amount only (no overheal)
<target> is the name of the target
]]
me.registerheal = function(spellname, spellid, amount, target)

	-- in general, don't count heals towards the master target
	if mod.boss.mastertarget and mod.boss.targetismaster(target) then
		return
	end
	
	me.event.hits = 1
	me.event.rage = 0
	me.event.damage = amount
	
	local threatmod = mod.my.globalthreat.value
	
	-- Special threat mod: priest silent resolve (spells only)
	if mod.my.class == "priest" then
		threatmod = threatmod * (1 + mod.my.mods.priest.silentresolve)
	end 
	
	me.event.threat = amount * threatmod * mod.data.threatconstants.healing
	
	-- class-based healing multipliers
	if mod.my.class == "paladin" then
		me.event.threat = me.event.threat * mod.my.mods.paladin.healing
		
	elseif mod.my.class == "druid" then
		me.event.threat = me.event.threat * mod.my.mods.druid.subtlety
		
		if spellid == "tranquility" then
			me.event.threat = me.event.threat * mod.my.mods.druid.tranquilitythreat
		end
		
	elseif mod.my.class == "shaman" then
		me.event.threat = me.event.threat * mod.my.mods.shaman.healing
		
	end 
	
	-- Special: healing abilities which don't cause threat
	if spellid == "holynova" then
		me.event.threat = 0
	elseif spellid == "siphonlife" then
		me.event.threat = 0
	elseif spellid == "drainlife" then
		me.event.threat = 0
	elseif spellid == "deathcoil" then
		me.event.threat = 0
	end
	
	me.addattacktodata(mod.string.get("threatsource", "healing"), me.event)
	
	me.event.damage = 0
	me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
end

--[[
me.powergain(amount, powertype)
Calculates the threat from gaining energy / mana / rage.
<amount> is the amount of power gained.
<powertype> is "Mana" or "Rage" or "Energy", but the localised versions.
<spellid> is the spell or effect that caused the power gain.
]]
me.powergain = function(amount, powertype, spellid)
	
	me.event.damage = amount
	me.event.hits = 1
	me.event.rage = 0
	
	-- 1) Prevent "overheal" for power gain
	local maxgain = UnitManaMax("player") - UnitMana("player")
	amount = math.min(maxgain, amount)
	
	if powertype == mod.string.get("power", "rage") then
		me.event.threat = amount * mod.data.threatconstants.ragegain
		
	elseif powertype == mod.string.get("power", "energy") then
		me.event.threat = amount * mod.data.threatconstants.energygain
		
	elseif powertype == mod.string.get("power", "mana") then
		me.event.threat = amount * mod.data.threatconstants.managain 
		
	else
		return
	end
	
	-- Special: abilities which don't cause threat
	if spellid == "lifetap" then
		me.event.threat = 0
	end
		
	me.addattacktodata(mod.string.get("threatsource", "powergain"), me.event)
	
	-- now mod it a bit to work into total better
	me.event.damage = 0
	me.event.hits = 0
	
	me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
	
end

--[[
me.addattacktodata(name, data)
Once the threat from an attack has been worked, add it.
<name> is the category to add the threat to
<data> is me.event, i think...
Heap Memory will be created when you call this method when out of combat (mostly healing). 
From the size of the list (two numbers in it), will be 50 bytes tops each time.
]]
me.addattacktodata = function(name, data)
	
	-- Ignore if charmed
	if mod.my.states.playercharmed.value == true then
		return
	end 
	
	if name ~= mod.string.get("threatsource", "total") then
		me.secondlastattack = me.lastattack
		me.lastattack = data
		
		-- Add this to the recent attacks list, if we are not in combat
		if mod.my.states.incombat.value == false then
			table.insert(me.recentattacks, {GetTime(), data.threat})
		end
	end
	
	-- Add a new column to mod.table.mydata, if it does not exist already
	if mod.table.mydata[name] == nil then
		mod.table.mydata[name] = mod.table.newdatastruct()
	end
	
	mod.table.mydata[name].hits = mod.table.mydata[name].hits + data.hits
	mod.table.mydata[name].threat = mod.table.mydata[name].threat + data.threat
	mod.table.mydata[name].rage = mod.table.mydata[name].rage + data.rage
	mod.table.mydata[name].damage = mod.table.mydata[name].damage + data.damage 
	
end


----------------------------------------------------------
--					Handling Sunder						--
----------------------------------------------------------


--[[
klhtu.combat.submitsundercast()
When you think you have just cast a sunder, call this method to make sure the addon knows. There's no way to
reliably detect a sunder hit, so we assume it has been cast, then look for failure signs (misses, spell errors)
It's highly recommended to use klhtm.combat.sunder() or KLHTM_Sunder() instead, because it will make sure
that threat is correct when you spam the button, which may not happen otherwise.
]]
me.submitsundercast = function()
	
	-- check for Master Target
	if mod.boss.targetismaster(UnitName("target")) == nil then
		return
	end
	
	me.specialattack("sunder", UnitName("target"), 0)
	KLHTM_RequestRedraw("self")
end

-- Call this from a macro, to replace your normal sunder button
me.spellbooksunderindex = 1

-- Kept for compatibility
function KLHTM_Sunder()
	
	me.sunder()
	
end

--[[
me.sunder()
Casts Sunder Armor if most checks reveal it is castable, and calls me.sumbitsundercast().
This method is safe to be spammed. It will only cast if the global cooldown is off, which means once you try to
cast it, you won't try again unless the server says "failed due to <x>". So it's like full good and stuff.
]]
me.sunder = function()
	
	if mod.my.class ~= "warrior" then
		return
	end
	
	-- 1) Check if the old position is fine
	if GetSpellName(me.spellbooksunderindex, "spell") ~= mod.string.get("spell", "sunder") then
		
		-- get spell number of spells
		local spelltabs = GetNumSpellTabs()
		local numspells = 0
		local i
		local temp
	
		for i = 1, spelltabs do
			_, _, _, temp = GetSpellTabInfo(i)
			numspells = numspells + temp
		end
	
		-- 2) locate sunder armor in the spell book
		for i = 1, numspells do
			if GetSpellName(me.spellbooksunderindex + i, "spell") == mod.string.get("spell", "sunder") then
				me.spellbooksunderindex = me.spellbooksunderindex + i
				break
				
			elseif i == numspells then
				if mod.out.checktrace("warning", me, "sunder") then
					mod.out.printtrace("Can't find sunder in your spellbook!")
				end
				return -- can't find sunder
			end
		end
	end
	
	-- Now we've found sunder. Check the cooldown		
	if GetSpellCooldown(me.spellbooksunderindex, "spell") ~= 0 then 
		return
	end
	
	-- Test for target
	if UnitCanAttack("player", "target") == nil then
		return
	end 
	
	-- Cast
	CastSpellByName(mod.string.get("spell", "sunder") .. "()")
	me.submitsundercast()
	
end

--[[ 
me.retractsundercast()
Called when an attempted cast of Sunder Armor was found to have failed. i.e. we received a message like
"Your sunder armor failed: not enough rage" in the combat log.
]]
me.retractsundercast = function()

	-- 1) check for master target
	if mod.boss.targetismaster(UnitName("target")) == nil then
		return
	end
	
	me.event.hits = -1
	me.event.damage = 0
	me.event.rage = - mod.my.ability("sunder", "rage")
	
	local threatmodifier = mod.my.globalthreat.value
	me.event.threat = - mod.my.ability("sunder", "threat") * threatmodifier 

	me.addattacktodata(mod.string.get("spell", "sunder"), me.event)
	me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
	
	KLHTM_RequestRedraw("self")
end

--[[
This code hooks UseAction, intercepts the user casting Sunder Armor, and runs me.sunder() instead.
]]

me.saveduseaction = UseAction

me.newuseaction = function(actionindex, x, y)
	
	-- Check Sunder
	if (mod.my.class == "warrior") and actionindex and (GetActionText(actionindex) == nil) and (GetActionTexture(actionindex) == "Interface\\Icons\\Ability_Warrior_Sunder") then
		
		if GetActionCooldown(actionindex) > 0 then
			if mod.out.checktrace("info", me, "sunder") then
				mod.out.printtrace("Preventing Sunder cast due to cooldown!")
			end
			return
			
		else
			-- Now check we have a decent target. Otherwise press "tab" - this is what the UI does anyway
			if UnitCanAttack("player", "target") ~= 1 then
				
				if mod.out.checktrace("info", me, "sunder") then
					mod.out.printtrace("Preventing Sunder cast due to invalid target!")
				end
				
				TargetNearestEnemy()
				return
			end
			
			me.submitsundercast()
			-- At the bottom of this method the sunder will be cast
		end
	
	end

   -- Call the original function
   me.saveduseaction(actionindex, x, y)   

end

UseAction = me.newuseaction
