--[[
Name: LibTalentQuery-1.0
Revision: $Rev: 80 $
Author: Rich Martel (richmartel@gmail.com)
Documentation: http://wowace.com/wiki/LibTalentQuery-1.0
SVN: svn://svn.wowace.com/wow/libtalentquery-1-0/mainline/trunk
Description: Library to help with querying unit talents
Dependancies: LibStub, CallbackHandler-1.0
License: LGPL v2.1

Example Usage:
	local TalentQuery = LibStub:GetLibrary("LibTalentQuery-1.0")
	TalentQuery.RegisterCallback(self, "TalentQuery_Ready")

	local raidTalents = {}
	...
	TalentQuery:Query(unit)
	...
	function MyAddon:TalentQuery_Ready(e, name, realm, unitid)
		local isnotplayer = not UnitIsUnit(unitid, "player")
		local spec = {}
		for tab = 1, GetNumTalentTabs(isnotplayer) do
			local treename, _, pointsspent = GetTalentTabInfo(tab, isnotplayer)
			tinsert(spec, pointsspent)
		end
		raidTalents[UnitGUID(unitid)] = spec
	end
]]

local MAJOR, MINOR = "LibTalentQuery-1.0", 90000 + tonumber(("$Rev: 80 $"):match("(%d+)"))

local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

local INSPECTDELAY = 1
local INSPECTTIMEOUT = 5
if not lib.events then
	lib.events = LibStub("CallbackHandler-1.0"):New(lib)
end

local validateTrees
local enteredWorld = IsLoggedIn()
local frame = lib.frame
if not frame then
	frame = CreateFrame("Frame", MAJOR .. "_Frame")
	lib.frame = frame
end
frame:UnregisterAllEvents()
frame:RegisterEvent("INSPECT_TALENT_READY")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_LEAVING_WORLD")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", function(this, event, ...)
	return lib[event](lib, ...)
end)

do
	local lastUpdateTime = 0
	frame:SetScript("OnUpdate", function(this, elapsed)
		lastUpdateTime = lastUpdateTime + elapsed
		if lastUpdateTime > INSPECTDELAY then
			lib:CheckInspectQueue()
			lastUpdateTime = 0
		end
	end)
	frame:Hide()
end

local inspectQueue = lib.inspectQueue or {}
lib.inspectQueue = inspectQueue
local garbageQueue = lib.garbageQueue or {}	-- Added a second queue to things. Inspects that initially fail are now
lib.garbageQueue = garbageQueue				-- thrown into second queue will will be processed once main queue is empty

if next(inspectQueue) then
	frame:Show()
end

local UnitIsPlayer = _G.UnitIsPlayer
local UnitName = _G.UnitName
local UnitExists = _G.UnitExists
local UnitGUID = _G.UnitGUID
local GetNumRaidMembers = _G.GetNumRaidMembers
local GetNumPartyMembers = _G.GetNumPartyMembers
local UnitIsVisible = _G.UnitIsVisible
local UnitIsConnected = _G.UnitIsConnected
local UnitCanAttack = _G.UnitCanAttack
local CanInspect = _G.CanInspect

local function UnitFullName(unit)
	local name, realm = UnitName(unit)
	local namerealm = realm and realm ~= "" and name .. "-" .. realm or name
	return namerealm
end

-- GuidToUnitID
local function GuidToUnitID(guid)
	local prefix, min, max = "raid", 1, GetNumRaidMembers()
	if max == 0 then
		prefix, min, max = "party", 0, GetNumPartyMembers()
	end

	-- Prioritise getting direct units first because other players targets
	-- can change between notify and event which can bugger things up
	for i = min, max do
		local unit = i == 0 and "player" or prefix .. i
		if (UnitGUID(unit) == guid) then
			return unit
		end
	end

	-- This properly detects target units
	if (UnitGUID("target") == guid) then
        return "target"
	elseif (UnitGUID("focus") == guid) then
		return "focus"
    end

	for i = min, max + 3 do
		local unit
		if i == 0 then
			unit = "player"
		elseif i == max + 1 then
			unit = "target"
		elseif i == max + 2 then
			unit = "focus"
		elseif i == max + 3 then
			unit = "mouseover"
		else
			unit = prefix .. i
		end
		if (UnitGUID(unit .. "target") == guid) then
			return unit .. "target"
		elseif (i <= max and UnitGUID(unit.."pettarget") == guid) then
			return unit .. "pettarget"
		end
	end
	return nil
