--[[
Name: RosterLib-2.0
Revision: $Revision: 9440 $
X-ReleaseDate: "$Date: 2006-08-10 08:55:29 +0200 (Thu, 10 Aug 2006) $
Author: Maia (maia.proudmoore@gmail.com)
Website: http://wiki.wowace.com/index.php/RosterLib-2.0
Documentation: http://wiki.wowace.com/index.php/RosterLib-2.0_API_Documentation
SVN: http://svn.wowace.com/root/trunk/RosterLib-2.0/
Description: party/raid roster management
Dependencies: AceLibrary, AceOO-2.0, AceEvent-2.0
]]

local MAJOR_VERSION = "RosterLib-2.0"
local MINOR_VERSION = "$Revision: 9440 $"

if not AceLibrary then error(vmajor .. " requires AceLibrary.") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end
if not AceLibrary:HasInstance("AceEvent-2.0") then error(MAJOR_VERSION .. " requires AceEvent-2.0") end

local Compost = AceLibrary:HasInstance("Compost-2.0") and AceLibrary("Compost-2.0")
local rosterCount, addonCount, standby
local updatedUnits = Compost and Compost:Acquire() or {}
local oldRoster = Compost and Compost:Acquire() or {}
local RosterLib = {}

AceLibrary("AceEvent-2.0"):embed(RosterLib)

------------------------------------------------
-- activate, enable, disable
------------------------------------------------

local function print(text)
	DEFAULT_CHAT_FRAME:AddMessage(text)
end

local function activate(self, oldLib, oldDeactivate)
	RosterLib = self
	if oldLib then
		self.registry = oldLib.registry
	end
	if not self.registry then
		self.registry = {}
	end
	if oldDeactivate then
		oldDeactivate(oldLib)
	end
	addonCount = 0
end


function RosterLib:Enable()
	addonCount = addonCount + 1
	if addonCount > 1 then return end
	if AceLibrary("AceEvent-2.0"):IsFullyInitialized() or standby then
		RosterLib:AceEvent_FullyInitialized()
	else
		self:RegisterEvent("AceEvent_FullyInitialized")
	end
	standby = false
	self.roster = Compost and Compost:Acquire() or {}
	rosterCount = 0
end


function RosterLib:Disable()
	addonCount = addonCount - 1
	if addonCount > 0 then return end
	self:TriggerEvent("RosterLib_Disabled")
	self:UnregisterAllEvents()
	standby = true
	rosterAvailable = false
	for name in pairs(self.roster) do
		if Compost then Compost:Reclaim(self.roster[name]) end
		self.roster[name] = nil
	end
	rosterCount = 0
end

------------------------------------------------
-- Internal functions
------------------------------------------------

function RosterLib:AceEvent_FullyInitialized()
	self:TriggerEvent("RosterLib_Enabled")
	self:RegisterEvent("RAID_ROSTER_UPDATE","UpdateRoster")
	self:RegisterEvent("PARTY_MEMBERS_CHANGED","UpdateRoster")
	self:RegisterEvent("UNIT_PET","UpdateRoster")
	self:CheckRoster()
end

