Quantcast
local gtt = GameTooltip;

-- Create Examiner Frame
local modName = ...;
local ex = CreateFrame("Frame",modName,UIParent);

-- Global Chat Message Function
function AzMsg(msg) DEFAULT_CHAT_FRAME:AddMessage(tostring(msg):gsub("|1","|cffffff80"):gsub("|2","|cffffffff"),0.5,0.75,1.0); end

-- Local Saved Tables
local cfg, cache;

-- Data Tables
local info = { Sets = {}, Items = {} };
local unitStats = {};
local equippedSlots = {};
local statTipStats1, statTipStats2 = {}, {};
ex.compareStats = {};
ex.unitStats = unitStats;
ex.info = info;

-- Misc Constants
local HOOK_DEFAULT_INSPECT = true;	-- Hook Default Inspect Frame -- Az: Disable for debugging
local DELAYED_INSPECT_TIME = 1;
local CLASSIFICATION_NAMES = {
	worldboss = BOSS,
	rareelite = ITEM_QUALITY3_DESC..ELITE,
	elite = ELITE,
	rare = ITEM_QUALITY3_DESC,
};

-- Colors
local VERTEX_COLOR_NORMAL = { 1, 1, 1 };
local VERTEX_COLOR_CACHED = { 1, 1, 0.4 };
local VERTEX_COLOR_LOADING = { 0.5, 1, 0.5 };
local CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS;

-- Texture Mapping
local TALENT_BACKGROUNDS = {
	"DeathKnightBlood", "DeathKnightFrost", "DeathKnightUnholy",
	"DruidBalance", "DruidFeralCombat", "DruidRestoration",
	"HunterBeastMastery", "HunterMarksmanship", "HunterSurvival",
--	"HunterPetCunning", "HunterPetFerocity", "HunterPetTenacity",
	"MageArcane", "MageFire", "MageFrost",
	"PaladinCombat", "PaladinHoly", "PaladinProtection",
	"PriestDiscipline", "PriestHoly", "PriestShadow",
	"RogueAssassination", "RogueCombat", "RogueSubtlety",
	"ShamanElementalCombat", "ShamanEnhancement", "ShamanRestoration",
	"WarlockCurses", "WarlockDestruction", "WarlockSummoning",
	"WarriorArms", "WarriorFury", "WarriorProtection",
};

-- Options
ex.options = {
	{ var = "makeMovable", default = false, label = "Make Examiner Movable", tip = "To freely move Examiner around, enable this option, otherwise it will behave like a normal frame, such as the Quest Log or Spellbook" },
	{ var = "autoInspect", default = true, label = "Auto Inspect on Target Change", tip = "With this option turned on, Examiner will automatically inspect your new target when you change it." },
	{ var = "clearInspectOnHide", default = false, label = "Clear Inspect Data on Hide", tip = "When Examiner gets hidden, this option will clear inspection data, thus freeing up some memory." },
	-- { var = "percentRatings", default = false, label = "Show Ratings in Percentage *", tip = "* = Not working in WoD.\nWith this option enabled, ratings will be displayed in percent relative to the inspected person's level." },
	{ var = "combineAdditiveStats", default = true, label = "Combine Additive Stats", tip = "This option will combine certain stats which stacks with others.\n- Spell Power to specific schools\n- Intellect to Spell Power\n- AP to Ranged AP" },
	{ var = "tooltipSmartAnchor", default = false, label = "Smart Tooltip Anchor", tip = "Instead of showing item tooltips next to the item button, it will place it next to the Examiner window, in a fixed position" },
};

-- Binding Name
BINDING_HEADER_EXAMINER = modName;
BINDING_NAME_EXAMINER_OPEN = "Open "..modName;
BINDING_NAME_EXAMINER_TARGET = INSPECT.." "..TARGET;
BINDING_NAME_EXAMINER_MOUSEOVER = INSPECT.." Mouseover";

