--[[
Name: SpecialEvents-Aura-2.0
Revision: $Rev: 13975 $
Author: Tekkub Stoutwrithe (tekkub@gmail.com)
Website: http://wiki.wowace.com/index.php/SpecialEvents_Addon
Documentation: http://wiki.wowace.com/index.php/SpecialEvents-Aura-2.0
SVN: svn://svn.wowace.com/root/trunk/SpecialEventsEmbed/SpecialEvents-Aura-2.0
Description: Special events for Aura's, (de)buffs gained, lost etc.
Dependencies: AceLibrary, AceEvent-2.0, Gratuity-2.0
]]


local vmajor, vminor = "SpecialEvents-Aura-2.0", "$Revision: 13975 $"

if not AceLibrary then error(vmajor .. " requires AceLibrary.") end
if not AceLibrary:HasInstance("AceEvent-2.0") then error(vmajor .. " requires AceEvent-2.0.") end
if not AceLibrary:IsNewVersion(vmajor, vminor) then return end

local lib = {}
AceLibrary("AceEvent-2.0"):embed(lib)

local gratuity = AceLibrary("Gratuity-2.0")
local RL
local debuffTextureCache = {}
local buffTextureCache = {}

local table_setn
do
	local version = GetBuildInfo()
	if string.find(version, "^2%.") then
		-- 2.0.0
		table_setn = function() end
	else
		table_setn = table.setn
	end
end

----------------------------
--      Compost Heap      --
----------------------------

local heap = {}
setmetatable(heap, {__mode = "kv"})


local function acquire(a1,a2,a3,a4,a5)
	local t = next(heap)
	if t then
		heap[t] = nil
		assert(not next(t), "A table in the compost heap has been modified!")
	end
	t = t or {}

	if a1 ~= nil then table.insert(t, a1) end
	if a2 ~= nil then table.insert(t, a2) end
	if a3 ~= nil then table.insert(t, a3) end
	if a4 ~= nil then table.insert(t, a4) end
	if a5 ~= nil then table.insert(t, a5) end
	return t
end


local function reclaim(t, d)
	if type(t) ~= "table" then return end

	if d and d > 0 then
		for i in pairs(t) do
			if type(t[i]) == "table" then reclaim(t[i], d - 1) end
		end
	end

	for i in pairs(t) do t[i] = nil end
	t.reset = 1
	t.reset = nil
	table_setn(t, 0)

	heap[t] = true
end


----------------------------
--     Initialization     --
----------------------------

local function registerevents(self)
	self:RegisterEvent("PLAYER_LEAVING_WORLD")
	self:RegisterEvent("UNIT_AURA", "AuraScan")
	self:RegisterEvent("UNIT_AURASTATE", "AuraScan")
	self:RegisterEvent("PLAYER_AURAS_CHANGED")
	self:RegisterEvent("PLAYER_TARGET_CHANGED")
	self:RegisterEvent("PLAYER_REGEN_ENABLED")
	-- check if RosterLib exists. We need to do that here and not earlier.
	RL = AceLibrary:HasInstance("RosterLib-2.0") and AceLibrary("RosterLib-2.0") or nil
	if RL then 
		RL:Enable()
		self:RegisterBucketEvent("RosterLib_UnitChanged",0.2)
	else
		self:RegisterEvent("PARTY_MEMBERS_CHANGED",1)
		self:RegisterEvent("RAID_ROSTER_UPDATE",1)
	end
end


-- Activate a new instance of this library
function activate(self, oldLib, oldDeactivate)
	if oldLib then
		self.vars = oldLib.vars
		if oldLib:IsEventRegistered("UNIT_AURA") then registerevents(self) end
		if oldLib:IsEventRegistered("RosterLib_UnitChanged") then
			if RL then RL:Disable() end
			oldLib:UnregisterAllEvents()
		end	
	else self.vars = {buffs = {}, debuffs = {}} end

	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	
	if oldDeactivate then oldDeactivate(oldLib) end
end


function lib:PLAYER_ENTERING_WORLD()
	self:ScanAllAuras()
	registerevents(self)
end


