--------------------------------------------------
-- BonusScanner v1.2
-- by Crowley <crowley@headshot.de>
-- performance improvements by Archarodim
--
-- get the latest version here:
-- http://ui.worldofwar.net/ui.php?id=1461
-- http://www.curse-gaming.com/mod.php?addid=2384
--------------------------------------------------

BONUSSCANNER_VERSION = "v1.2";


BONUSSCANNER_PATTERN_SETNAME = "^(.*) %(%d/%d%)$";
BONUSSCANNER_PATTERN_GENERIC_PREFIX = "^%+(%d+)%%?(.*)$";
BONUSSCANNER_PATTERN_GENERIC_SUFFIX = "^(.*)%+(%d+)%%?$";

BonusScanner = {
	bonuses = {};
	bonuses_details = {};

    IsUpdating		    = false; -- not sure if this check is needed but who knows with multithreading...
    MinCheckInterval	    = 1;	 -- Minimum time to wait between each scan
    CheckIntervalCounter    = 0;	 -- counter, do not change
    CheckForBonusPlease	    = 0;	 -- The flag that when set makes BonusScanner scan the equipment and call the update function
    ShowDebug		    = false; -- tells when the equipment is scanned

	active = nil;
	temp = { 
		sets = {},
		set = "",
		slot = "",
		bonuses = {},
		details = {}
	};

	types = {
		"STR", 			-- strength	
		"AGI", 			-- agility
		"STA", 			-- stamina
		"INT", 			-- intellect
		"SPI", 			-- spirit
		"ARMOR", 		-- reinforced armor (not base armor)
  
		"ARCANERES",	-- arcane resistance
		"FIRERES",  	-- fire resistance
		"NATURERES",	-- nature resistance 	
		"FROSTRES", 	-- frost resistance
		"SHADOWRES",	-- shadow resistance
	
		"FISHING",  	-- fishing skill
		"MINING",		-- mining skill
		"HERBALISM",	-- herbalism skill
		"SKINNING", 	-- skinning skill
		"DEFENSE",  	-- defense skill

		"BLOCK",    	-- chance to block
		"BLOCKVALUE",	-- increased block value
		"DODGE",		-- chance to dodge
		"PARRY",		-- chance to parry
		"ATTACKPOWER",	-- attack power
		"ATTACKPOWERUNDEAD", -- attack power against undead
		"ATTACKPOWERFERAL",  -- attack power in feral form
		
		"CRIT",			-- chance to get a critical strike
		"RANGEDATTACKPOWER", -- ranged attack power
		"RANGEDCRIT",	-- chance to get a crit with ranged weapons
		"TOHIT",		-- chance to hit

		"DMG",			-- spell damage
		"DMGUNDEAD",	-- spell damage against undead
		
		"ARCANEDMG",	-- arcane spell damage
		"FIREDMG",		-- fire spell damage
		"FROSTDMG",		-- frost spell damage
		"HOLYDMG",		-- holy spell damage
		"NATUREDMG",	-- nature spell damage
		"SHADOWDMG",	-- shadow spell damage
		"SPELLCRIT",	-- chance to crit with spells
		"HEAL",			-- healing 
		"HOLYCRIT", 	-- chance to crit with holy spells
		"SPELLTOHIT", 	-- Chance to Hit with spells

		"SPELLPEN", 	-- amount of spell resist reduction

		"HEALTHREG",	-- health regeneration per 5 sec.
		"MANAREG",		-- mana regeneration per 5 sec.
		"HEALTH",		-- health points
		"MANA",			-- mana points
	};

	slots = {
		"Head",
		"Neck",
		"Shoulder",
		"Shirt",
		"Chest",
		"Waist",
		"Legs",
		"Feet",
		"Wrist",
		"Hands",
		"Finger0",
		"Finger1",
		"Trinket0",
		"Trinket1",
		"Back",
		"MainHand",
		"SecondaryHand",
		"Ranged",
		"Tabard",
	};
}

