--===================================================
--									
--									GHI Buff Handler
--								GHI_BuffHandler.lua
--
--				Handles applying of GHI buffs, including area buffs.
--	
-- 						(c)2012 The Gryphonheart Team
--								All rights reserved
--===================================================


local class;
function GHI_BuffHandler()
	if class then
		return class;
	end
	class = GHClass("GHI_BuffHandler");

	local ApplyBuff, PackBuffInfo, UnpackBuffInfoAndRemoveDelay, ReceiveApplyBuff, CheckTarget, BuffRecieveSubscription, SendBuffInfo, BuffRecieveInfo, BuffChanged, UpdateBuffSubscriptions;
	local delayedBuffs = {};
	local delayedRemoveBuffs = {};
	local subscribedPlayers = {};
	local subscriptionsSend = {};
	local buff = GHI_BuffUI("Custom GHI buff", 0.0, 0.7, 0.5);
	local areaBuff = GHI_AreaBuff();
	local comm = GHI_Comm();
	local errorThrower = GHI_ErrorThrower();
	local log = GHI_Log();

	GHI_Timer(function()
		for i, v in pairs(delayedBuffs) do
			if type(v) == "table" and (v.time or 0) <= time() then
				class.CastBuff(UnpackBuffInfoAndRemoveDelay(v));
				delayedBuffs[i] = nil;
			end
		end
		for i, v in pairs(delayedRemoveBuffs) do
			if type(v) == "table" and (v.time or 0) <= time() then
				class.RemoveBuff(unpack(v));
				delayedRemoveBuffs[i] = nil;
			end
		end
		CheckTarget();
	end, 1);

	class.CastBuff = function(buffName, buffDetails, buffIcon, untilCanceled, filter, buffType, buffDuration, cancelable, stackable, count, delay, range, alwaysCastOnSelf)
		log.Add(3,"Cast Buff",{
			buffName=buffName,
			buffDetails=buffDetails,
			buffIcon=buffIcon,
			untilCanceled=untilCanceled,
			filter=filter,
			buffType=buffType,
			buffDuration=buffDuration,
			cancelable=cancelable,
			stackable=stackable,
			count=count,
			delay=delay,
			range=range,
			alwaysCastOnSelf=alwaysCastOnSelf,
		});

          if type(delay) == "number" and delay > 0 then
			local packedBuffInfo = PackBuffInfo(buffName, buffDetails, buffIcon, untilCanceled, filter, buffType, buffDuration, cancelable, stackable, count, delay, range, alwaysCastOnSelf)
			packedBuffInfo.time = time() + delay;
			table.insert(delayedBuffs, packedBuffInfo);
			return
		end

		if type(range) == "number" and range > 0 then
			areaBuff.Send(buffName, buffDetails, buffIcon, untilCanceled, filter, buffType, buffDuration, cancelable, stackable, count, delay, range);
		else
			if alwaysCastOnSelf or UnitName("target") == UnitName("player") or not (UnitName("target")) then
				ApplyBuff(buffName, buffDetails, buffIcon, untilCanceled, filter, buffType, buffDuration, cancelable, stackable, count)
			else
				if CheckInteractDistance("target", 1) then
					comm.Send(nil, UnitName("target"), "ApplyBuff", click)
				else
					errorThrower.TooFarAway();
				end
			end
		end
	end

	ApplyBuff = function(name, text, texture, untilCancelled, filter, debuffType, duration, cancelable, stackable, count)
		buff:CastBuff(string.lower(filter), name, UnitGUID("player"), name, text, texture, (not (untilCancelled) and duration) or 0, (not (untilCancelled) and GetTime() + duration) or 0, count, debuffType, stackable)
	end

	PackBuffInfo = function(buffName, buffDetails, buffIcon, untilCanceled, filter, buffType, buffDuration, cancelable, stackable, count, delay, range, alwaysCastOnSelf)
		return {
			buffName = buffName,
			buffDetails = buffDetails,
			buffIcon = buffIcon,
			untilCanceled = untilCanceled,
			filter = filter,
			buffType = buffType,
			buffDuration = buffDuration,
			cancelable = cancelable,
			stackable = stackable,
			count = count,
			delay = delay,
			range = range,
			alwaysCastOnSelf = alwaysCastOnSelf,
		};
	end
	UnpackBuffInfoAndRemoveDelay = function(info)
		return info.buffName, info.buffDetails, info.buffIcon, info.untilCanceled, info.filter, info.buffType, info.buffDuration, info.cancelable, info.stackable, info.count, 0, info.range, info.alwaysCastOnSelf;
	end

	ReceiveApplyBuff = function(sender, click)
		ApplyBuff(click.buffName, click.buffDetails, click.buffIcon, click.untilCanceled, click.filter, click.buffType, click.buffDuration, click.cancelable, click.stackable, click.count or 1, click.delay or 0)
	end
	comm.AddRecieveFunc("ApplyBuff", ReceiveApplyBuff)


	-- stick Setup
	if GHI_MiscData["stick_player_buffs"] == nil then
		if BuffFrame:IsShown() then
			GHI_MiscData["stick_player_buffs"] = true;
		else
			GHI_MiscData["stick_player_buffs"] = false;
		end
	end


	buff:StickToBlizzFrame("player", GHI_MiscData["stick_player_buffs"]);
	buff:StickToBlizzFrame("target", GHI_MiscData["stick_target_buffs"]);

	GHI_Event("PLAYER_TARGET_CHANGED", function(...)
		if UnitName("target") and (GHI_MiscData["stick_target_buffs"] == nil) then
			if TargetFrame:IsShown() then
				GHI_MiscData["stick_target_buffs"] = true;
			else
				GHI_MiscData["stick_target_buffs"] = false;
			end
			buff:StickToBlizzFrame("target", GHI_MiscData["stick_target_buffs"]);
		end
		CheckTarget();
	end);


	class.UpdateBuffSticking = function() -- used by options panel
		buff:StickToBlizzFrame("player", GHI_MiscData["stick_player_buffs"]);
		buff:StickToBlizzFrame("target", GHI_MiscData["stick_target_buffs"]);
	end

	class.CountBuffs = function(name, unit)
		unit = unit or "player";
		return buff:CountBuff(name, UnitGUID(unit))
	end

	class.RemoveBuff = function(name, filter, count, delay)
		if delay and delay > 0 then
			local t = {name, filter, count, 0};
			t.time = time();
			table.insert(delayedRemoveBuffs,t);
			return;
		end
		buff:RemoveBuff(name, UnitGUID("player"), count or 1,filter)
	end

	class.RemoveAllBuffs = function()
		buff:ClearAllBuffs(UnitGUID("player"));
		delayedBuffs = {};
	end

	CheckTarget = function()
		local unit = "target";
		local name, realm = UnitName(unit)
		local legalPlayerUnit = not (strlower(unit) == "player") and UnitIsPlayer(unit);
		local factionAvailability = UnitIsFriend(unit, "PLAYER") or (UnitIsPlayer(unit) and #(GHI_VersionInfo().GetPlayerAddOns(UnitName(unit))) > 0);
		if legalPlayerUnit and factionAvailability and realm == nil then
			if type(subscriptionsSend[name]) == "number" and (GetTime() - subscriptionsSend[name]) < 60 * 3 then
				return;
			end
			comm.Send("NORMAL", name, "BuffSubscribe", 60 * 5)
			subscriptionsSend[name] = GetTime();
		end
	end

	BuffRecieveSubscription = function(sender, subscriptionTime, requestedFromWrathProtocol, ...)
		if requestedFromWrathProtocol and (subscribedPlayers[sender] or 0) + subscriptionTime > GetTime() then --v.1.3 change
			return;
		end
		subscribedPlayers[sender] = GetTime() + subscriptionTime;
		SendBuffInfo(sender);
	end
	comm.AddRecieveFunc("BuffSubscribe", BuffRecieveSubscription)

	SendBuffInfo = function(players)
		local guid = UnitGUID("player");
		local buffs, debuffs = buff:Serialize(guid);

		if type(players) == "string" then
			local name = players;
			players = {};
			players[name] = "dummy";
		end


		for name, _ in pairs(players) do
			comm.Send("NORMAL", name, "BuffInfo", guid, buffs, debuffs)
		end
	end

	BuffRecieveInfo = function(sender, guid, buffData, debuffData, ...)
		buff:Deserialize(guid, buffData, debuffData);
		if buffData == false then --v.1.3
			subscriptionsSend[sender] = nil;
		end
	end

	BuffChanged = function(guid)
		if UnitGUID("player") == guid then
			UpdateBuffSubscriptions()
			SendBuffInfo(subscribedPlayers)
		end
	end


	UpdateBuffSubscriptions = function()
		for player, timeout in pairs(subscribedPlayers) do
			if timeout < GetTime() then
				subscribedPlayers[player] = nil;
			end
		end
	end

	buff:SetFeedbackFunc(BuffChanged)
	areaBuff.SetApplyBuffFunc(ApplyBuff);

	return class;
end