function lib:PLAYER_LEAVING_WORLD()
	self:UnregisterAllEvents()
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	if RL then 
		RL:Disable()
	end
end


--------------------------------
--      Tracking methods      --
--------------------------------

function lib:PLAYER_AURAS_CHANGED()
	self:AuraScan("player")
	if GetNumRaidMembers() > 0 then
		local u
		for i=1,GetNumRaidMembers() do
			if UnitIsUnit("raid"..i, "player") then u = "raid"..i end
		end
		self:AuraScan(u)
	end
end


function lib:PLAYER_TARGET_CHANGED()
	self:AuraScan("target")
	self:TriggerEvent("SpecialEvents_AuraTargetChanged")
end


function lib:RosterLib_UnitChanged(units)
	for unit in pairs(units) do
		if unit and UnitExists(unit) then
			self:AuraScan(unit)
		end
	end
	if GetNumRaidMembers() > 0 then
		self:TriggerEvent("SpecialEvents_AuraRaidRosterUpdate")
	else
		self:TriggerEvent("SpecialEvents_AuraPartyMembersChanged")
	end
end


function lib:PARTY_MEMBERS_CHANGED()
	if UnitExists("pet") then self:AuraScan("pet") end

	for i=1,4 do
		if UnitExists("party"..i) then self:AuraScan("party"..i) end
		if UnitExists("partypet"..i) then self:AuraScan("partypet"..i) end
	end
	self:TriggerEvent("SpecialEvents_AuraPartyMembersChanged")
end


function lib:RAID_ROSTER_UPDATE()
	for i=1,40 do
		if UnitExists("raid"..i) then self:AuraScan("raid"..i) end
		if UnitExists("raidpet"..i) then self:AuraScan("raidpet"..i) end
	end
	self:TriggerEvent("SpecialEvents_AuraRaidRosterUpdate")
end

function lib:PLAYER_REGEN_ENABLED()
	reclaim(debuffTextureCache)
	debuffTextureCache = acquire()
	reclaim(buffTextureCache)
	buffTextureCache = acquire()
end

function lib:ScanAllAuras()
	if UnitExists("player") then self:AuraScan("player") end
	if UnitExists("pet") then self:AuraScan("pet") end

	for i=1,4 do
		if UnitExists("party"..i) then self:AuraScan("party"..i) end
		if UnitExists("partypet"..i) then self:AuraScan("partypet"..i) end
	end

	for i=1,40 do
		if UnitExists("raid"..i) then self:AuraScan("raid"..i) end
		if UnitExists("raidpet"..i) then self:AuraScan("raidpet"..i) end
	end

	if UnitExists("target") then self:AuraScan("target") end
--~~ 	if UnitExists("mouseover") then self:AuraScan("mouseover") end
end