-- Update function to hook into. 
-- Gets called, when Equipment changes (after UNIT_INVENTORY_CHANGED)
function BonusScanner_Update()
end

function BonusScanner:GetBonus(bonus)
	if(BonusScanner.bonuses[bonus]) then
		return BonusScanner.bonuses[bonus];
	end;
	return 0;
end

function BonusScanner:GetSlotBonuses(slotname)
	local i, bonus, details;
	local bonuses = {};
	for bonus, details in BonusScanner.bonuses_details do
		if(details[slotname]) then
			bonuses[bonus] = details[slotname];
		end
	end
	return bonuses;
end

function BonusScanner:GetBonusDetails(bonus)
	if(BonusScanner.bonuses_details[bonus]) then
		return BonusScanner.bonuses_details[bonus];
	end;
	return {};
end

function BonusScanner:GetSlotBonus(bonus, slotname)
	if(BonusScanner.bonuses_details[bonus]) then
		if(BonusScanner.bonuses_details[bonus][slotname]) then
			return BonusScanner.bonuses_details[bonus][slotname];
		end;
	end;
	return 0;
end


function BonusScanner:OnLoad()
	this:RegisterEvent("PLAYER_ENTERING_WORLD");
	this:RegisterEvent("PLAYER_LEAVING_WORLD");
end

function BonusScanner:OnEvent()

    BonusScanner:Debug(event);

    if ((event == "UNIT_INVENTORY_CHANGED") and BonusScanner.active) then
		BonusScanner.CheckForBonusPlease = 1;
	return;
    end
	if (event == "PLAYER_ENTERING_WORLD") then
		BonusScanner.active = 1;
		BonusScanner.CheckForBonusPlease = 1;
		this:RegisterEvent("UNIT_INVENTORY_CHANGED");
	return;
	end
	if (event == "PLAYER_LEAVING_WORLD") then
		this:UnregisterEvent("UNIT_INVENTORY_CHANGED");
	return;
    end	
end


-- A little debug function
function BonusScanner:Debug( Message )
    if (BonusScanner.ShowDebug) then
	DEFAULT_CHAT_FRAME:AddMessage("Bonus-Scanner: " .. Message, 0.5, 0.8, 1);
	end	
end

-- The use of the <OnUpdate></OnUpdate> *feature* avoid freezes and lags caused by the useless repeated call of BonusScanner:ScanEquipment()...
function BonusScanner:OnUpdate (elapsed)

    -- BonusScanner:Debug(elapsed);
    if (BonusScanner.IsUpdating) then
		return;
    end

    BonusScanner.IsUpdating = true;

    -- if the equipment has changed then check if we are allowed to test for bonuses
    if (BonusScanner.CheckForBonusPlease == 1) then

	BonusScanner.CheckIntervalCounter = BonusScanner.CheckIntervalCounter + elapsed;

	-- if we have wait long enough then proceed...
	if (BonusScanner.CheckIntervalCounter > BonusScanner.MinCheckInterval) then
	    BonusScanner.CheckForBonusPlease = 2; -- means we are currently checking
	    BonusScanner:ScanEquipment(); -- scan the equiped items
	    BonusScanner_Update();	  -- call the update function (for the mods using this library)
	    if (BonusScanner.CheckForBonusPlease ~= 1) then -- if no other update has been requested
		BonusScanner.CheckForBonusPlease = 0;
	    end
	    BonusScanner.CheckIntervalCounter = 0;
	end
    end

    BonusScanner.IsUpdating = false;
end

