--[[
	DynamicData

	By sarf

	This AddOn allows you to access dynamic data in WoW without being forced to rely on strange Blizzard functions

	Thanks goes to the Sea people, the Cosmos team and finally the nice (but strange) people at 
	 #cosmostesters and Blizzard.
	
	CosmosUI URL:
	http://www.cosmosui.org/forums/viewtopic.php?t=NOT_YET_ANNOUNCED
	
   ]]


-- Utility functions
DynamicData.util = {

-- public functions	

	-- 
	-- addOnWhateverHandler (func)
	--
	--  Adds a function name that will be called on whatever.
	--
	addOnWhateverHandler = function (list, func)
		for k, v in list do
			if ( v == func ) then
				return false;
			end
		end
		table.insert(list, func);
		return true;
	end;

	-- 
	-- removeOnWhateverHandler (list, func)
	--
	--  Removes the specified function, so that it will not be called on whatever.
	--
	removeOnWhateverHandler = function (list, func)
		local index = nil;
		for k, v in list do
			if ( v == func ) then
				index = k;
				break;
			end
		end
		if ( index ) then
			table.remove(list, index);
			return true;
		else
			return false;
		end
	end;

	-- 
	-- notifyWhateverHandlers (list, ...)
	--
	-- Args: 
	--  list - the list of handlers
	-- 
	--  Notifies all handlers that an Whatever has occurred.
	--
	notifyWhateverHandlers = function (list, ...) 
		local func = nil;
		for k, v in list do
			func = v;
			if ( arg ) then
				func(unpack(arg));
			else
				func();
			end
		end
	end;

	--
	-- removeEntryFromList (value, list, limit)
	--
	--  Removes value from the list, up to limit times. 
	--  If limit is not specified, it will be removed once. If limit is 0 or lower, all values will be removed.
	--
	removeEntryFromList = function (value, list, limit)
		if ( not limit ) then limit = 1; end
		if ( limit <= 0 ) then limit = -1; end
		local hasRemoved = false;
		local found = true;
		local index = 1;
		while ( found ) do
			found = false;
			index = DynamicData.util.getIndexInList(value, list);
			if ( index ) then
				found = true;
				table.remove(list, index);
				hasRemoved = true;
				if ( limit > 0 ) then
					limit = limit - 1;
				end
				if ( limit == 0 ) then
					break;
				end
			end
		end
		return hasRemoved;
	end;

	--
	-- isThingyEqual(value, valueCheck)
	--
	--  compares two things and sees if they are equal to each other
	-- 
	isThingyEqual = function(value, valueCheck)
		if ( value == valueCheck ) then
			return true;
		else
			if ( type(valueCheck) == "table" ) then
				return DynamicData.util.isStringInList(value, valueCheck);
			end
			return false;
		end
	end;

	--
	-- getIndexInList (value, list, start)
	--
	--  Retrieves the index of value in list, starting at start.
	--  Start is 1 if unspecified.
	--
	getIndexInList = function (value, list, start)
		if ( not value ) then
			return nil;
		end
		if ( not list ) then
			return nil;
		end
		if ( type(list) ~= "table" ) and ( type(value) == "table" ) then
			local tmp = list;
			list = value;
			value = tmp;
		end
		local found = true;
		local index = nil;
		local listSize = table.getn(list);
		local oldStart = start;
		if ( listSize > 0 ) then
			if ( not start ) then start = 1; end
			for i = start, listSize do
				if ( DynamicData.util.isThingyEqual(list[i], value ) ) then
					index = i;
					break;
				end
			end
			if ( not index ) then
				for k, v in list do
					if ( ( not oldStart ) or ( oldStart >= k ) ) and ( DynamicData.util.isThingyEqual(v, value ) ) then
						index = k;
						break;
					end
				end
			end
		else
			for k, v in list do
				if ( DynamicData.util.isThingyEqual(v, value ) ) then
					index = k;
					break;
				end
			end
		end
		return index;
	end;


	--
	-- isStringInList (str, list)
	--
	--  Returns true if str is a value of list.
	--
	isStringInList = function (param1, param2)
		if ( not param1 ) or ( not param2 ) then
			return false;
		end
		local str = param1;
		local list = param2;
		if ( type(param1) == "table" ) then
			str = param2;
			list = param1;
		end
		for k, v in list do
			if ( v == str ) then
				return true;
			end
		end
		return false;
	end;
	
	--
	-- safeToUseTooltips ()
	--
	--  Checks if it is "safe" to use tooltips to dump information.
	--  Basically, it checks if one (or more) particular frames are visible.
	--  If they are, it is not safe to use tooltips.
	safeToUseTooltips = function (ignoreList)
		local frame = nil;
		local list = {};
		if ( DynamicData.util.tooltipUsingFrames ) then
			for k, v in DynamicData.util.tooltipUsingFrames do
				list[v] = 1;
			end
		end
		if ( ignoreList ) then
			for k, v in ignoreList do
				list[v] = nil;
			end
		end
		for k, v in list do
			frame = getglobal(k);
			if ( ( frame ) and ( frame:IsVisible() ) ) then
				return false;
			end
		end
		return true;
	end;

	--
	-- clearTooltipStrings (tooltipName)
	--
	--  Clears a tooltip of text.
	--
	clearTooltipStrings = function (tooltipName)
		local tooltip = getglobal(tooltipName);
		if ( tooltip ) then
			local textObj = nil;
			for i = 1, 15 do
				textObj = getglobal(tooltipName.."TextLeft"..i);
				if ( textObj ) then
					textObj:SetText("");
				end
				textObj = getglobal(tooltipName.."TextRight"..i);
				if ( textObj ) then
					textObj:SetText("");
				end
			end
		end
	end;

	--
	-- getNameFromTooltip (tooltipName)
	--
	--  Retrieves the name from the specified tooltip.
	--
	getNameFromTooltip = function (tooltipName)
		local strings = DynamicData.util.getTooltipStrings(tooltipName);
		if ( strings ) and ( strings[1] ) then
			return strings[1].left;
		else
			return nil;
		end
	end;
	
	--
	-- getTooltipStrings (tooltipName)
	--
	--  Retrieves strings from the specified tooltip.
	--  It basically takes all strings in the tooltip and puts them in an array and returns the array.
	--  All credit to Sea for this one, even though it is not copy/pasted! ;)
	--
	getTooltipStrings = function (tooltipName)
		local strings = {};
		local tooltip = getglobal(tooltipName);
		if ( tooltip ) then
			local textObj = nil;
			local textLeft = nil;
			local textRight = nil;
			for i = 1, 15 do
				strings[i] = {};
				textObj = getglobal(tooltipName.."TextLeft"..i);
				if ( textObj ) then
					textLeft = textObj:GetText();
				else
					textLeft = nil;
				end
				strings[i].left = textLeft;
				textObj = getglobal(tooltipName.."TextRight"..i);
				if ( textObj ) then
					textRight = textObj:GetText();
				else
					textRight = nil;
				end
				strings[i].right = textRight;
			end
		end
		return strings;
	end;

	--
	-- getItemTooltipInfo (bag, slot, tooltipName)
	--
	--  Retrieves info about a particular item.
	--
	getItemTooltipInfo = function (bag, slot, tooltipName)
		if ( not tooltipName ) then
			tooltipName = "DynamicDataTooltip";
		end
		local tooltip = getglobal(tooltipName);
		local strings = nil;
		DynamicData.util.clearTooltipStrings(tooltipName);
		if ( bag > -1 ) then
			DynamicData.util.protectTooltipMoney();
			tooltip:SetBagItem(bag, slot);
			DynamicData.util.unprotectTooltipMoney();
			strings = DynamicData.util.getTooltipStrings(tooltipName);
		else
			if ( slot == -1 ) then
				return nil;
			end
			DynamicData.util.protectTooltipMoney();
			local hasItem, hasCooldown = tooltip:SetInventoryItem("player", slot);
			DynamicData.util.unprotectTooltipMoney();
			strings = DynamicData.util.getTooltipStrings(tooltipName);
			if ( not hasItem) then
				DynamicData.util.clearTooltipStrings(tooltipName);
				if ( strings[1] ) then
					strings[1].left = "";
				end
			end
		end
		return strings;
	end;

	--
	-- getCurrentAndMaxDurability (strings)
	--
	--  Extracts durability info from an array that correspond to a tooltip.
	--
	getCurrentAndMaxDurability = function (strings)
		if ( not strings ) then
			return nil, nil;
		end
		local index = nil;
		local index2 = nil;
		local tmp = nil;
		for k, v in strings do
			if ( v.left )  then
				index = strfind(v.left, DYNAMICDATA_DURABILITY);
				if ( index ) then
					tmp = strsub(v.left, strlen(DYNAMICDATA_DURABILITY)+1);
					while ( strsub(tmp, 1, 1) == " " ) do
						tmp = strsub(tmp, 2);
					end
					index = strfind(tmp, " ");
					index2 = strfind(tmp, "/");
					if ( index2 < index ) then
						local indexTmp = index;
						index = index2;
						index2 = indexTmp;
					end
					local currentDurability = tonumber(strsub(tmp, 1, index-1));
					tmp = strsub(tmp, index2+1);
					while ( strsub(tmp, 1, 1) == " " ) do
						tmp = strsub(tmp, 2);
					end
					index = strfind(tmp, " ");
					if ( not index ) then
						index = strlen(tmp);
					end
					local maxDurability = tonumber(strsub(tmp, 1, index));
					return currentDurability, maxDurability;
				end					
			end
		end
		return nil, nil;
	end;
	
	--
	-- getDPS (strings)
	--
	--  Extracts DPS info from an array that correspond to a tooltip.
	--
	getDPS = function (strings)
		if ( not strings ) then
			return nil, nil;
		end
		local tmpNumber = nil;
		local tmp = nil;
		for k, v in strings do
			for tmp in string.gfind(v.left, DYNAMICDATA_DPS_GSUB) do 
				tmpNumber = tonumber(tmp);
				if ( tmpNumber ) then
					return tmpNumber;
				end
			end
		end
		return nil;
	end;
	
	
	--
	-- getItemNameFromLink (link)
	--  
	--  retrieves an item name from a link or nil if it fails
	--  Thanks to Telo for providing me with the code!
	--
	getItemNameFromLink = function (link)
		local name;
		if( not link ) then
			return nil;
		end
		for name in string.gfind(link, "|c%x+|Hitem:%d+:%d+:%d+:%d+|h%[(.-)%]|h|r") do
			return name;
		end
		return nil;
	end;

	--
	-- isItemNotBindOnAnything (strings)
	--
	--  Checks if the item represented by the parameter is not bind on anything.
	--
	isItemNotBindOnAnything = function (strings)
		if ( not strings ) then
			return false;
		end
		local tmpNumber = nil;
		local tmp = nil;
		for k, v in strings do
			if ( ( v.left ) and ( string.find(v.left, DYNAMICDATA_BIND_ON) ) ) then
				return true;
			end
		end
		return false;
	end;
	
	--
	-- getDamage (strings)
	--
	--  Extracts damage info from an array that correspond to a tooltip.
	--
	getDamage = function (strings)
		if ( not strings ) then
			return nil, nil;
		end
		local tmpNumber = nil;
		local tmp = nil;
		for k, v in strings do
			if ( v.left )  then
				local firstNumber = nil;
				for tmp in string.gfind(v.left, "%w+") do 
					tmpNumber = tonumber(tmp);
					if ( tmpNumber ) then
						if ( firstNumber ) then
							return firstNumber, tmpNumber;
						else
							firstNumber = tmpNumber;
						end
					end
				end
			end
		end
		return nil, nil;
	end;

	--
	-- getItemStringType (strings)
	--
	--  Extracts item type information from an array that correspond to a tooltip.
	--
	getItemStringType = function (strings)
		if ( not strings ) then
			return nil;
		end
		local str = nil;
		if ( ( strings[2] ) and ( strings[2].right ) ) then
			str = strings[2].right;
		end
		if ( ( str ) and ( strlen(str) <= 0 ) ) then
			if ( ( strings[3] ) and ( strings[3].right ) ) then
				str = strings[3].right;
			end
		end
		if ( ( str ) and ( strlen(str) <= 0 ) ) then
			if ( ( strings[4] ) and ( strings[4].right ) ) then
				str = strings[4].right;
			end
		end
		return str;
	end;

	--
	-- extractWeaponSpeed (str)
	--
	--  Extracts weapon speed information from a string.
	--
	extractWeaponSpeed = function (str)
		local speed = -1;
		local tmp = str;
		local index = strfind(tmp, DYNAMICDATA_WEAPON_SPEED);
		if ( index ) then
			tmp = strsub(tmp, index+1);
			while ( strsub(tmp, 1, 1) == " " ) do
				tmp = strsub(tmp, 2);
			end
			speed = tonumber(tmp);
			if ( not speed ) then
				speed = -1;
			end
		end
		return speed;
	end;

	--
	-- getWeaponSpeed (strings)
	--
	--  Extracts weapon speed information from an array that correspond to a tooltip.
	--
	getWeaponSpeed = function (strings)
		if ( not strings ) then
			return nil;
		end
		local str = "";
		if ( ( strings[2] ) and ( strings[2].right ) ) then
			str = strings[2].right;
		end
		if ( strlen(str) <= 0 ) then
			if ( ( strings[3] ) and ( strings[3].right ) ) then
				str = strings[3].right;
			end
		else
		end
		if ( strlen(str) <= 0 ) then
			if ( ( strings[4] ) and ( strings[4].right ) ) then
				str = strings[4].right;
			end
		end
		local speed = DynamicData.util.extractWeaponSpeed(str);
		if ( speed <= 0 ) then
			return nil;
		else
			return speed;
		end
	end;

	--
	-- protectTooltipMoney ()
	--
	--  Thanks to Telo for providing this solution!
	--  Prevents the clearing of money from tooltips.
	--  USE WITH CAUTION! ALWAYS CALL unhookMoneyTooltip() AFTER SETTING THE TOOLTIP!
	--
	protectTooltipMoney = function()
		if ( not DynamicData.util.saved_GameTooltip_ClearMoney ) then
			DynamicData.util.saved_GameTooltip_ClearMoney = GameTooltip_ClearMoney;
			GameTooltip_ClearMoney = DynamicData.util.doNothing;
		end
	end;

	--
	-- unprotectTooltipMoney ()
	--
	--  Thanks to Telo for providing this solution!
	--  Allows the clearing of money from tooltips.
	--
	unprotectTooltipMoney = function()
		if ( DynamicData.util.saved_GameTooltip_ClearMoney ) then
			GameTooltip_ClearMoney = DynamicData.util.saved_GameTooltip_ClearMoney;
			DynamicData.util.saved_GameTooltip_ClearMoney = nil;
		end
	end;

	--
	-- scheduleFunction(when, function, [arguments])
	--
	--  schedules a function to occur in WHEN seconds.
	-- 
	scheduleFunction = function(when, func, ...)
		if ( not when ) then
			return;
		end
		if ( not func ) then
			return;
		end
		local scheduledFunction = {};
		scheduledFunction.time = when+GetTime();
		scheduledFunction.func = func;
		scheduledFunction.args = arg;
		DynamicData.util.addScheduledFunction(scheduledFunction);
	end;

	--
	-- scheduleNamedFunction(name, when, function, [arguments])
	--
	--  schedules a function to occur in WHEN seconds. Only one function with the same name will be scheduled.
	-- 
	scheduleNamedFunction = function(name, when, func, ...)
		if ( not name ) then
			if ( arg ) then
				return DynamicData.util.scheduleFunction(when, func, unpack(arg));
			else
				return DynamicData.util.scheduleFunction(when, func);
			end
		end
		if ( not func ) then
			return;
		end
		local scheduledFunction = {};
		scheduledFunction.name = name;
		scheduledFunction.time = when+GetTime();
		scheduledFunction.func = func;
		scheduledFunction.args = arg;
		local i;
		local size = table.getn(DynamicData.util.scheduledFunctions);
		local entry = nil;
		for i=1, size, 1 do
			entry = DynamicData.util.scheduledFunctions[i];
			if ( ( entry.name ) and ( entry.name == name ) ) then
				table.remove(DynamicData.util.scheduledFunctions, i);
				break;
			end
		end
		DynamicData.util.addScheduledFunction(scheduledFunction);
	end;

	--
	-- scheduleAfterInitFunction (function, [args] )
	--
	--  schedules a function to be run after the init.
	--
	scheduleAfterInitFunction = function (func, ...) 
		local playerName = UnitName("player");
		if ( ( playerName ) and ( playerName ~= TEXT(UKNOWNBEING) ) and ( DynamicData.util.variablesHasBeenLoaded ) ) then
			if ( arg ) then
				func(unpack(arg));
			else
				func();
			end
		else
			if ( arg ) then
				DynamicData.util.scheduleFunction(1, DynamicData.util.scheduleAfterInitFunction, func, unpack(arg));
			else
				DynamicData.util.scheduleFunction(1, DynamicData.util.scheduleAfterInitFunction, func);
			end
		end
	end;
	