-- Allow Inspect from Any Range -- This was apparently causing TAINT, so it has now been disabled.
--UnitPopupButtons.INSPECT.dist = 0;
-- UIPanelWindow Entry  -- Az: Can this cause TAINT?
UIPanelWindows[modName] = { area = "left", pushable = 1, whileDead = 1 };
-- Allows the use of Esc to close the window -- Az: Can this cause TAINT? Many other addons uses this, so probably no?
UISpecialFrames[#UISpecialFrames + 1] = modName;

--------------------------------------------------------------------------------------------------------
--                                          Examiner Scripts                                          --
--------------------------------------------------------------------------------------------------------

-- OnShow
local function Examiner_OnShow(self)
	self:RegisterEvent("PLAYER_TARGET_CHANGED");
	self:RegisterEvent("UNIT_MODEL_CHANGED");
	self:RegisterEvent("UNIT_PORTRAIT_UPDATE");
	self:RegisterEvent("UNIT_INVENTORY_CHANGED");
	self:RegisterEvent("MODIFIER_STATE_CHANGED");
end

-- OnHide
local function Examiner_OnHide(self)
	self.model.isRotating = nil;
	self.model.isPanning = nil;
	self:UnregisterEvent("INSPECT_READY");
	self:UnregisterEvent("PLAYER_TARGET_CHANGED");
	self:UnregisterEvent("UNIT_MODEL_CHANGED");
	self:UnregisterEvent("UNIT_PORTRAIT_UPDATE");
	self:UnregisterEvent("UNIT_INVENTORY_CHANGED");
	self:UnregisterEvent("MODIFIER_STATE_CHANGED");
	if (cfg.clearInspectOnHide) then
		ex:ClearInspect();
	else
		self:SetScript("OnUpdate",nil);
	end
end

-- OnUpdate -- Only used for units outside inspect range
local function Examiner_OnUpdate(self,elapsed)
	if (self:ValidateUnit()) and (CheckInteractDistance(self.unit,3)) then
		self:DoInspect(self.unit);
	end
end

--------------------------------------------------------------------------------------------------------
--                                           Event Handling                                           --
--------------------------------------------------------------------------------------------------------

-- Variables Loaded
function ex:VARIABLES_LOADED(event)
	-- Config
	if (not Examiner_Config) then
		Examiner_Config = {};
	end
	cfg = Examiner_Config;
	if (not cfg.caching) then
		cfg.caching = {};
	end
	self.cfg = cfg;
	-- Load Defaults
	if (cfg.showBackground == nil) then
		cfg.showBackground = true;
	end
	for _, option in ipairs(self.options) do
		if (cfg[option.var] == nil) then
			cfg[option.var] = option.default;
		end
	end
	-- Cache
	if (not Examiner_Cache) then
		Examiner_Cache = {};
	end
	cache = Examiner_Cache;
	-- SET: Background Visibility | Scale | Frame Movability
	self:ShowBackground();
	self:SetScale(cfg.scale or 1);
	self:SetMovable(cfg.makeMovable);
	if (cfg.makeMovable) and (cfg.left and cfg.bottom) then
		self:ClearAllPoints();
		self:SetPoint("BOTTOMLEFT",cfg.left,cfg.bottom);
	end
	-- HOOK: InspectUnit
	if (HOOK_DEFAULT_INSPECT) then
		InspectUnit = function(...) ex:DoInspect(...); end
	end
	-- Init Modules
	for index, mod in ipairs(self.modules) do
		if (mod.OnInitialize) then
			mod:OnInitialize();
			mod.OnInitialize = nil;
		end
	end
	-- Is active page valid?
	if (cfg.activePage) and (cfg.activePage > #self.modules or not self.modules[cfg.activePage].page) then
		cfg.activePage = nil;
	end
	-- Initialise the Config for Modules
	for _, option in ipairs(self.options) do
		self:SendModuleEvent("OnConfigChanged",option.var,cfg[option.var]);
	end
	-- Remove this event
	self:UnregisterEvent(event);
	self[event] = nil;
end

-- Target Unit Changed
function ex:PLAYER_TARGET_CHANGED(event)
	if (cfg.autoInspect) and (UnitExists("target")) then
		self:DoInspect("target");
	elseif (self.unit == "target") then
		self.unit = nil;
		self:SetScript("OnUpdate",nil);
	end
end

-- Mouseover Unit Changed
function ex:UPDATE_MOUSEOVER_UNIT(event)
	self.unit = nil;
	self:SetScript("OnUpdate",nil);
	self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT");
end

-- Model or Portrait Change
function ex:UNIT_MODEL_CHANGED(event,unit)
	if (unit and self:ValidateUnit() and UnitIsUnit(unit,self.unit)) then
		self.model:SetUnit(self.unit);
		SetPortraitTexture(self.portrait,self.unit);
	end
end
ex.UNIT_PORTRAIT_UPDATE = ex.UNIT_MODEL_CHANGED;

-- Rescan Gear on Item Change
function ex:UNIT_INVENTORY_CHANGED(event,unit)
	if (self:ValidateUnit() and UnitIsUnit(unit,self.unit) and CheckInteractDistance(self.unit,3)) then
		self:ScanGear(unit);
		self:SendModuleEvent("OnInspectReady",unit,self.guid);
	end
end

-- Modifier State Changed
function ex:MODIFIER_STATE_CHANGED(event)
	if (self.tooltipSlot) then
		local onEnterFunc = self.tooltipSlot:GetScript("OnEnter");
		if (onEnterFunc) then
			onEnterFunc(self.tooltipSlot);
		end
	end
end

-- Inspect Ready -- This event will never fire when on a public transport mount for some odd reason
function ex:INSPECT_READY(event,guid)
	self:UnregisterEvent("INSPECT_READY");
	self:InspectReady(guid);
end

-- INSPECT_HONOR_UPDATE
function ex:INSPECT_HONOR_UPDATE(event)
	self:RequestHonorData();
	self:SendModuleEvent("OnHonorReady");
end

-- Achievement Inspection Ready
function ex:INSPECT_ACHIEVEMENT_READY(event,guid)
	if (AchievementFrameComparison) then
		AchievementFrameComparison:RegisterEvent("INSPECT_ACHIEVEMENT_READY");
	end
	self:UnregisterEvent("INSPECT_ACHIEVEMENT_READY");
	self:SendModuleEvent("OnAchievementReady",self.unit,guid);
end

--------------------------------------------------------------------------------------------------------
--                                         Model Frame Scripts                                        --
--------------------------------------------------------------------------------------------------------

-- Model Frame Workaround
local function Model_OnShow(self)
	if (self.cleared) then
		self:ClearModel();
	end
end

local function Model_OnUpdate(self,elapsed)
	if (self.isRotating) then
		local endx, endy = GetCursorPosition();
		self.rotation = (endx - self.startx) / 34 + self:GetFacing();
		self:SetFacing(self.rotation);
		self.startx, self.starty = GetCursorPosition();
	elseif (self.isPanning) then
		local endx, endy = GetCursorPosition();
		local z, x, y = self:GetPosition(z,x,y);
		x = (endx - self.startx) / 45 + x;
		y = (endy - self.starty) / 45 + y;
		self:SetPosition(z,x,y);
		self.startx, self.starty = GetCursorPosition();
	end
end

local function Model_OnMouseWheel(self,delta)
	local z, x, y = self:GetPosition();
	local scale = (IsControlKeyDown() and 2 or 0.7);
	z = (delta > 0 and z + scale or z - scale);
	self:SetPosition(z,x,y);
end

local function Model_OnMouseDown(self,button)
	self.startx, self.starty = GetCursorPosition();
	if (button == "LeftButton") then
		self.isRotating = true;
		if (IsControlKeyDown()) then
			ex:SetBackgroundTexture(true);
		end
	elseif (button == "RightButton") then
		self.isPanning = true;
		if (IsControlKeyDown()) then
			cfg.showBackground = (not cfg.showBackground);
			ex:ShowBackground();
		end
	end
end

local function Model_OnMouseUp(self,button)
	if (button == "LeftButton") then
		self.isRotating = nil;
	elseif (button == "RightButton") then
		self.isPanning = nil;
	end
end

--------------------------------------------------------------------------------------------------------
--                                            Init Examiner                                           --
--------------------------------------------------------------------------------------------------------

-- Examiner Main Frame
ex:SetSize(384,440);
ex:SetPoint("CENTER");
ex:EnableMouse(true);
ex:SetToplevel(true);
ex:Hide();
ex:SetHitRectInsets(12,35,10,2);
ex:SetScript("OnShow",Examiner_OnShow);
ex:SetScript("OnHide",Examiner_OnHide);
ex:SetScript("OnEvent",function(self,event,...) self[event](self,event,...); end);
ex:SetScript("OnMouseDown",function(self,button) if (self:IsMovable()) then self:StartMoving(); end end);
ex:SetScript("OnMouseUp",function(self,button) if (self:IsMovable()) then self:StopMovingOrSizing(); cfg.left = self:GetLeft(); cfg.bottom = self:GetBottom() end end);

-- Events
ex:RegisterEvent("VARIABLES_LOADED");

-- Close Button
ex.close = CreateFrame("Button",nil,ex,"UIPanelCloseButton"):SetPoint("TOPRIGHT",-30,-8);

-- Portrait
ex.portrait = ex:CreateTexture(nil,"BACKGROUND");
ex.portrait:SetSize(60,60);
ex.portrait:SetPoint("TOPLEFT",7,-6);

-- FontStrings
ex.title = ex:CreateFontString(nil,"ARTWORK","GameFontNormal");
ex.title:SetPoint("TOP",5,-17);
ex.details = ex:CreateFontString(nil,"ARTWORK","GameFontHighlightSmall");
ex.details:SetPoint("TOP",5,-44);
ex.guild = ex:CreateFontString(nil,"ARTWORK","GameFontHighlightSmall");
ex.guild:SetPoint("TOP",ex.details,"BOTTOM",0,-2);

-- Dialog Textures
ex.dlgTopLeft = ex:CreateTexture(nil,"ARTWORK");
ex.dlgTopLeft:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-TopLeft");
ex.dlgTopLeft:SetPoint("TOPLEFT");
ex.dlgTopLeft:SetSize(256,256);
ex.dlgTopRight = ex:CreateTexture(nil,"ARTWORK");
ex.dlgTopRight:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-TopRight");
ex.dlgTopRight:SetPoint("TOPRIGHT");
ex.dlgTopRight:SetSize(128,256);
ex.dlgBottomLeft = ex:CreateTexture(nil,"ARTWORK");
ex.dlgBottomLeft:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-BottomLeft");
ex.dlgBottomLeft:SetPoint("TOPLEFT",0,-256);
ex.dlgBottomLeft:SetSize(256,256);
ex.dlgBottomRight = ex:CreateTexture(nil,"ARTWORK");
ex.dlgBottomRight:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-BottomRight");
ex.dlgBottomRight:SetPoint("TOPRIGHT",0,-256);
ex.dlgBottomRight:SetSize(128,256);

-- Background Textures -- Resize height, so they go beneath module buttons
local bgScale = 1.064;
ex.bgTopLeft = ex:CreateTexture(nil,"OVERLAY");
ex.bgTopLeft:SetPoint("TOPLEFT",21,-76);
ex.bgTopLeft:SetHeight(256 * bgScale);
ex.bgTopRight = ex:CreateTexture(nil,"OVERLAY");
ex.bgTopRight:SetPoint("LEFT",ex.bgTopLeft,"RIGHT");
ex.bgTopRight:SetHeight(256 * bgScale);
ex.bgBottomLeft = ex:CreateTexture(nil,"OVERLAY");
ex.bgBottomLeft:SetPoint("TOP",ex.bgTopLeft,"BOTTOM");
ex.bgBottomLeft:SetHeight(128 * bgScale);
ex.bgBottomRight = ex:CreateTexture(nil,"OVERLAY");
ex.bgBottomRight:SetPoint("LEFT",ex.bgBottomLeft,"RIGHT");
ex.bgBottomRight:SetHeight(128 * bgScale);

-- Model
ex.model = CreateFrame("PlayerModel",nil,ex);
ex.model:SetSize(320,354);
ex.model:SetPoint("BOTTOM",-11,10);
ex.model:EnableMouse(true);
ex.model:EnableMouseWheel(true);
ex.model:SetScript("OnShow",Model_OnShow);
ex.model:SetScript("OnUpdate",Model_OnUpdate);
ex.model:SetScript("OnMouseDown",Model_OnMouseDown);
ex.model:SetScript("OnMouseUp",Model_OnMouseUp);
ex.model:SetScript("OnMouseWheel",Model_OnMouseWheel);

--------------------------------------------------------------------------------------------------------
--                                            Unit Details                                            --
--------------------------------------------------------------------------------------------------------

local uDetails = {};

-- Unit Detail String
function ex:SetUnitDetailString()
	-- Level
	local color = GetQuestDifficultyColor(info.level ~= -1 and info.level or 500);
	uDetails[#uDetails + 1] = format("%s |cff%.2x%.2x%.2x%s|r",LEVEL,color.r * 255,color.g * 255,color.b * 255,(info.level ~= -1 and info.level or "??"));
	-- Classification (non players only, so ok to use ex.unit)
	if (not info.raceFixed) then
		local classification = UnitClassification(ex.unit);
		if (CLASSIFICATION_NAMES[classification]) then
			uDetails[#uDetails + 1] = "("..CLASSIFICATION_NAMES[classification]..")";
		end
	end
	-- Race for Players / Family or Type for NPC's
	if (info.race) then
		uDetails[#uDetails + 1] = (info.race ~= "Not specified" and info.race or UNKNOWN);
	end
	-- Players Only: Class (+ Realm)
	if (info.raceFixed) then
		if (info.class) then
			local color = CLASS_COLORS[info.classFixed];
			uDetails[#uDetails + 1] = format("|cff%.2x%.2x%.2x%s|r",color.r * 255,color.g * 255,color.b * 255,info.class);
		end
		if (info.realm) then
			uDetails[#uDetails + 1] = "of";
			uDetails[#uDetails + 1] = info.realm;
		end
	end
	-- Set Result
	self.details:SetText(table.concat(uDetails," "));
	wipe(uDetails);
end

-- Unit Guild String (Faction for NPC's)
function ex:SetUnitGuildString()
	-- Init as empty
	self.guild:SetText("");
	-- Players
	if (info.raceFixed) then
		if (info.guild and info.guildRank and info.guildIndex) then
			self.guild:SetFormattedText("%s (%d) of <%s>",info.guildRank,info.guildIndex,info.guild);
		end
	-- NPC's only, so ok to use 'ex.unit' here
	else
		LibGearExamTip:ClearLines();
		LibGearExamTip:SetUnit(ex.unit);
		local line;
		for i = 2, LibGearExamTip:NumLines() - 1 do
			line = _G["LibGearExamTipTextLeft"..i]:GetText();
			if (line:find("^"..TOOLTIP_UNIT_LEVEL:gsub("%%s",".+"))) then
				line = _G["LibGearExamTipTextLeft"..(i + 1)]:GetText();
				if (line ~= PVP_ENABLED) then
					self.guild:SetText(line);
					return;
				end
			end
		end
	end
end

-- Return name used for entires
function ex:GetEntryName()
	return (info.realm and info.name.."-"..info.realm or info.name);
end

--------------------------------------------------------------------------------------------------------
--                                           Cache Functions                                          --
--------------------------------------------------------------------------------------------------------

-- Cache Player
function ex:CachePlayer(override)
	if (cfg.caching.Core or override) then
		local entry = ex:GetEntryName();
		cache[entry] = CopyTable(info);
		self:SendModuleEvent("OnCache",cache[entry]);
		return true;
	end
end

-- Load player from cache
function ex:LoadPlayerFromCache(entryName)
	local entry = cache[entryName];
	if (not entry) then
		return false;
	end
	-- Load Depending on Unit Token
	info.time = entry.time;
	info.zone = entry.zone;
	if (not self.unit or info.level == -1) then
		info.level = entry.level;
	end
	if (not self.unit or not UnitIsVisible(self.unit)) then
		info.pvpName = entry.pvpName;
		info.guild, info.guildRank, info.guildIndex = entry.guild, entry.guildRank, entry.guildIndex;
		--info.guildID, info.guildLevel, info.guildXP, info.guildMembers = entry.guildID, entry.guildLevel, entry.guildXP, entry.guildMembers; -- Old Pre-MoP
		--info.guildLevel, info.guildXP, info.guildMembers = entry.guildLevel, entry.guildXP, entry.guildMembers;	-- Old Pre-WoD
		info.guildPoints, info.guildMembers = entry.guildPoints, entry.guildMembers;	-- Az: move to guild module
		if (not self.unit) then
			info.name, info.realm = entry.name, entry.realm;
			info.level = entry.level;
			info.class, info.classFixed = entry.class, entry.classFixed;
			info.race, info.raceFixed = entry.race, entry.raceFixed;
			info.sex = entry.sex;
		end
	end
	-- Item Slots
	for slotName, slotId in next, LibGearExam.SlotIDs do
		local link = entry.Items[slotName];
		info.Items[slotName] = link;
		LibGearExam:ScanItemLink(link,unitStats);
	end
	-- Sets + Set Bonuses [NEW]
	for setName, setEntry in next, entry.Sets do
		info.Sets[setName] = { count = setEntry.count, max = setEntry.max };
		LibGearExam:ScanArmorSetBonuses(setName,setEntry.count,unitStats,info.Items);
	end
	-- Sets + Set Bonuses [OLD]
--	for setName, setEntry in next, entry.Sets do
--		info.Sets[setName] = { count = setEntry.count, max = setEntry.max };
--		local idx = 1;
--		while (setEntry["setBonus"..idx]) do
--			info.Sets[setName]["setBonus"..idx] = setEntry["setBonus"..idx];
--			LibGearExam:ScanLineForPatterns(setEntry["setBonus"..idx],unitStats);
--			idx = (idx + 1);
--		end
--	end
	-- Finalize
	self.isCacheEntry = true;
	self.itemsLoaded = true;
	-- Update UI
	self:SetBackgroundVertex(unpack(VERTEX_COLOR_CACHED));
	self:UpdateObjects();
	self:ShowModulePage();
	-- Modules: OnCacheLoaded
	self:SendModuleEvent("OnCacheLoaded",entry,self.unit);
	return true;
end

--------------------------------------------------------------------------------------------------------
--                                         Inspect Functions                                          --
--------------------------------------------------------------------------------------------------------

-- CanInspect function override as the normal function seems bugged.
function ex:CanInspect(unit)
	-- If CanInspect() says yes, then go ahead. Otherwise only inspect if not npc, is visible, and in range, not only that, but the unit has to be friendly, or a non flagged foe.
	return CanInspect(unit) or (ex.unitType ~= 1 and UnitIsVisible(unit) and CheckInteractDistance(unit,3) and (ex.unitType == 3 or not UnitIsPVP(unit) or UnitIsPVPSanctuary(unit)));

end

-- Show or hide Examiner, but ensures that its done using the movable option
function ex:Display(state)
	state = (state == nil and true or state);
	if (not state) then
		HideUIPanel(self);
	elseif (cfg.makeMovable) then
		self:Show();
	else
		ShowUIPanel(self);
	end
end

-- Normal Open
function ex:OpenSimple()
	if (not info.name) then
		self:DoInspect("player");
	else
		self:Display(not self:IsVisible());
	end
end

-- Normal Open
function ex:InspectMouseover()
	local unit = "mouseover";
	-- See if mouseover == unitframe unit
	local mouseFocus = GetMouseFocus();
	if (mouseFocus) then
		unit = (mouseFocus:GetAttribute("unit") or unit);
	end
	-- Show/Hide
	if (UnitExists(unit)) then
		self:DoInspect(unit);
	else
		self:Display(false);
	end
end

-- ClearInspect -- Clears all work variables. Always called before a new inspect request!
function ex:ClearInspect()
	INSPECTED_UNIT = nil;

	-- unit specific
	self.unit = nil;
	self.guid = nil;
	self.isSelf = nil;
	self.unitType = nil;
	self.canInspect = nil;

	-- data specific
	self.itemsLoaded = nil;
	self.isCacheEntry = nil;

	-- clear inspections
	ClearInspectPlayer();
	if (self.requestedAchievementData) then
		ClearAchievementComparisonUnit();
		self.requestedAchievementData = nil;
	end
	self.requestedHonorData = nil;

	-- core
	wipe(unitStats);
	for k, v in next, info do
		if (type(v) == "table") then
			wipe(v);
		else
			info[k] = nil;
		end
	end
	self:SetScript("OnUpdate",nil);

	-- Reset Vertex Color
	self:SetBackgroundVertex(unpack(VERTEX_COLOR_NORMAL));

	-- post module event
	self:SendModuleEvent("OnClearInspect");
end

-- Inspect Ready
function ex:InspectReady(guid)
	if (self:ValidateUnit()) then
		local unit = self.unit;
		-- Guild Vars -- Returns zeros for unguilded people
		local info = self.info;
		if (info.guild) then
			--info.guildID, info.guildLevel, info.guildXP, info.guildMembers = GetInspectGuildInfo(unit);	-- Az: move this into guild.lua? -- Old Pre-MoP
			--info.guildLevel, info.guildXP, info.guildMembers = GetInspectGuildInfo(unit);	-- MoP: No more guildID as first return -- Az: move this into guild.lua?
			--info.guildPoints, info.guildMembers = GetInspectGuildInfo(unit);		-- WoD: Guild leveling removed. Az: What are guildPoints?
		end
		-- Scan Gear & Post InspectReady
		self:ScanGear(unit);
		self:ShowModulePage();
		self:SendModuleEvent("OnInspectReady",unit,guid);
		-- Cache
		if (self.itemsLoaded) then
			self:CachePlayer();
		end
		-- Reset Vertex Color
		self:SetBackgroundVertex(unpack(VERTEX_COLOR_NORMAL));
	end
end

-- Converts a unit token such as "mouseover" or "target" into a more static unit token such as "party4" or "raid17"
local function UnitToStaticUnit(unit)
	local count = GetNumGroupMembers();
	if (count) and (count > 0) then
		local isRaid = IsInRaid();
		for i = 1, count do
			local static = (isRaid and "raid" or "party")..i;
			if (UnitIsUnit(unit,static)) then
				return static;
			end
		end
	end
end

-- Inspect Unit
function ex:DoInspect(unit,openFlag)
	-- Clear
	self:ClearInspect();

	-- Check unit, fall back on player
	if (not unit or not UnitExists(unit)) then
		unit = "player";
	-- Convert "mouseover" and "target" units to party/raid unit
	elseif (unit == "mouseover") or (unit == "target") then
		unit = UnitToStaticUnit(unit) or unit;
	end

	-- Mouseover Event
	if (unit == "mouseover") then
		self:RegisterEvent("UPDATE_MOUSEOVER_UNIT");
	else
		self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT");
	end

	-- Set Work Variables
	INSPECTED_UNIT = unit;
	self.unit = unit;
	self.guid = UnitGUID(unit);
	self.isSelf = UnitIsUnit(unit,"player");
	self.unitType = (not UnitIsPlayer(unit) and 1) or (UnitCanCooperate("player",unit) and 3) or 2;	-- Unit Type (1 = npc, 2 = opposing faction, 3 = same faction)
	self.canInspect = self:CanInspect(unit);

	-- Gather Unit Info
	info.isSelf = self.isSelf;	-- This is what allows the cache module to filter out alts
	info.name, info.realm = UnitName(unit);
	if (info.realm == "") then
		info.realm = nil;
	end
	info.pvpName = UnitPVPName(unit);
	info.level = (UnitLevel(unit) or 0);
	info.sex = (UnitSex(unit) or 1);
	info.class, info.classFixed, info.classID = UnitClass(unit);
	info.race, info.raceFixed = UnitRace(unit);
	if (not info.race) then
		info.race = UnitCreatureFamily(unit) or UnitCreatureType(unit);
	end
	info.guild, info.guildRank, info.guildIndex = GetGuildInfo(unit);
	info.time = time();
	info.zone = GetMinimapZoneText();
	local realZone = GetRealZoneText();
	if (realZone ~= info.zone) then
		info.zone = realZone..", "..info.zone;
	end

	-- Players we can Inspect
	if (self.canInspect) then
		-- Vertex Color = LOADING
		self:SetBackgroundVertex(unpack(VERTEX_COLOR_LOADING));
		-- Magic Inspect Supernova
		self:RegisterEvent("INSPECT_READY");
		NotifyInspect(unit);
		lastInspectRequest = GetTime();
		-- When we call ScanGear here, even before INSPECT_READY. We can sorta force the client into precaching the items.
		self:ScanGear(unit);
	end

	-- Update the UI Objects
	if (self.unitType == 1) and (cfg.activePage) then
		self.modules[cfg.activePage].page:Hide();
	else
		self:ShowModulePage();
	end
	self:UpdateObjects();

	-- Modules: OnInspect
	self:SendModuleEvent("OnInspect",unit,self.guid);

	-- Player Only Code
	if (self.unitType ~= 1) then
		-- We couldn't Inspect, try and see if we have them cached?
		if (not self.canInspect) and (not self:LoadPlayerFromCache(self:GetEntryName())) and (cfg.activePage) then
			self.modules[cfg.activePage].page:Hide();	-- Az: this is slightly bad to do, what if a module still have data to show? feats still work outside inspect range for example
		end
		-- Outside range, monitor range and inspect as soon as they are in range
		if (not CheckInteractDistance(unit,3)) then
			self:SetScript("OnUpdate",Examiner_OnUpdate);
		end
	end

	-- Show Examiner
	if (openFlag ~= false) and (not self:IsShown()) then
		self:Display();
	end
end

-- Scans the Gear
function ex:ScanGear(unit)
	wipe(unitStats);
	wipe(info.Sets);
	LibGearExam:ScanUnitItems(unit,unitStats,info.Sets);
	for slotName, slotId in next, LibGearExam.SlotIDs do
		local link = (GetInventoryItemLink(unit,slotId) or ""):match(LibGearExam.ITEMLINK_PATTERN);
		info.Items[slotName] = LibGearExam:FixItemStringLevel(link,info.level);
		if (link) then
			ex.itemsLoaded = true;
		end
	end
end

--------------------------------------------------------------------------------------------------------
--                                        Additional Inspection                                       --
--------------------------------------------------------------------------------------------------------

-- Requests Honor Data
function ex:RequestHonorData()
	if (self.requestedHonorData) then
		return;
	elseif (HasInspectHonorData()) then
		self:UnregisterEvent("INSPECT_HONOR_UPDATE");
		self:SendModuleEvent("OnHonorReady");
	else
		self.requestedHonorData = true;
		self:RegisterEvent("INSPECT_HONOR_UPDATE");
		RequestInspectHonorData();
	end
end

-- Requests Achievement Data
function ex:RequestAchievementData()
	if (self.requestedAchievementData) then
		return;
	end
	-- Makes the Achievement UI, if loaded, not update when we query the achievements
	if (achievementFunctions) and (type(achievementFunctions.selectedCategory) == "string") then
		achievementFunctions.selectedCategory = 92;
	end
	if (AchievementFrameComparison) then
		AchievementFrameComparison:UnregisterEvent("INSPECT_ACHIEVEMENT_READY");
	end
	-- Request
	self.requestedAchievementData = true;
	self:RegisterEvent("INSPECT_ACHIEVEMENT_READY");
	SetAchievementComparisonUnit(self.unit);
end

--------------------------------------------------------------------------------------------------------
--                                          Helper Functions                                          --
--------------------------------------------------------------------------------------------------------

-- Format Time (sec)
function ex:FormatTime(time,short)
	-- bugged?
	if (time < 0) then
		return "n/a";
	-- under a min
	elseif (time < 60) then
		return time..(short and "s" or " seconds");
	-- less than 1 hour
	elseif (time < 60*60) then
		return format(short and "%dm %.2ds" or "%d minutes and %.2d seconds",time / 60,time % 60);
	-- less than 1 day
	elseif (time < 60*60*24) then
		time = (time/60);
		return format(short and "%dh %.2dm" or "%d hours and %.2d minutes",time / 60,time % 60);
	-- above 1 day
	else
		time = (time/60/60);
		return format(short and "%dd %.2dh" or "%d days and %.2d hours",time / 24,time % 24);
	end
end

-- Cache current stats for compare or clear previous marked one
function ex:CacheStatsForCompare(unmark)
	wipe(ex.compareStats);
	if (unmark) then
		ex.isComparing = nil;
	else
		ex.isComparing = true;
		ex.compareStats.entry = ex:GetEntryName();
		for k, v in next, unitStats do
			ex.compareStats[k] = v;
		end
		for slotName, itemLink in next, info.Items do
			ex.compareStats[slotName] = itemLink;
		end
	end
	-- Post OnCompare Event
	self:SendModuleEvent("OnCompare",ex.isComparing,ex.compareStats);
end

-- Sets the background vertex color
function ex:SetBackgroundVertex(r,g,b,a)
	self.dlgTopLeft:SetVertexColor(r,g,b,a);
	self.dlgTopRight:SetVertexColor(r,g,b,a);
	self.dlgBottomLeft:SetVertexColor(r,g,b,a);
	self.dlgBottomRight:SetVertexColor(r,g,b,a);
end

-- Toggle the Background
function ex:ShowBackground(show)
	show = (show) or (show == nil and cfg.showBackground);
	ex.bgTopLeft:SetShown(show);
	ex.bgTopRight:SetShown(show);
	ex.bgBottomLeft:SetShown(show);
	ex.bgBottomRight:SetShown(show);
end

-- Updates the Main Objects
function ex:UpdateObjects()
	-- Textures & Model
--	ex.model:SetPosition(0,0,0);
--	ex.model:SetFacing(0);
	if (UnitIsVisible(ex.unit)) then
		SetPortraitTexture(ex.portrait,ex.unit);
		ex.model:SetUnit(ex.unit);
		ex.model.cleared = nil;
	else
		local genderRace = (info.sex == 3 and "Female" or "Male").."-"..(info.raceFixed or "");	-- Az: WoD: raceFixed has a risk of being nil
		ex.portrait:SetTexture("Interface\\CharacterFrame\\TemporaryPortrait-"..genderRace);
		ex.model:ClearModel();
		ex.model.cleared = true;
	end
	ex:SetBackgroundTexture();
	-- Title, Detail & Guild Text
	ex.title:SetText(info.pvpName or info.name);
	ex:SetUnitDetailString();
	ex:SetUnitGuildString();
end

-- Show Module Page
function ex:ShowModulePage(index)
	-- hide current page
	local lastMod = ex.modules[cfg.activePage];
	if (lastMod) then
		lastMod.page:Hide();
	end
	-- when we have a new index
	if (index) then
		-- save old page
		if (cfg.activePage) then
			cfg.prevPage = cfg.activePage;
		end
		-- toggle page
		if (index == cfg.activePage) then
			self:SendModuleEvent("OnPageChanged",lastMod,false);
			cfg.activePage = nil;
		else
			cfg.activePage = index;
		end
	end
	-- show active page
	if (cfg.activePage) then
		local shownMod = ex.modules[cfg.activePage];
		shownMod.page:Show();
		self:SendModuleEvent("OnPageChanged",shownMod,true);
	end
end

-- Background Texture -- param can be a texture, or true for loading racial texture, or nothing to load a random texture
function ex:SetBackgroundTexture(param)
	local texture;
	-- Find Texture
	if (type(param) == "string") then
		texture = param;
	elseif (param == true) or (not info.raceFixed) then
		texture = "Interface\\TalentFrame\\"..TALENT_BACKGROUNDS[random(#TALENT_BACKGROUNDS)].."-";
	else
		param = (info.raceFixed == "Gnome" and "Dwarf") or (info.raceFixed == "Troll" and "Orc") or (info.raceFixed);
		texture = "Interface\\DressUpFrame\\DressUpBackground-"..param;
	end
	-- Set Texture
	local normal = texture:find("DressUpFrame");
	ex.bgTopLeft:SetTexture(texture..(normal and "1" or "TopLeft"));
	ex.bgTopRight:SetTexture(texture..(normal and "2" or "TopRight"));
	ex.bgBottomLeft:SetTexture(texture..(normal and "3" or "BottomLeft"));
	ex.bgBottomRight:SetTexture(texture..(normal and "4" or "BottomRight"));
	-- Set Texture Width -- Scaled about 1.063 for the talent frame textures
	local scale = (normal and 1 or 1.063);
	ex.bgTopLeft:SetWidth(256 * scale);
	ex.bgTopRight:SetWidth(64 * scale);
	ex.bgBottomLeft:SetWidth(256 * scale);
	ex.bgBottomRight:SetWidth(64 * scale);
end

-- Check Last Unit -- Clears unit if invalid
function ex:ValidateUnit()
	if (ex.unit) and (UnitExists(ex.unit)) and (UnitGUID(ex.unit) == ex.guid) then
		return true;
	else
		ex.unit = nil;
		ex:SetScript("OnUpdate",nil);
	end
end

--------------------------------------------------------------------------------------------------------
--                                   Widget Script Helper Functions                                   --
--------------------------------------------------------------------------------------------------------

-- Hide GTT
function ex.HideGTT(self)
	gtt:Hide();
end

-- Item Button OnClick
function ex.ItemButton_OnClick(self,button)
	if (self.link) then
		if (button == "RightButton") then
			-- Get Examiner item link and player item link
			local _, ExaminerItemLink = GetItemInfo(self.link);
			local PlayerItemslotId = self.id
			local PlayerItemLink = GetInventoryItemLink("player", PlayerItemslotId)
			local itemStats1, itemStats2 = {}, {};
			LibGearExam:ScanItemLink(ExaminerItemLink,itemStats1);
			LibGearExam:ScanItemLink(PlayerItemLink,itemStats2);
			-- Compare both items
			if not (PlayerItemLink == nil) then
				AzMsg("--- Comparing "..ExaminerItemLink.." over "..PlayerItemLink.." |r---");
				for statToken, statName in next, LibGearExam.StatNames do
					if (itemStats1[statToken] or itemStats2[statToken]) then
						if (not cfg.percentRatings) then
						end
						local statText = LibGearExam:GetStatValue(statToken,itemStats1,itemStats2,UnitLevel("player"),cfg.combineAdditiveStats,cfg.percentRatings);
						AzMsg(format("%s = |1%s|r.",statName,statText));
					end
				end
			else
				AzMsg("---|1 Cannot compare "..ExaminerItemLink.."|1 since you don't have any item in that slot. |r---");
			end
		elseif (button == "LeftButton") then
			local editBox = ChatEdit_GetActiveWindow();
			if (IsModifiedClick("DRESSUP")) then
				DressUpItemLink(itemLink);
			elseif (IsModifiedClick("CHATLINK")) and (editBox) and (editBox:IsVisible()) then
				editBox:Insert(itemLink);
			end
		end
	end
end

-- Item Button OnLeave
function ex.ItemButton_OnLeave(self)
	ex.tooltipSlot = nil;
	ResetCursor();
	gtt:Hide();
end

-- Item Button OnEnter
function ex.ItemButton_OnEnter(self,motion)
	-- Inspect Cursor
	if (IsModifiedClick("DRESSUP")) then
		ShowInspectCursor();
	else
		ResetCursor();
	end

	-- Anchor -- Az: add new option here to anchor it to the default anchor?
	ex.tooltipSlot = self;
	gtt:SetOwner(self,"ANCHOR_NONE");
	if (cfg.tooltipSmartAnchor) then
		gtt:SetPoint("TOPLEFT",ex,"TOPRIGHT",-34,-12);
	else
		gtt:SetPoint("TOPLEFT",self,"TOPRIGHT");
	end

	-- Fill in tooltip
	-- itemstring breakdown
	if (self.link) and (IsShiftKeyDown()) and (IsAltKeyDown()) then
		local itemName, _, itemRarity = GetItemInfo(self.link);
		local is = LibItemString:New(self.link);
		gtt:AddLine(itemName,GetItemQualityColor(itemRarity));
		for k, v in ipairs(is) do
			--gtt:AddDoubleLine(format("|cffaaaaaa[%.2d]|r %s",k,is:GetFieldName(k)),v,1,1,1);
			gtt:AddDoubleLine(format("|cffffffff%.2d|r %s",k,is:GetFieldName(k)),v,nil,nil,nil,1,1,1);
		end
		gtt:Show();
	-- stat breakdown /w compare support
	elseif (self.link) and (IsAltKeyDown()) then
		local itemName, _, itemRarity = GetItemInfo(self.link);
		gtt:AddLine(itemName,GetItemQualityColor(itemRarity));
		LibGearExam:ScanItemLink(self.link,statTipStats1);
		if (ex.isComparing) and (self.slotName) then
			LibGearExam:ScanItemLink(ex.compareStats[self.slotName],statTipStats2);
		end
		for index, statToken in ipairs(LibGearExam.StatNamesSorted) do
			if (statTipStats1[statToken]) or (statTipStats2 and statTipStats2[statToken]) then
				local statText = LibGearExam:GetStatValue(statToken,statTipStats1,ex.isComparing and statTipStats2,info.level,cfg.combineAdditiveStats,cfg.percentRatings);
				--gtt:AddDoubleLine(LibGearExam:FormatStatName(statToken,cfg.percentRatings),statText,1,1,1);
				gtt:AddDoubleLine(LibGearExam:FormatStatName(statToken,cfg.percentRatings),statText,nil,nil,nil,1,1,1);
			end
		end
		wipe(statTipStats1);
		wipe(statTipStats2);
		gtt:Show();
	-- set item from unit
	elseif (self.id and ex:ValidateUnit() and CheckInteractDistance(ex.unit,3) and gtt:SetInventoryItem(ex.unit,self.id)) then
	-- set item from link
	elseif (self.link) then
		gtt:SetHyperlink(self.link);
	-- no valid item available, so add text that we can reload cache to get item
	elseif (self.realLink) then
		-- Az: should not be like this anymore, a single call to GetItemInfo() would make the client cache the item, so just make some kind of postcacheload thingie, or just redo the OnCacheLoaded event?
		gtt:SetText(_G[self.slotName:upper()]);
		gtt:AddLine("ItemID: "..self.realLink:match(LibGearExam.ITEMLINK_PATTERN_ID),0,0.44,0.86);
		gtt:AddLine("This item was not in the local item cache, click this button to reload the cached player, so the item will update.",1,1,1,1);
		gtt:Show();
	-- empty item slot
	elseif (self.slotName) then
		gtt:SetText(_G[self.slotName:upper()]);
	end
end

--------------------------------------------------------------------------------------------------------
--                                           Slash Handling                                           --
--------------------------------------------------------------------------------------------------------

-- Slash Commands -- Array indices are used for command help. Commands starting with underscore are meant for debugging
ex.slashCommands = {
	-- Inspect Unit
	" |2inspect <unit>|r = Inspects the given unit ('target' if no unit given)",
	inspect = function(cmd)
		ex:DoInspect(cmd == "" and "target" or cmd);
	end,
	-- Scan a Single Item
	" |2si <itemlink>|r = Scans one item and shows the total sum of its stats combined",
	si = function(cmd)
		if (cmd ~= "") then
			local itemStats = {};
			LibGearExam:ScanItemLink(cmd,itemStats);
			AzMsg("--- |2Scan Overview for "..cmd.."|r ---");
			for stat in next, itemStats do
				local statName = (cfg.percentRatings and LibGearExam.StatNames[stat] or LibGearExam.StatNames[stat].." "..RATING);
				local statText, altText = LibGearExam:GetStatValue(stat,itemStats,nil,UnitLevel("player"),cfg.combineAdditiveStats,cfg.percentRatings);
				AzMsg(format(altText and "%s = |1%s|r (|1%s|r)." or "%s = |1%s|r.",statName,statText,altText));
			end
		else
			AzMsg("No item link given.");
		end
	end,
	-- Compares two Items
	" |2compare <itemlink1> <itemlink2>|r = Compares two items by listing the stat differences",
	compare = function(cmd)
		if (cmd ~= "") then
			local item1, item2 = cmd:match("(|c.+|r)%s+(|c.+|r)");
			if (item1 and item2) then
				local itemStats1, itemStats2 = {}, {};
				LibGearExam:ScanItemLink(item1,itemStats1);
				LibGearExam:ScanItemLink(item2,itemStats2);
				AzMsg("--- |2Using "..item1.."|2 over "..item2.."|r ---");
				for statToken, statName in next, LibGearExam.StatNames do
					if (itemStats1[statToken] or itemStats2[statToken]) then
						if (not cfg.percentRatings) then
							statName = (statName.." "..RATING);
						end
						local statText = LibGearExam:GetStatValue(statToken,itemStats1,itemStats2,UnitLevel("player"),cfg.combineAdditiveStats,cfg.percentRatings);
						AzMsg(format("%s = |1%s|r.",statName,statText));
					end
				end
			else
				AzMsg("Could not parse item links.");
			end
		else
			AzMsg("No item links given.");
		end
	end,
	-- Rating Converter
	" |2rating <stat> <rating> <level>|r = Rating Converter",
	rating = function(cmd)
		local stat, rating, level = cmd:match("([^%s]+) (%d+%.?%d*) ?(%d*)");
		rating = tonumber(rating);
		if (stat and rating) then
			local value = LibGearExam:GetRatingInPercent(stat:upper(),rating,tonumber(level) or UnitLevel("player"));
			AzMsg("Converted Rating = |1"..(value or "Invalid Input"));
		else
			AzMsg("Invalid Input - Use: <stat> <rating> <level>");
		end
	end,
	-- Reset Window Position
	" |2reset|r = Resets the position to the center, in case it was moved off screen",
	reset = function(cmd)
		if (cfg.makeMovable) then
			ex:ClearAllPoints();
			ex:SetPoint("CENTER");
		else
			AzMsg("This command is only available when Examiner is movable");
		end
	end,
	-- Scale
	" |2scale <value>|r = Sets the scale of the Examiner window (Default is 1)",
	scale = function(cmd)
		cmd = tonumber(cmd);
		if (type(cmd) == "number") then
			cfg.scale = cmd;
			ex:SetScale(cmd);
		end
	end,
	-- Clear Cache
	" |2clearcache|r = Clears the entire Examiner cache",
	clearcache = function(cmd)
		wipe(cache);
	end,
	-- Scans all cached entries
	_scanall = function(cmd)
		local count = 0;
		for name, entry in next, cache do
			ex:ClearInspect();
			ex:LoadPlayerFromCache(name);
			count = (count + 1);
		end
		AzMsg("Scanned |1"..count.."|r cached entries.");
	end,
	-- Reports patterns with no uses. Use "_scanall" first to find uses
	_uses = function(cmd)
		for index, tbl in ipairs(LibGearExam.Patterns) do
			if (not tbl.uses) then
				AzMsg("|2"..tostring(tbl.s).."|r = |1"..tostring(tbl.p));
			end
		end
	end,
	-- Load all items, so they are cached locally
	_loaditems = function(cmd)
		local count = 0;
		for name, entry in next, cache do
			for slotName, link in next, entry.Items do
				GetItemInfo(link);
				count = (count + 1);
			end
		end
		AzMsg("Went through |1"..count.."|r items, including duplicates.");
	end,
};

local function HandleSlashCommand(cmd,commandTable)
	-- Extract Paramters
	local param1, param2 = cmd:match("^([^%s]+)%s*(.*)$");
	param1 = (param1 and param1:lower() or cmd:lower());
	-- Check Param Function
	if (commandTable[param1]) then
		commandTable[param1](param2)
	-- Invalid or No Command
	else
		UpdateAddOnMemoryUsage();
		AzMsg(format("----- |2%s|r |1%s|r ----- |1%.2f |2kb|r -----",modName,GetAddOnMetadata(modName,"Version"),GetAddOnMemoryUsage(modName)));
		AzMsg("The following |2parameters|r are valid for this addon:");
		for index, help in ipairs(commandTable) do
			AzMsg(help);
		end
	end
end

-- Slash Handler
_G["SLASH_"..modName.."1"] = "/examiner";
_G["SLASH_"..modName.."2"] = "/ex";
SlashCmdList[modName] = function(cmd) HandleSlashCommand(cmd,ex.slashCommands) end