function BonusScanner:ScanEquipment()
	local slotid, slotname, hasItem, i;

	BonusScannerTooltip:SetOwner(UIParent, "ANCHOR_NONE");

    BonusScanner:Debug("Scanning Equipment has requested");

	BonusScanner.temp.bonuses = {};
	BonusScanner.temp.details = {};
	BonusScanner.temp.sets = {};
	BonusScanner.temp.set = "";

	for i, slotname in BonusScanner.slots do
		slotid, _ = GetInventorySlotInfo(slotname.. "Slot");
		hasItem = BonusScannerTooltip:SetInventoryItem("player", slotid);
	
		if ( hasItem ) then
			BonusScanner.temp.slot = slotname;
			BonusScanner:ScanTooltip();
			-- if set item, mark set as already scanned
			if(BonusScanner.temp.set ~= "") then
				BonusScanner.temp.sets[BonusScanner.temp.set] = 1;
			end;
		end
	end
	BonusScanner.bonuses = BonusScanner.temp.bonuses;
	BonusScanner.bonuses_details = BonusScanner.temp.details;
end

function BonusScanner:ScanItem(itemlink)
	local name = GetItemInfo(itemlink);
	if(name) then
		BonusScanner.temp.bonuses = {};
		BonusScanner.temp.sets = {};
		BonusScanner.temp.set = "";
		BonusScanner.temp.slot = "";
		BonusScannerTooltip:ClearLines();
		BonusScannerTooltip:SetHyperlink(itemlink);
		BonusScanner:ScanTooltip();
		return BonusScanner.temp.bonuses;
	end
	return false;
end

function BonusScanner:ScanTooltip()
	local tmpTxt, line;
	local lines = BonusScannerTooltip:NumLines();

	for i=2, lines, 1 do
		tmpText = getglobal("BonusScannerTooltipTextLeft"..i);
		val = nil;
		if (tmpText:GetText()) then
			line = tmpText:GetText();
			BonusScanner:ScanLine(line);
		end
	end
end

function BonusScanner:AddValue(effect, value)
	local i,e;
	
	if(type(effect) == "string") then
		if(BonusScanner.temp.bonuses[effect]) then
			BonusScanner.temp.bonuses[effect] = BonusScanner.temp.bonuses[effect] + value;
		else
			BonusScanner.temp.bonuses[effect] = value;
		end
		
		if(BonusScanner.temp.slot) then
			if(BonusScanner.temp.details[effect]) then
				if(BonusScanner.temp.details[effect][BonusScanner.temp.slot]) then
					BonusScanner.temp.details[effect][BonusScanner.temp.slot] = BonusScanner.temp.details[effect][BonusScanner.temp.slot] + value;
				else
					BonusScanner.temp.details[effect][BonusScanner.temp.slot] = value;
				end
			else
				BonusScanner.temp.details[effect] = {};
				BonusScanner.temp.details[effect][BonusScanner.temp.slot] = value;
			end
		end;
	else 
	-- list of effects
		if(type(value) == "table") then
			for i,e in effect do
				BonusScanner:AddValue(e, value[i]);
			end
		else
			for i,e in effect do
				BonusScanner:AddValue(e, value);
			end
		end
	end
end;

function BonusScanner:ScanLine(line)
	local tmpStr, found;
	-- Check for "Equip: "
	if(string.sub(line,0,string.len(BONUSSCANNER_PREFIX_EQUIP)) == BONUSSCANNER_PREFIX_EQUIP) then

		tmpStr = string.sub(line,string.len(BONUSSCANNER_PREFIX_EQUIP)+1);
		BonusScanner:CheckPassive(tmpStr);

	-- Check for "Set: "
	elseif(string.sub(line,0,string.len(BONUSSCANNER_PREFIX_SET)) == BONUSSCANNER_PREFIX_SET
			and BonusScanner.temp.set ~= "" 
			and not BonusScanner.temp.sets[BonusScanner.temp.set]) then

		tmpStr = string.sub(line,string.len(BONUSSCANNER_PREFIX_SET)+1);
		BonusScanner.temp.slot = "Set";
		BonusScanner:CheckPassive(tmpStr);

	-- any other line (standard stats, enchantment, set name, etc.)
	else
		-- Check for set name
		_, _, tmpStr = string.find(line, BONUSSCANNER_PATTERN_SETNAME);
		if(tmpStr) then
			BonusScanner.temp.set = tmpStr;
		else
			found = BonusScanner:CheckGeneric(line);
			if(not found) then
				BonusScanner:CheckOther(line);
			end;
		end
	end