function lib:AuraScan(targ)
	local maxbuffs, maxdebuffs, t = 16, 16, targ or arg1
	local oldd, oldb = self.vars.debuffs[t], self.vars.buffs[t]
	local newd, newb = acquire(), acquire()

	if t == "player" then
		for i=0,(maxbuffs-1) do
			local bidx = GetPlayerBuff(i, "HELPFUL")
			if bidx and bidx ~= -1 then
				gratuity:SetPlayerBuff(bidx)
				local txt = gratuity:GetLine(1)
				if txt then newb[txt] = i end
			end
		end

		for i=0,(maxdebuffs-1) do
			local didx = GetPlayerBuff(i, "HARMFUL")
			if didx and didx ~= -1 then
				gratuity:SetPlayerBuff(didx)
				local txt, txtr = gratuity:GetLine(1)

				local apps = GetPlayerBuffApplications(didx)
				local texture = GetPlayerBuffTexture(didx)
				local dbtype = GetPlayerBuffDispelType(didx)

				if txt then
					local txtindex = txt
					if texture then txtindex = txtindex..texture end
					newd[txtindex] = acquire(i, txt, apps, dbtype, texture)
				end
			end
		end
	else
		for i=1,maxbuffs do
			local txt
			local texture, apps = UnitBuff (t, i)
			if texture then 
				if not buffTextureCache[texture] then
					gratuity:SetUnitBuff(t,i)
					txt = gratuity:GetLine(1)
					buffTextureCache[texture] = txt
				else
					txt = buffTextureCache[texture]
				end
				if txt then newb[txt] = i end
			end
		end

		for i=1,maxdebuffs do
			local txt
			local texture, apps, dbtype = UnitDebuff (t, i)
			if texture then
				if not debuffTextureCache[texture] then
					gratuity:SetUnitDebuff(t,i)
					txt = gratuity:GetLine(1)
					debuffTextureCache[texture] = txt
				else
					txt = debuffTextureCache[texture]
				end
				if txt then
					local txtindex = txt .. texture
					reclaim(newd[txtindex])
					newd[txtindex] = acquire(i, txt, apps, dbtype, texture)
				end
			end
		end
	end

	self.vars.buffs[t] = newb
	self.vars.debuffs[t] = newd

	if oldb then
		for b,i in pairs(oldb) do
			if not newb[b] then
				self:TriggerEvent("SpecialEvents_UnitBuffLost", t, b)
				if t == "player" then self:TriggerEvent("SpecialEvents_PlayerBuffLost", b) end
			end
		end
	end

	if oldd then
		for d,i in pairs(oldd) do
			if type(i) ~= "table" then
				local t = ""
				for d,i in pairs(oldd) do t = t.." "..d.."="..i end
				error(string.format("Debuff error! Unit: %s, Table dump:%s", targ, t))
			elseif not newd[d] then
				self:TriggerEvent("SpecialEvents_UnitDebuffLost", t, i[2], i[3], i[4], i[5])
				if t == "player" then self:TriggerEvent("SpecialEvents_PlayerDebuffLost", i[2], i[3], i[4], i[5]) end
			end
		end
	end

	for b,i in pairs(newb) do
		if (not oldb or not oldb[b]) then
			self:TriggerEvent("SpecialEvents_UnitBuffGained", t, b)
			if t == "player" then self:TriggerEvent("SpecialEvents_PlayerBuffGained", b, i) end
		end
	end

	for d,i in pairs(newd) do
		assert(type(i) == "table", string.format("Debuff: %s - Value not a table: %s", d, type(i) == "table" and "table" or i or "nil"))

		local o2 = oldd and type(oldd[d]) == "table" and oldd[d][2]
		if not o2 or o2 ~= i[2] then
			self:TriggerEvent("SpecialEvents_UnitDebuffGained", t, i[2], i[3], i[4], i[5])
			if t == "player" then self:TriggerEvent("SpecialEvents_PlayerDebuffGained", i[2], i[3], i[4], i[5]) end
		end
	end

	reclaim(oldb)
	reclaim(oldd, 1)
end


-----------------------------
--      Query Methods      --
-----------------------------

function lib:UnitHasBuff(targ, buff)
	if self.vars.buffs[targ] then return self.vars.buffs[targ][buff] end
end


function lib:UnitHasDebuff(targ, debuff)
	if self.vars.debuffs[targ] then
		for i, v in pairs(self.vars.debuffs[targ]) do
			if v[2] == debuff then
				return v[1], v[3]
			end
		end
	end
end


function lib:UnitHasDebuffType(targ, debufftype)
	if self.vars.debuffs[targ] then
		for i,v in pairs(self.vars.debuffs[targ]) do
			if v[4] == debufftype then return v end
		end
	end
end


function lib:BuffIter(unitid)
	local f = function(unitid, i)
		if not self.vars.buffs[unitid] then return end
		local idx = next(self.vars.buffs[unitid], i)
		local v = self.vars.buffs[unitid][idx]
		if v then return idx, v end
	end
	return f, unitid, nil
end

function lib:DebuffIter(unitid)
	local idx = nil
	local f = function(unitid)
		if not self.vars.debuffs[unitid] then return end
		idx = next(self.vars.debuffs[unitid], idx)
		local v = self.vars.debuffs[unitid][idx]
		if v then return v[2], v[3], v[4], v[5] end
	end
	return f, unitid, nil
end

--------------------------------
--      Load this bitch!      --
--------------------------------
AceLibrary:Register(lib, vmajor, vminor, activate)