end

-- Query
function lib:Query(unit)
	if (UnitLevel(unit) < 10 or UnitName(unit) == UNKNOWN) then
		return
	end

	self.lastQueuedInspectReceived = nil
	if UnitIsUnit(unit, "player") then
		self.events:Fire("TalentQuery_Ready", UnitName("player"), nil, "player")
	else
		if type(unit) ~= "string" then
			error(("Bad argument #2 to 'Query'. Expected %q, received %q (%s)"):format("string", type(unit), tostring(unit)), 2)
		elseif not UnitExists(unit) or not UnitIsPlayer(unit) then
			error(("Bad argument #2 to 'Query'. %q is not a valid player unit"):format(tostring(unit)), 2)
		elseif not UnitExists(unit) or not UnitIsPlayer(unit) then
			error(("Bad argument #2 to 'Query'. %q does not require a server query before reading talents"):format("player"), 2)
		else
			local name = UnitFullName(unit)
			if (not inspectQueue[name]) then
				inspectQueue[name] = UnitGUID(unit)
				garbageQueue[name] = nil
			end
			frame:Show()
		end
	end
end

-- CheckInspectQueue
-- Originally, it would wait until no pending NotifyInspect() were expected, and then do it's own.
-- It was also only bother looking at ready results if it had triggered the Notify for that occasion.
-- For the changes I've done, no assumption is made about which mod is performing NotifyInspect().
-- We note the name, unit, time of any inspects done whether from this queue or any other source,
-- we remove from our queue any we were expecting, and use a seperate event in case extra talent
-- info is any time wanted (opportunistic refreshes etc) - Zeksie, 20th May 2009
function lib:CheckInspectQueue()
	if (_G.InspectFrame and _G.InspectFrame:IsShown()) then
		return
	end

	if (not self.lastInspectTime or self.lastInspectTime < GetTime() - INSPECTTIMEOUT) then
		self.lastInspectPending = 0
	end

	if (self.lastInspectPending > 0 or not enteredWorld) then
		return
	end

	if (self.lastQueuedInspectReceived and self.lastQueuedInspectReceived < GetTime() - 60) then
		-- No queued results received for a minute, so purge the queue as invalid and move on with our lives
		self.lastQueuedInspectReceived = nil
		inspectQueue = {}
		lib.inspectQueue = inspectQueue
		garbageQueue = {}
		lib.garbageQueue = garbageQueue
		frame:Hide()
		return
	end

	for name,guid in pairs(inspectQueue) do
		local unit = GuidToUnitID(guid)
		if (not unit) then
			inspectQueue[name] = nil
		else
			if (UnitIsVisible(unit) and UnitIsConnected(unit) and not UnitCanAttack("player", unit) and not UnitCanAttack(unit, "player") and CanInspect(unit) and UnitClass(unit)) then
				NotifyInspect(unit)
				break
			else
				garbageQueue[name] = guid	-- Not available, throw into secondary queue and continue
				inspectQueue[name] = nil
			end
		end
	end

	if (not next(inspectQueue)) then
		if (next(garbageQueue)) then
			-- Retry initially failed inspects
			lib.inspectQueue = garbageQueue
			inspectQueue = lib.inspectQueue
			lib.garbageQueue = {}
			garbageQueue = lib.garbageQueue
		else
			frame:Hide()
		end
	end
end

-- NotifyInspect
if not lib.NotifyInspect then -- don't hook twice
	hooksecurefunc("NotifyInspect", function(...) return lib:NotifyInspect(...) end)
end
function lib:NotifyInspect(unit)
	if (not (UnitExists(unit) and UnitIsVisible(unit) and UnitIsConnected(unit) and CheckInteractDistance(unit, 4))) then
		return
	end
	self.lastInspectUnit = unit
	self.lastInspectGUID = UnitGUID(unit)
	self.lastInspectTime = GetTime()
	self.lastInspectName = UnitFullName(unit)
	self.lastInspectPending = self.lastInspectPending + 1
	local isnotplayer = not UnitIsUnit("player", unit)
	self.lastInspectTree = GetTalentTabInfo(1, isnotplayer)		-- Talent tree names are available immediately
