--[[
Name: Roster-2.1
Revision: $Revision: 24288 $
X-ReleaseDate: $Date: 2007-01-09 15:20:18 -0500 (Tue, 09 Jan 2007) $
Author: Maia (maia.proudmoore@gmail.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/wiki/Roster-2.1
SVN: http://svn.wowace.com/wowace/trunk/RosterLib/
Description: Party/raid roster management
Dependencies: AceLibrary, AceOO-2.0, AceEvent-2.0
]]

local MAJOR_VERSION = "Roster-2.1"
local MINOR_VERSION = "$Revision: 24288 $"

if not AceLibrary then error(MAJOR_VERSION .. " 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 unknownUnits = {}
local Lib    = {}
local roster
local new, del
do
	local cache = setmetatable({}, {__mode='k'})
	function new()
		local t = next(cache)
		if t then
			cache[t] = nil
			return t
		else
			return {}
		end
	end
	
	function del(t)
		for k in pairs(t) do
			t[k] = nil
		end
		cache[t] = true
		return nil
	end
end

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

local function activate(self, oldLib, oldDeactivate)
	if oldLib then
		if oldLib.roster then self.roster = oldLib.roster end
		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
		instance:embed(self)
		if instance:IsFullyInitialized() then
			self:AceEvent_FullyInitialized()
		else
			self:RegisterEvent("AceEvent_FullyInitialized")
		end
	end
end

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

function Lib:AceEvent_FullyInitialized()
	self:TriggerEvent("RosterLib_Enabled")

	self:RegisterBucketEvent({"RAID_ROSTER_UPDATE", "PARTY_MEMBERS_CHANGED"}, 0.2, "ScanFullRoster")
	self:RegisterBucketEvent("UNIT_PET", 0.2, "ScanPet")

	self:ScanFullRoster()
end


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

local UnitIterator
do
	local rmem, pmem, step, count
	local function SelfIterator()
		while step do
			local unit
			if step == 1 then
				-- STEP 1: player
				unit = "player"
				step = 2
			elseif step == 2 then
				-- STEP 2: pet
				unit = "pet"
				step = nil
			end
			if unit and UnitExists(unit) then return unit end
		end
	end

	local function SelfAndPartyIterator()
		while step do
			local unit
			if step <= 2 then
				unit = SelfIterator()
				if not step then step = 3 end
			elseif step == 3 then
				-- STEP 3: party units
				unit = string.format("party%d", count)
				step = 4
			elseif step == 4 then
				-- STEP 4: party pets
				unit = string.format("partypet%d", count)
				count = count + 1
				step = count <= pmem and 3 or nil
			end
			if unit and UnitExists(unit) then return unit end
		end
	end

	local function RaidIterator()
		while step do
			local unit
			if step == 1 then
				-- STEP 1: raid units
				unit = string.format("raid%d", count)
				step = 2
			elseif step == 2 then
				-- STEP 2: raid pets
				unit = string.format("raidpet%d", count)
				count = count + 1
				step = count <= rmem and 1 or nil
			end
			if unit and UnitExists(unit) then return unit end
		end
	end

	function UnitIterator()
		rmem = GetNumRaidMembers()
		step = 1
		if rmem == 0 then
			pmem = GetNumPartyMembers()
			if pmem == 0 then
				return SelfIterator, false
			else
				count = 1
				return SelfAndPartyIterator, false
			end
		else
			count = 1
			return RaidIterator, true
		end
	end
end

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

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



function Lib:ScanPet(owner_list)
	local changed
	for owner in pairs(owner_list) do
		local unitid = self:GetPetFromOwner(owner)
		if not unitid then return end

		if 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)
					changed = true
				end
			end
		else
			_, changed = self:CreateOrUpdateUnit(unitid)
		end
		self:ProcessRoster()
	end
	if changed then
		self:TriggerEvent("RosterLib_RosterUpdated")
	end
end


function Lib: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 owner:find("raid") then
		return owner:gsub("raid", "raidpet")
	elseif owner:find("party") then
		return owner:gsub("party", "partypet")
	elseif owner == "player" then
		return "pet"
	else
		return nil
	end
end


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


function Lib:ProcessRoster()
	if next(unknownUnits, nil) then
		self:CancelScheduledEvent("ScanUnknownUnits")
		self:ScheduleEvent("ScanUnknownUnits", self.ScanUnknownUnits, 1, self)
	end
end


function Lib:CreateOrUpdateUnit(unitid, isInRaid)
	-- check for name
	local name = UnitName(unitid)
	if name and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING and not UnitIsCharmed(unitid) then
		local unit = roster[name]
		local isPet = unitid:find("pet")

		-- 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 isPet and unit and unit.class ~= "PET" then
			return name
		end
		-- save old data if existing
		local old_name, old_unitid, old_class, old_rank, old_subgroup, old_online
		if unit then
			old_name     = unit.name
			old_unitid   = unit.unitid
			old_class    = unit.class
			old_rank     = unit.rank
			old_subgroup = unit.subgroup
			old_online   = unit.online
		else
			-- object
			unit = new()
			roster[name] = unit
		end

		local new_name, new_unitid, new_class, new_rank, new_subgroup, new_online

		-- name
		new_name = name
		-- unitid
		new_unitid = unitid
		-- class
		if isPet then
			new_class = "PET"
		else
			_, new_class = UnitClass(unitid)
		end
		if isInRaid == nil and GetNumRaidMembers() > 0 then
			isInRaid = true
		end

		-- subgroup and rank
		new_subgroup = 1
		new_rank = 0
		if isInRaid then
			local _,_,num = unitid:find("(%d+)")
			if num then
				_, new_rank, new_subgroup = GetRaidRosterInfo(num)
			end
		else
			new_rank = UnitIsPartyLeader(new_unitid) and 2 or 0
		end
		-- online/offline status
		new_online = UnitIsConnected(unitid)

		-- compare data
		if not old_name
		or new_name     ~= old_name
		or new_unitid   ~= old_unitid
		or new_class    ~= old_class
		or new_subgroup ~= old_subgroup
		or new_rank     ~= old_rank
		or new_online   ~= old_online
		then
			unit.name = new_name
			unit.unitid = new_unitid
			unit.class = new_class
			unit.subgroup = new_subgroup
			unit.rank = new_rank
			unit.online = new_online
			self:TriggerEvent("RosterLib_UnitChanged", 
				new_unitid, new_name, new_class, new_subgroup, new_rank,
				old_name, old_unitid, old_class, old_subgroup, old_rank)
			return name, true
		end
		return name
	else
		unknownUnits[unitid] = true
		return false
	end
end


function Lib:RemoveUnit(name)
	local r = roster[name]
	roster[name] = nil
	self:TriggerEvent("RosterLib_UnitChanged",
		nil, nil, nil, nil, nil,
		r.name, r.unitid, r.class, r.subgroup, r.rank)
	r = del(r)
end


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

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


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


function Lib:GetUnitObjectFromName(name)
	return roster[name]
end


function Lib:GetUnitObjectFromUnit(unit)
	local name = UnitName(unit)
	return roster[name]
end


local function iter(t)
	local key = t.key
	local pets = t.pets
	repeat
		key = next(roster, key)
		if not key then
			t = del(t)
			return nil
		end
	until (pets or roster[key].class ~= "PET")
	t.key = key
	return roster[key]
end

function Lib:IterateRoster(pets)
	local t = new()
	t.pets = pets
	return iter, t
end

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