end;


-- Scans passive bonuses like "Set: " and "Equip: "
function BonusScanner:CheckPassive(line)
	local i, p, value, found;

	found = nil;
	for i,p in BONUSSCANNER_PATTERNS_PASSIVE do
		_, _, value = string.find(line, "^" .. p.pattern);
		if(value) then
			BonusScanner:AddValue(p.effect, value)
			found = 1;
			break; -- prevent duplicated patterns to cause bonuses to be counted several times
		end
	end
	if(not found) then
		BonusScanner:Debug("\"" .. line .. "\"");
		BonusScanner:CheckGeneric(line);
	end
end


-- Scans generic bonuses like "+3 Intellect" or "Arcane Resistance +4"
function BonusScanner:CheckGeneric(line)
	local value, token, pos, tmpStr, found;

	-- split line at "/" for enchants with multiple effects
	found = false;
	while(string.len(line) > 0) do
		pos = string.find(line, "/", 1, true);
		if(pos) then
			tmpStr = string.sub(line,1,pos-1);
			line = string.sub(line,pos+1);
		else
			tmpStr = line;
			line = "";
		end

		-- trim line
	    tmpStr = string.gsub( tmpStr, "^%s+", "" );
   	    tmpStr = string.gsub( tmpStr, "%s+$", "" );
       	tmpStr = string.gsub( tmpStr, "%.$", "" );

		_, _, value, token = string.find(tmpStr, BONUSSCANNER_PATTERN_GENERIC_PREFIX);
		if(not value) then
			_, _,  token, value = string.find(tmpStr, BONUSSCANNER_PATTERN_GENERIC_SUFFIX);
		end
		if(token and value) then
			-- trim token
		    token = string.gsub( token, "^%s+", "" );
    	    token = string.gsub( token, "%s+$", "" );
	       	token = string.gsub( token, "%.$", "" );
	
			if(BonusScanner:CheckToken(token,value)) then
				found = true;
			end
		end
	end
	return found;
end


-- Identifies simple tokens like "Intellect" and composite tokens like "Fire damage" and 
-- add the value to the respective bonus. 
-- returns true if some bonus is found
function BonusScanner:CheckToken(token, value)
	local i, p, s1, s2;
	
	if(BONUSSCANNER_PATTERNS_GENERIC_LOOKUP[token]) then
		BonusScanner:AddValue(BONUSSCANNER_PATTERNS_GENERIC_LOOKUP[token], value);
		return true;
	else
		s1 = nil;
		s2 = nil;
		for i,p in BONUSSCANNER_PATTERNS_GENERIC_STAGE1 do
			if(string.find(token,p.pattern,1,1)) then
				s1 = p.effect;
			end
		end	
		for i,p in BONUSSCANNER_PATTERNS_GENERIC_STAGE2 do
			if(string.find(token,p.pattern,1,1)) then
				s2 = p.effect;
			end
		end	
		if(s1 and s2) then
			BonusScanner:AddValue(s1..s2, value);
			return true;
		end 
	end
	return false;
end

-- Last fallback for non generic enchants, like "Mana Regen x per 5 sec."
function BonusScanner:CheckOther(line)
	local i, p, value, start, found;

	for i,p in BONUSSCANNER_PATTERNS_OTHER do
		start, _, value = string.find(line, "^" .. p.pattern);
		if(start) then
			if(p.value) then
				BonusScanner:AddValue(p.effect, p.value)
			elseif(value) then
				BonusScanner:AddValue(p.effect, value)
			end
			return true;
		end
	end
	return false;