-- protected functions

	-- 
	-- postpone (params)
	--
	--  params.schedulingName = scheduling name
	--  params.func = function name
	--  params.args = function arguments
	--  params.lastPostponedName = name of the variable with last postponed information
	--  params.lastUpdatedName = name of the variable with last updated information
	--  params.minimumDelay = the smallest amount of time between function calls
	--
	postpone = function (params)
		local curTime = GetTime();
		if ( not params.minimumDelay ) then
			params.minimumDelay = 0.1;
		end
		if ( not params.lastPostponedName ) then
			params.lastPostponedName = params.schedulingName.."_lastPostponedName";
		end
		if ( not params.lastUpdatedName ) then
			params.lastUpdatedName = params.schedulingName.."_lastUpdatedName";
		end
		setglobal(params.lastPostponedName, curTime);
		-- postpone checking until the updates have stopped
		if ( params.schedulingName ) then
			Cosmos_ScheduleByName(params.schedulingName, params.minimumDelay+0.01, DynamicData.util.handlePostponement, params);
		else
			Cosmos_Schedule(params.minimumDelay+0.01, DynamicData.util.handlePostponement, params);
		end
	end;


	-- 
	-- handlePostponement (params)
	--
	--  params.schedulingName = scheduling name
	--  params.func = function name
	--  params.args = function arguments
	--  params.lastPostponedName = name of the variable with last postponed information
	--  params.lastUpdatedName = name of the variable with last updated information
	--  params.minimumDelay = the smallest amount of time between function calls
	--
	handlePostponement = function (params)
		local curTime = GetTime();
		local lastPostponed = getglobal(params.lastPostponedName);
		local lastUpdated = getglobal(params.lastUpdatedName);
		if ( not lastPostponed ) then
			lastPostponed = 0;
		end
		if ( not lastUpdated ) then
			lastUpdated = 0;
		end
		if ( ( lastPostponed == 0 ) or ( curTime-lastPostponed <= params.minimumDelay ) and ( params.allowInitialUpdate == 1 ) ) then
			postpone(params);
			return;
		end
		if ( curTime-lastUpdated <= params.minimumDelay ) then
			postpone(params);
			return;
		end
		setglobal(params.lastPostponedName, 0);
		setglobal(params.lastUpdatedName, curTime);
		local f = params.func;
		if ( f ) then
			if ( params.args ) then
				f(unpack(params.args));
			else
				f();
			end
		end
	end;