--[[

roster checks:

1.	AceEvent_FullyInitialized() is triggered when data *should* be available. It will
	register party/raid/pet events as we don't want then to fire before.
	Also, it will trigger CheckRoster().

2.	CheckRoster() will compare the roster table with the currently available data. If
	there's a difference, it will schedule UpdateRoster() with a delay of 0.5sec.

3.	UpdateRoster() will create/update the roster. To see if everything worked fine,
	it will call CheckRoster() when it's done.

4.	Now the saved data should be valid. If it is, it'll trigger the event
	RosterLib_RosterChanged your addon can listen to. It'll pass a table with the unit
	names that have changed as first argument. If not (server lag?) it'll send a
	RosterLib_PendingRefresh event in case the roster was valid before (to not spam
	this event when we're waiting for the server)

5.	WOW events that are related to roster updates directly trigger UpdateRoster().

]]

local function GetName(unit)
	if not UnitExists(unit) then return nil end
	local n = UnitName(unit)
	if n and n ~= UNKNOWNOBJECT and n ~= UKNOWNBEING then return n end
end

local playersent, petsent, unitcount, petcount, pmem, rmem

local function NextUnit()
	-- STEP 1: pet
	if not petsent then
		petsent = true
		if rmem == 0 then
			unit = "pet"
			name = GetName(unit)
			if name and not UnitIsCharmed(unit) then
				return unit, name
			end
		end
	end
	-- STEP 2: player
	if not playersent then
		playersent = true
		if rmem == 0 then
			unit = "player"
			name = GetName(unit)
			if name then
				return unit, name
			end
		end
	end
	-- STEP 3: raid units
	if rmem > 0 then
		-- STEP 3a: pet units
		for i = petcount, rmem do
			unit = string.format("raidpet%d", i)
			petcount = petcount + 1
			name = GetName(unit)
			if name and not UnitIsCharmed(unit) then
				return unit, name
			end
		end
		-- STEP 3b: player units
		for i = unitcount, rmem do
			unit = string.format("raid%d", i)
			unitcount = unitcount + 1
			name = GetName(unit)
			if name then
				return unit, name
			end
		end
	-- STEP 4: party units
	elseif pmem > 0 then
		-- STEP 3a: pet units
		for i = petcount, pmem do
			unit = string.format("partypet%d", i)
			petcount = petcount + 1
			name = GetName(unit)
			if name and not UnitIsCharmed(unit) then
				return unit, name
			end
		end
		-- STEP 3b: player units
		for i = unitcount, pmem do
			unit = string.format("party%d", i)
			unitcount = unitcount + 1
			name = GetName(unit)
			if name then
				return unit, name
			end
		end
	end
end

local function UnitIterator()
	playersent, petsent, unitcount, petcount, pmem, rmem = false, false, 1, 1, GetNumPartyMembers(), GetNumRaidMembers()
	return NextUnit
end


local function RosterValid()
	local mem = 0
	local n, u
	-- STEP 1: count how many units the roster should have
	-- note: if the player has joined a raid but doesnt have a raid id yet
	-- (can take a second or two), the count will be one less.
	local testRoster = Compost and Compost:Acquire() or {}
	for u,n in UnitIterator(true) do
		testRoster[n] = true
	end
	for i in pairs(testRoster) do
		mem = mem + 1
	end
	if Compost then Compost:Reclaim(testRoster) end
	if mem ~= rosterCount then
		if rosterAvailable then
			print(string.format("RosterLib: count mismatch: roster:%d members:%d (TELL MAIA)", rosterCount, mem))
		end
		return false
	end
	-- STEP 2: compare unit names
	for n in pairs(RosterLib.roster) do
		u = RosterLib.roster[n]
		if n ~= UnitName(u.unitid) then
			if rosterAvailable then
				print(string.format("name mismatch: name:%s, UnitName(%s):%q (TELL MAIA)", n or "nil", u.unitid or "nil", UnitName(u.unitid) or "nil"))
			end
			return false
		end
	end
	return true
end


function RosterLib:CheckRoster()
	local wasAvailable = rosterAvailable
	if RosterValid() then
		rosterAvailable = true
		local cnt = 0
		for name in pairs(updatedUnits) do cnt = cnt + 1 end
		if cnt > 0 then
			self:TriggerEvent("RosterLib_RosterChanged", updatedUnits)
			for name in pairs(updatedUnits) do
				local u = updatedUnits[name]
				self:TriggerEvent("RosterLib_UnitChanged", u.unitid, u.name, u.class, u.subgroup, u.rank, u.oldname, u.oldunitid, u.oldclass, u.oldsubgroup, u.oldrank)
				if Compost then Compost:Reclaim(updatedUnits[name]) end
				updatedUnits[name] = nil
			end
		end
	else
		rosterAvailable = false
		if wasAvailable then
			print("RosterLib_PendingRefresh")
			self:TriggerEvent("RosterLib_PendingRefresh")
		end
		self:ScheduleEvent(self.UpdateRoster, 0.5, self)  --what do we need to change here to convert it to a local schedule?
	end
end


function RosterLib:UpdateRoster()
	local unitid, name
--	print("RosterLib:UpdateRoster")
	-- STEP 1: copy the roster to be able to compare it later.
	for name in pairs(self.roster) do
		oldRoster[name]          = Compost and Compost:Acquire() or {}
		oldRoster[name].name     = self.roster[name].name
		oldRoster[name].unitid   = self.roster[name].unitid
		oldRoster[name].class    = self.roster[name].class
		oldRoster[name].rank     = self.roster[name].rank
		oldRoster[name].subgroup = self.roster[name].subgroup
	end
	-- STEP 2: clear the .unitids, as we're replacing them and removing old units later
	for name in pairs(self.roster) do self.roster[name].unitid = nil end
	-- STEP 3: add all units and see if something has changed.
	for unitid, name in UnitIterator(true) do
		-- object
		if not self.roster[name] then
			self.roster[name] = Compost and Compost:Acquire() or {}
		end
		-- name
		self.roster[name].name = name
		-- unitid
		self.roster[name].unitid = unitid
		-- class
		if string.find(unitid,"pet") then
			self.roster[name].class = "PET"
		else
			_,self.roster[name].class = UnitClass(unitid)
		end
		-- subgroup and rank
		if GetNumRaidMembers() > 0 then
			local _,_,num = string.find(unitid, "(%d+)")
			_,self.roster[name].rank,self.roster[name].subgroup = GetRaidRosterInfo(num)
		else
			self.roster[name].subgroup = 1
			self.roster[name].rank = 0
		end
		-- compare data
		if not oldRoster[name]
		or self.roster[name].name     ~= oldRoster[name].name
		or self.roster[name].unitid   ~= oldRoster[name].unitid
		or self.roster[name].class    ~= oldRoster[name].class
		or self.roster[name].subgroup ~= oldRoster[name].subgroup
		or self.roster[name].rank     ~= oldRoster[name].rank
		then
			updatedUnits[name]             = Compost and Compost:Acquire() or {}
			updatedUnits[name].oldname     = (oldRoster[name] and oldRoster[name].name) or nil
			updatedUnits[name].oldunitid   = (oldRoster[name] and oldRoster[name].unitid) or nil
			updatedUnits[name].oldclass    = (oldRoster[name] and oldRoster[name].class) or nil
			updatedUnits[name].oldsubgroup = (oldRoster[name] and oldRoster[name].subgroup) or nil
			updatedUnits[name].oldrank     = (oldRoster[name] and oldRoster[name].rank) or nil
			updatedUnits[name].name        = self.roster[name].name
			updatedUnits[name].unitid      = self.roster[name].unitid
			updatedUnits[name].class       = self.roster[name].class
			updatedUnits[name].subgroup    = self.roster[name].subgroup
			updatedUnits[name].rank        = self.roster[name].rank
		end
	end
	-- STEP 4: now delete the old units in roster that have left your group
	for name in pairs(self.roster) do
		if not self.roster[name].unitid then
			if Compost then Compost:Reclaim(self.roster[name]) end
			self.roster[name] = nil
			updatedUnits[name]             = Compost and Compost:Acquire() or {}
			updatedUnits[name].oldname     = oldRoster[name].name
			updatedUnits[name].oldunitid   = oldRoster[name].unitid
			updatedUnits[name].oldclass    = oldRoster[name].class
			updatedUnits[name].oldsubgroup = oldRoster[name].subgroup
			updatedUnits[name].oldrank     = oldRoster[name].rank
		end
	end
	-- STEP 5: save the number of units, we need that in RosterValid()
	rosterCount = 0
	for name in pairs(self.roster) do rosterCount = rosterCount + 1 end
	-- STEP 6: clear oldRoster
	if Compost then
		for name in pairs(oldRoster) do
			Compost:Reclaim(oldRoster[name])
			oldRoster[name] = nil
		end
	else
		oldRoster = {}
	end
	-- STEP 6: we're done. Hopefully.
	self:CheckRoster(true)
end


------------------------------------------------
-- API
------------------------------------------------

function RosterLib:GetUnitIDFromName(name)
	if rosterAvailable and self.roster[name] then
		return self.roster[name].unitid
	else
		return nil
	end
end


function RosterLib:GetUnitIDFromUnit(unit)
	local name = UnitName(unit)
	if rosterAvailable and name and self.roster[name] then
		return self.roster[name].unitid
	else
		return nil
	end
end


function RosterLib:GetUnitObjectFromName(name)
	if rosterAvailable and self.roster[name] then
		return self.roster[name]
	else
		return nil
	end
end


function RosterLib:GetUnitObjectFromUnit(unit)
	local name = UnitName(unit)
	if rosterAvailable and self.roster[name] then
		return self.roster[name]
	else
		return nil
	end
end


function RosterLib:IterateRoster(pets)
	if not rosterAvailable then return nil end
	local key
	return function()
		key = next(self.roster, key)
		if key and (pets or self.roster[key].class ~= "PET") then
			return self.roster[key]
		end
	end
end


--[[
TODO:
- use events AceEvent_EventRegistered, AceEvent_EventUnregistered plus :IsEventRegistered("event") to see if we can disable the library
- optimize GC in IterateRoster()
]]


AceLibrary:Register(RosterLib, MAJOR_VERSION, MINOR_VERSION, activate)
RosterLib = AceLibrary(MAJOR_VERSION)
