--[[
Name: RosterLib-2.0
Revision: $Revision: 18313 $
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: 18313 $"

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 updatedUnits = {}
local unknownUnits = {}
local RosterLib = {}
local roster

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

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

local function activate(self, oldLib, oldDeactivate)
	RosterLib = self
	if oldLib then
		self.roster = oldLib.roster
		oldLib:UnregisterAllEvents()
		oldLib:CancelAllScheduledEvents()
	end
	if not self.roster then self.roster = {} end
	if oldDeactivate then oldDeactivate(oldLib) end
	roster = self.roster
end


local function external(self, major, instance)
	if major == "AceEvent-2.0" then
		AceEvent = instance
		AceEvent:embed(self)
		self:UnregisterAllEvents()
		self:CancelAllScheduledEvents()
		if AceEvent:IsFullyInitialized() then
			self:AceEvent_FullyInitialized()
		else
			self:RegisterEvent("AceEvent_FullyInitialized", "AceEvent_FullyInitialized", true)
		end		
	elseif major == "Compost-2.0" then
		Compost = instance
	end
end


function RosterLib:Enable()
-- not used anymore, but as addons still might be calling this method, we're keeping it.
end


function RosterLib:Disable()
-- not used anymore, but as addons still might be calling this method, we're keeping it.
end

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

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


------------------------------------------------
-- Unit iterator
------------------------------------------------

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

local function NextUnit()
	-- STEP 1: Check the player's pet
	if not petsent then
		petsent = true
		local unit = "pet"
		if rmem == 0 and UnitExists(unit) then
			return unit
		end
	end

	-- STEP 2: Check the player unit
	if not playersent then
		playersent = true
		local unit = "player"
		if rmem == 0 and UnitExists(unit) then
			return unit
		end
	end

	-- STEP 3: Iterate over raid and party units
	local prefix, limit = (rmem > 0 and "raid" or "party"), (rmem > 0 and rmem or pmem)
	while petIndex <= limit do
		local unit = prefix .. "pet" .. petIndex
		petIndex = petIndex + 1
		if UnitExists(unit) then
			return unit
		end
	end
	while unitIndex <= limit do
		local unit = prefix .. unitIndex
		unitIndex = unitIndex + 1
		if UnitExists(unit) then
			return unit
		end
	end
end

local function UnitIterator()
	playersent, petsent = false, false
	unitIndex, petIndex = 1, 1
	pmem, rmem = GetNumPartyMembers(), GetNumRaidMembers()

	if rmem > 0 then
		prefix, limit = "raid", rmem
	else
		prefix, limit = "party", pmem
	end

	return NextUnit
end

------------------------------------------------
-- Roster code
------------------------------------------------


-- consolidate bulk roster updates
function RosterLib:ScanFullRoster()
	if not RosterLib:IsEventScheduled("RosterUpdateDelay") then
		RosterLib:ScheduleEvent("RosterUpdateDelay", function () RosterLib:DoScanFullRoster() end, 0.2);
	end
end

function RosterLib:DoScanFullRoster()
	-- save all units we currently have, this way we can check who to remove from roster later.
	local temp = Compost and Compost:Acquire() or {}
	for name in pairs(roster) do 
		temp[name] = true
	end
	-- update data
	for unitid in UnitIterator() do
		local name = self:CreateOrUpdateUnit(unitid)
		-- we successfully added a unit, so we don't need to remove it next step
		if name then temp[name] = nil end
	end
	-- clear units we had in roster that either left the raid or are unknown for some reason.
	for name in pairs(temp) do
		self:RemoveUnit(name)
	end
	if Compost then Compost:Reclaim(temp) end
	self:ProcessRoster()
end


function RosterLib:ScanPet(owner)
	local unitid = self:GetPetFromOwner(owner)
	if not unitid then
		return
	elseif not UnitExists(unitid) then
		unknownUnits[unitid] = nil
		-- find the pet in the roster we need to delete
		for _,u in pairs(roster) do
			if u.unitid == unitid then
				self:RemoveUnit(u.name)
			end
		end
	else
		self:CreateOrUpdateUnit(unitid)
	end
	self:ProcessRoster()
end


function RosterLib:GetPetFromOwner(id)
	-- convert party3 crap to raid IDs when in raid.
	local owner = self:GetUnitIDFromUnit(id)
	if not owner then
		return
	end
	-- get ID
	if string.find(owner,"raid") then
		return string.gsub(owner, "raid", "raidpet")
	elseif string.find(owner,"party") then
		return string.gsub(owner, "party", "partypet")
	elseif owner == "player" then
		return "pet"
	else
		return nil
	end
end


function RosterLib:ScanUnknownUnits()
	local name
	for unitid in pairs(unknownUnits) do 
		if UnitExists(unitid) then
			name = self:CreateOrUpdateUnit(unitid)
		else
			unknownUnits[unitid] = nil
		end
		-- some pets never have a name. too bad for them, farewell!
		if not name and string.find(unitid,"pet") then
			unknownUnits[unitid] = nil
		end
	end
	self:ProcessRoster()
end


function RosterLib:ProcessRoster()
	if next(updatedUnits, nil) 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
	if next(unknownUnits, nil) then
		self:CancelScheduledEvent("ScanUnknownUnits")
		self:ScheduleEvent("ScanUnknownUnits",self.ScanUnknownUnits, 1, self)
	end
end


function RosterLib:CreateOrUpdateUnit(unitid)
	local old = nil
	-- check for name
	local name = UnitName(unitid)
	if name and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING and not UnitIsCharmed(unitid) then
		-- clear stuff
		unknownUnits[unitid] = nil
		-- return if a pet attempts to replace a player name
		-- this doesnt fix the problem with 2 pets overwriting each other FIXME
		if string.find(unitid,"pet") then
			if roster[name] and roster[name].class ~= "pet" then
				return name
			end
		end
		-- save old data if existing
		if roster[name] then
			old          = Compost and Compost:Acquire() or {}
			old.name     = roster[name].name
			old.unitid   = roster[name].unitid
			old.class    = roster[name].class
			old.rank     = roster[name].rank
			old.subgroup = roster[name].subgroup
			old.online   = roster[name].online
		end
		-- object
		if not roster[name] then
			roster[name] = Compost and Compost:Acquire() or {}
		end
		-- name
		roster[name].name = name
		-- unitid
		roster[name].unitid = unitid
		-- class
		if string.find(unitid,"pet") then
			roster[name].class = "PET"
		else
			_,roster[name].class = UnitClass(unitid)
		end
		-- subgroup and rank
		local _,_,num = string.find(unitid, "(%d+)")
		if GetNumRaidMembers() > 0 and num then
			_,roster[name].rank,roster[name].subgroup = GetRaidRosterInfo(num)
		else
			roster[name].subgroup = 1
			roster[name].rank = 0
		end
		-- online/offline status
		roster[name].online = UnitIsConnected(unitid)

		-- compare data
		if not old
		or roster[name].name     ~= old.name
		or roster[name].unitid   ~= old.unitid
		or roster[name].class    ~= old.class
		or roster[name].subgroup ~= old.subgroup
		or roster[name].rank     ~= old.rank
		or roster[name].online   ~= old.online
		then
			updatedUnits[name]             = Compost and Compost:Acquire() or {}
			updatedUnits[name].oldname     = (old and old.name) or nil
			updatedUnits[name].oldunitid   = (old and old.unitid) or nil
			updatedUnits[name].oldclass    = (old and old.class) or nil
			updatedUnits[name].oldsubgroup = (old and old.subgroup) or nil
			updatedUnits[name].oldrank     = (old and old.rank) or nil
			updatedUnits[name].oldonline   = (old and old.online) or nil
			updatedUnits[name].name        = roster[name].name
			updatedUnits[name].unitid      = roster[name].unitid
			updatedUnits[name].class       = roster[name].class
			updatedUnits[name].subgroup    = roster[name].subgroup
			updatedUnits[name].rank        = roster[name].rank
			updatedUnits[name].online      = roster[name].online
		end
		-- compost our table
		if old and Compost then
			Compost:Reclaim(old)
		end
		return name
	else
		unknownUnits[unitid] = true
		return false
	end
end


function RosterLib:RemoveUnit(name)
	updatedUnits[name]             = Compost and Compost:Acquire() or {}
	updatedUnits[name].oldname     = roster[name].name
	updatedUnits[name].oldunitid   = roster[name].unitid
	updatedUnits[name].oldclass    = roster[name].class
	updatedUnits[name].oldsubgroup = roster[name].subgroup
	updatedUnits[name].oldrank     = roster[name].rank
	if Compost then Compost:Reclaim(roster[name]) end
	roster[name] = nil
end


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

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


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


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


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


function RosterLib:IterateRoster(pets)
	local key
	return function()
		repeat
			key = next(roster, key)
		until (roster[key] == nil or pets or roster[key].class ~= "PET")

		return roster[key]
	end
end


AceLibrary:Register(RosterLib, MAJOR_VERSION, MINOR_VERSION, activate, nil, external)