-- private functions	

	--
	-- addTooltipUsingFrame (frame)
	--
	--  Adds a frame to the list of frames that want tooltips to be untouched.
	--
	addTooltipUsingFrame = function(frame)
		if ( not frame ) then
			return;
		end
		local frameName = nil;
		if ( type(frame) == "string" ) then
			frameName = frame;
		else
			frameName = frame:GetName();
		end
		if ( not frameName ) then
			return;
		end
		for k, v in DynamicData.util.tooltipUsingFrames do
			if ( v == frameName ) then
				return;
			end
		end
		table.insert(DynamicData.util.tooltipUsingFrames, frameName);
	end;


	--
	-- doNothing()
	--
	--  Does nothing.
	--
	doNothing = function()
	end;

	--
	-- handleScheduledFunctions(elapsed)
	--
	--  handles scheduled functions
	-- 
	handleScheduledFunctions = function(elapsed)
		local curTime = GetTime();
		local scheduledFunction = {};
		while ( ( DynamicData.util.scheduledFunctions[1] ) and ( DynamicData.util.scheduledFunctions[1].time <= curTime ) ) do
			scheduledFunction = table.remove(DynamicData.util.scheduledFunctions, 1);
			if ( scheduledFunction.args) then
				scheduledFunction.func(unpack(scheduledFunction.args));
			else
				scheduledFunction.func();
			end
		end
	end;
	
	--
	-- OnLoad()
	--
	--  sets up the util library. NOTE: This inserts an emulated Cosmos_Schedule if not present.
	--
	OnLoad = function()
		DynamicData.util.addOnTimeUpdateHandler(handleScheduledFunctions);
		if ( not Cosmos_Schedule ) then
			Cosmos_Schedule = DynamicData.util.scheduleFunction;
		end
		if ( not Cosmos_ScheduleByName ) then
			Cosmos_ScheduleByName = DynamicData.util.scheduleNamedFunction;
		end
		if ( not Cosmos_AfterInit ) then
			Cosmos_AfterInit = DynamicData.util.scheduleAfterInitFunction;
		end
	end;

	--
	-- addOnTimeUpdateHandler(func)
	--
	--  adds the func to the OnTime update handlers
	--
	addOnTimeUpdateHandler = function (func)
		DynamicData.util.addOnWhateverHandler(DynamicData.util.onUpdateHandlers, func);
	end;

	--
	-- removeOnTimeUpdateHandler(func)
	--
	--  removes the func from the OnTime update handlers
	--
	removeOnTimeUpdateHandler = function (func)
		DynamicData.util.removeOnWhateverHandler(DynamicData.util.onUpdateHandlers, func);
	end;

	--
	-- variablesLoaded()
	--
	--  run on VARIABLES_LOADED event
	--
	variablesLoaded = function ()
		variablesHasBeenLoaded = true;
	end;
	
	--
	-- addScheduledFunction(scheduledFunction)
	--
	--  schedules a function to occur sometime
	-- 
	addScheduledFunction = function(scheduledFunction)
		local i = 1;
		while ( ( DynamicData.util.scheduledFunctions[i] ) and ( DynamicData.util.scheduledFunctions[i].time < scheduledFunction.time ) ) do
			i = i + 1;
		end
		table.insert(DynamicData.util.scheduledFunctions, i, scheduledFunction);
	end;

-- variables

	variablesHasBeenLoaded = false;

	scheduledFunctions = {};

	onUpdateHandlers = {};

	saved_GameTooltip_ClearMoney = nil;

	-- contains default frames which should prevent tooltips from being parsed.
	tooltipUsingFrames = {
		"TaxiFrame", "MerchantFrame", "TradeSkillFrame", 
		"SuggestFrame", "WhoFrame", "AuctionFrame", "MailFrame" 
	};

};