end



-- Slash Command functions

function BonusScanner_Cmd(cmd)

	local _, _, itemlink, itemid = string.find(cmd, "|c%x+|H(item:(%d+):%d+:%d+:%d+)|h%[.-%]|h|r");
	if(itemid) then
  		local name = GetItemInfo(itemid);
		if(name) then
			DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "Item bonuses of: " .. HIGHLIGHT_FONT_COLOR_CODE .. name);
			local bonuses = BonusScanner:ScanItem(itemlink);
			if(not bonuses) then
				DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "error scanning item (probably not cached)");
			else
	  			BonusScanner:PrintInfo(bonuses);
			end
  		end
  		return;
  	end
  	if(string.lower(cmd) == "show") then
	  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "Current equipment bonuses:");
		BonusScanner:PrintInfo(BonusScanner.bonuses);
  		return;
  	end
  	if(string.lower(cmd) == "details") then
	  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "Current equipment bonus details:");
		BonusScanner:PrintInfoDetailed();
  		return;
  	end
	for i, slotname in BonusScanner.slots do
		if(string.lower(cmd) == string.lower(slotname)) then
		  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "Bonuses of '"..LIGHTYELLOW_FONT_COLOR_CODE .. slotname .. GREEN_FONT_COLOR_CODE .. "' slot:");
		  	local bonuses = BonusScanner:GetSlotBonuses(slotname);
		  	BonusScanner:PrintInfo(bonuses);
		  	return
		end;
	end
  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "BonusScanner " .. BONUSSCANNER_VERSION .. " by Crowley");
  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "/bscan show - shows all bonus of the current equipment");
  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "/bscan details - shows bonuses with slot distribution");
  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "/bscan <itemlink> - shows bonuses of linked item (insert link with Shift-Click)");
  	DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. "/bscan <slotname> - shows bonuses of given equipment slot");
end

SLASH_BONUSSCANNER1 = "/bonusscanner";
SLASH_BONUSSCANNER2 = "/bscan";
SlashCmdList["BONUSSCANNER"] = BonusScanner_Cmd;


function BonusScanner:PrintInfoDetailed()
	local bonus, name, i, j, slot, first, s;
	for i, bonus in BonusScanner.types do
		if(BonusScanner.bonuses[bonus]) then
			first = true;
			s = "(";
			for j, slot in BonusScanner.slots do 
				if(BonusScanner.bonuses_details[bonus][slot]) then
					if(not first) then
						s = s .. ", ";
					else
						first = false;
					end
					s = s .. 	LIGHTYELLOW_FONT_COLOR_CODE .. slot .. 
								HIGHLIGHT_FONT_COLOR_CODE .. ": " .. BonusScanner.bonuses_details[bonus][slot];
				end
			end;
			if(BonusScanner.bonuses_details[bonus]["Set"]) then
				if(not first) then
					s = s .. ", ";
				end
				s = s .. 	LIGHTYELLOW_FONT_COLOR_CODE .. "Set" .. 
							HIGHLIGHT_FONT_COLOR_CODE .. ": " .. BonusScanner.bonuses_details[bonus]["Set"];
				end
			s = s .. ")";
			DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. BONUSSCANNER_NAMES[bonus] .. ": " .. HIGHLIGHT_FONT_COLOR_CODE .. BonusScanner.bonuses[bonus] .. " " .. s);
		end
	end
end

function BonusScanner:PrintInfo(bonuses)
	local bonus, i;
	for i, bonus in BonusScanner.types do
		if(bonuses[bonus]) then
			DEFAULT_CHAT_FRAME:AddMessage(GREEN_FONT_COLOR_CODE .. BONUSSCANNER_NAMES[bonus] .. ": " .. HIGHLIGHT_FONT_COLOR_CODE .. bonuses[bonus]);
		end
	end
end