end

-- Reset
function lib:Reset()
	self.lastInspectPending = 0
	self.lastInspectUnit = nil
	self.lastInspectTime = nil
	self.lastInspectName = nil
	self.lastInspectGUID = nil
	self.lastInspectTree = nil
end

-- INSPECT_TALENT_READY
function lib:INSPECT_TALENT_READY()
	self.lastInspectPending = self.lastInspectPending - 1

	-- Results are valid only when we have received as many events as we have posted notifies
	if (self.lastInspectName and self.lastInspectPending == 0) then
		-- Check unit ID is still pointing to same actual unit
		if (UnitGUID(self.lastInspectUnit) == self.lastInspectGUID) then
			local guid = inspectQueue[self.lastInspectName]
			inspectQueue[self.lastInspectName] = nil

			local name, realm = strsplit("-", self.lastInspectName)

			self.lastQueuedInspectReceived = GetTime()

			-- Notify of expected talent results
			local isnotplayer = not UnitIsUnit("player", self.lastInspectName)
			local group = GetActiveTalentGroup(isnotplayer)
			local tree1, _, spent1 = GetTalentTabInfo(1, isnotplayer, nil, group)
			if (tree1 ~= self.lastInspectTree) then
				-- Expected talent tree name to be the same as it was when we triggered the NotifyInspect()
				garbageQueue[self.lastInspectName] = self.lastInspectGUID
				self:Reset()
				self:CheckInspectQueue()
				return

			elseif (validateTrees) then
				-- Double checking here. Check the tree name matches what we expect for this class
				local _, class = UnitClass(self.lastInspectUnit)
				if (tree1 ~= validateTrees[class]) then
					garbageQueue[self.lastInspectName] = self.lastInspectGUID
					self:Reset()
					self:CheckInspectQueue()
					return
				end
			end

			local tree2, _, spent2 = GetTalentTabInfo(2, isnotplayer, nil, group)
			local tree3, _, spent3 = GetTalentTabInfo(3, isnotplayer, nil, group)
			if ((spent1 or 0) + (spent2 or 0) + (spent3 or 0) > 0) then
				if (guid) then
					-- It was in our queue
					self.events:Fire("TalentQuery_Ready", name, realm, self.lastInspectUnit)
				else
					-- Also notify of non-expected ones, as it's entirely useful to refresh them if they're there
					-- It is up to the receiving applicating to determine whether they want to receive the information
					self.events:Fire("TalentQuery_Ready_Outsider", name, realm, self.lastInspectUnit)
				end
			else
				-- Tree came back with zero points spent, probably an issue while logging in
				garbageQueue[self.lastInspectName] = guid
			end
		end

		self:Reset()
		self:CheckInspectQueue()
	end
end

function lib:PLAYER_ENTERING_WORLD()
	-- We can't inspect other's talents until now
	-- We just get 0/0/0 back even though we get an INSPECT_TALENT_READY event
	enteredWorld = true
end

function lib:PLAYER_LEAVING_WORLD()
	enteredWorld = nil
end

function lib:PLAYER_LOGIN()
	validateTrees = {
		DRUID = "Balance",
		PRIEST = "Discipline",
		ROGUE = "Assassination",
		HUNTER = "Beast Mastery",
		WARLOCK = "Affliction",
		WARRIOR = "Arms",
		DEATHKNIGHT = "Blood",
		PALADIN = "Holy",
		SHAMAN = "Elemental",
		MAGE = "Arcane",
	}

	if (GetLocale() ~= "enUS" and GetLocale() ~= "enGB") then
		-- LibBabble-TalentTree-3.0 only loaded if present and not enUS
		local LBT = LibStub("LibBabble-TalentTree-3.0", true)
		if (not LBT) then
			LoadAddOn("LibBabble-TalentTree-3.0")
			LBT = LibStub("LibBabble-TalentTree-3.0", true)
		end
		LBT = LBT and LBT:GetLookupTable()
		if (LBT) then
			for class,tree1 in pairs(validateTrees) do
				validateTrees[class] = LBT[tree1]
			end
		else
			validateTrees = nil
		end
	end
	
	self.PLAYER_LOGIN = nil
end

lib:Reset()
