------------------------------------------------------------------------------------
--
-- Census
-- A WoW UI customization by Ian Pieragostini
--
--
------------------------------------------------------------------------------------
----------------------------------------------------------------------------------
--
-- Constants
-- 
---------------------------------------------------------------------------------
local CENSUS_VERSION = "1.0";					-- version
local CENSUS_MAXBARHEIGHT = 128;				-- Length of blue bars
local CENSUS_NUMGUILDBUTTONS = 10;				-- How many guild buttons are on the UI?
local MAX_CHARACTER_LEVEL = 60;					-- Maximum level a PC can attain
local MAX_WHO_RESULTS = 49;						-- Maximum number of who results the server will return
CENSUS_GUILDBUTTONSIZEY = 16;
local CENSUS_UPDATEDELAY = 5;					-- Delay time between /who messages
	
----------------------------------------------------------------------------------
--
-- Print a string to the chat frame
--	msg - message to print
-- 
---------------------------------------------------------------------------------
local function Census_Msg(msg)
	ChatFrame1:AddMessage("Census: "..msg, 1.0, 1.0, 0.5);
end

----------------------------------------------------------------------------------
--
-- Global scope variables
-- 
---------------------------------------------------------------------------------
Census_Database = {};							-- Database of all census results


----------------------------------------------------------------------------------
--
-- File scope variables
-- 
---------------------------------------------------------------------------------
local g_CensusInitialized;						-- Is Census initialized?
local g_JobQueue = {};							-- The queue of pending jobs
local g_CurrentJob = {};						-- Current job being executed
local g_IsCensusInProgress = false;				-- Is a census in progress?

local g_NumNewCharacters = 0;					-- How many new characters found this census
local g_NumUpdatedCharacters = 0;				-- How many characters were updated during this census

local g_MobXPByLevel = {};						-- XP earned for killing
local g_CharacterXPByLevel = {};				-- XP required to advance through the given level
local g_TotalCharacterXPPerLevel = {};			-- Total XP required to attain the given level

local g_Guilds = {};							-- All known guilds

local g_TotalCharacterXP = 0;					-- Total character XP for currently selected search
local g_TotalCount = 0;							-- Total number of characters which meet search criteria
local g_RaceCount = {};							-- Totals for each race given search criteria
local g_ClassCount = {};						-- Totals for each class given search criteria
local g_LevelCount = {};						-- Totals for each level given search criteria

local g_GuildSelected = 0;						-- Search criteria: Currently selected guild, 0 indicates none
local g_RaceSelected = 0;						-- Search criteria: Currently selected race, 0 indicates none
local g_ClassSelected = 0;						-- Search criteria: Currently selected class, 0 indicates none

local g_LastOnUpdateTime = 0;					-- Last time OnUpdate was called
local g_WaitingForWhoUpdate = false;			-- Are we waiting for a who update event?

-----------------------------------------------------------------------------------
--
-- Insert a job at the end of the job queue
--
-----------------------------------------------------------------------------------
local function InsertJobIntoQueue(job)
	local whoText = Census_CreateWhoText(job);
	table.insert(g_JobQueue, job);
end

-----------------------------------------------------------------------------------
--
-- Initialize the tables of constants for XP calculations
--
-----------------------------------------------------------------------------------
local function InitConstantTables()
	--
	-- XP earned for killing
	--
	for i = 1, MAX_CHARACTER_LEVEL, 1 do
		g_MobXPByLevel[i] = (i * 5) + 45;
	end
	
	--
	-- XP required to advance through the given level
	--
	for i = 1, MAX_CHARACTER_LEVEL, 1 do
		g_CharacterXPByLevel[i] = ((8 * i * g_MobXPByLevel[i]) / 100) * 100;
	end

	--
	-- Total XP required to attain the given level
	--
	local totalCharacterXP = 0;
	for i = 1, MAX_CHARACTER_LEVEL, 1 do
		g_TotalCharacterXPPerLevel[i] = totalCharacterXP;
		totalCharacterXP = totalCharacterXP + g_CharacterXPByLevel[i];
	end
end

-----------------------------------------------------------------------------------
--
-- Return a table of races for the input faction
--
-----------------------------------------------------------------------------------
local function GetFactionRaces(faction)
	local ret = {};
	if (faction == "Horde") then
		ret = {"Orc", "Tauren", "Troll", "Undead"};
	elseif (faction == "Alliance") then
		ret = {"Dwarf", "Gnome", "Human", "Night Elf"};
	end
	return ret;
end

-----------------------------------------------------------------------------------
--
-- Return a table of classes for the input faction
--
-----------------------------------------------------------------------------------
local function GetFactionClasses(faction)
	local ret = {};
	if (faction == "Horde") then
		ret = {"Druid", "Hunter", "Mage", "Priest", "Rogue", "Warlock", "Warrior", "Shaman"};
	elseif (faction == "Alliance") then
		ret = {"Druid", "Hunter", "Mage", "Priest", "Rogue", "Warlock", "Warrior", "Paladin"};
	end
	return ret;
end

-----------------------------------------------------------------------------------
--
-- Return a table of classes for the input race
--
-----------------------------------------------------------------------------------
local function GetRaceClasses(race)
	local ret = {};
	if (race == "Orc") then
		ret = {"Warrior", "Hunter", "Rogue", "Shaman", "Warlock"};
	elseif (race == "Tauren") then
		ret = {"Warrior", "Hunter", "Shaman", "Druid"};
	elseif (race == "Troll") then
		ret = {"Warrior", "Hunter", "Rogue", "Priest", "Shaman", "Mage"};
	elseif (race == "Undead") then
		ret = {"Warrior", "Rogue", "Priest", "Mage", "Warlock"};
	elseif (race == "Dwarf") then
		ret = {"Warrior", "Paladin", "Hunter", "Rogue", "Priest"};
	elseif (race == "Gnome") then
		ret = {"Warrior", "Rogue", "Mage", "Warlock"};
	elseif (race == "Human") then
		ret = {"Warrior", "Paladin", "Rogue", "Priest", "Mage", "Warlock"};
	elseif (race == "Night Elf") then
		ret = {"Warrior", "Hunter", "Rogue", "Priest", "Druid"};
	end
	return ret;
end
	


---------------------------------------------------------------------------------
--
-- Register with Cosmos UI
-- 
---------------------------------------------------------------------------------
local function Census_RegisterCosmos()
	--
	-- If Cosmos is installed, add a button to the Cosmos page to activate Census
	--
	if ( Cosmos_RegisterButton ) then 
		Cosmos_RegisterButton(CENSUS_BUTTON_TEXT, CENSUS_BUTTON_SUBTEXT, CENSUS_BUTTON_TIP, "Interface\\AddOns\\Census\\Census_Icon", Census_Toggle);
	end
end


----------------------------------------------------------------------------------
--
-- Called when the main window is shown
-- 
---------------------------------------------------------------------------------
function Census_OnShow()
	-- Initialize if this is the first OnShow event
	if (g_CensusInitialized == false) then
		g_CensusInitialized = true;
	end
	Census_UpdateView();
end

----------------------------------------------------------------------------------
--
-- Toggle hidden status
-- 
---------------------------------------------------------------------------------
function Census_Toggle() 
	if ( Census:IsVisible() ) then 
		Census:Hide();
	else
		Census:Show();
	end
end

-----------------------------------------------------------------------------------
--
-- Called once on load
--
-----------------------------------------------------------------------------------
function Census_OnLoad()
	Census_Msg("OnLoad");

	--
	-- Update the version number
	--
	CensusText:SetText("Census v"..CENSUS_VERSION);

	--
	-- Init constant tables
	--
	InitConstantTables();

	--
	-- Register the database for saving
	--
	RegisterForSave("Census_Database");
	
	--
	-- Register with Cosmos, if it is installed
	--
	Census_RegisterCosmos();
	
	--
	-- Register for events
	--
	this:RegisterEvent("WHO_LIST_UPDATE");

	--
	-- Register a slash command
	--
	SLASH_CENSUSCMD1 = "/census";
	SlashCmdList["CENSUSCMD"] = Census_Toggle;
end

-----------------------------------------------------------------------------------
--
-- Purge the database for this realm and faction
--
-----------------------------------------------------------------------------------
function Census_OnPurge()
	local realmName = GetCVar("realmName");
	local factionGroup = UnitFactionGroup("player");	
	local realmDatabase = Census_Database[realmName];
	if (realmDatabase ~= nil) then
		realmDatabase[factionGroup] = nil
	end
	Census_UpdateView();
	Census_Msg("Purged character database for realm "..realmName.." and faction "..factionGroup..".");	
end

-----------------------------------------------------------------------------------
--
-- Take a census
--
-----------------------------------------------------------------------------------
function Census_OnTake()
	if (g_IsCensusInProgress) then
		-- Do not initiate a new census while one is in progress
		Census_Msg("A census is in progress, try again later");
	else
		--
		-- Initialize the job queue and counters
		--
		Census_Msg("Taking census of characters online...");
		g_NumNewCharacters = 0;
		g_NumUpdatedCharacters = 0;
		g_JobQueue = {};
		--
		-- First job covers all characters by searching all levels
		--
		local job = {m_MinLevel = 1, m_MaxLevel = MAX_CHARACTER_LEVEL};
		g_IsCensusInProgress = true;
		g_WaitingForWhoUpdate = false;
		InsertJobIntoQueue(job);
	end
end

-----------------------------------------------------------------------------------
--
-- Create a who command text for the input job
--
-----------------------------------------------------------------------------------
function Census_CreateWhoText(job)
	local whoText = "";
	local race = job.m_Race;
	if (race ~= nil) then
		whoText = whoText.." r-\""..race.."\"";
	end
	local class = job.m_Class;
	if (class ~= nil) then
		whoText = whoText.." c-\""..class.."\"";
	end
	local minLevel = job.m_MinLevel;
	if (minLevel == nil) then
		minLevel = 1;
	end
	local maxLevel = job.m_MaxLevel;
	if (maxLevel == nil) then
		maxLevel = 60;
	end
	whoText = whoText.." "..minLevel.."-"..maxLevel;
	return whoText;
end

-----------------------------------------------------------------------------------
--
-- Called on events
--
-----------------------------------------------------------------------------------
function Census_OnEvent(event,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9)
	
	--
	-- If we have not been initialized, do nothing
	--
	if (g_CensusInitialized == false) then
		return
	end
	
	--
	-- WHO_LIST_UPDATE
	--
	if (event == "WHO_LIST_UPDATE") then
		--
		-- Only process who results if a census is in progress
		--
		if (g_IsCensusInProgress) then
			local numWhoResults = GetNumWhoResults();
			if (numWhoResults < MAX_WHO_RESULTS) then
				--
				-- We have not overflowed who list, so process these returns
				--
				Census_ProcessWhoResults();
			else
				--
				-- Who list is overflowed, split the query to make the return smaller
				--
				local minLevel = g_CurrentJob.m_MinLevel;
				local maxLevel = g_CurrentJob.m_MaxLevel;
				local race = g_CurrentJob.m_Race;
				local class = g_CurrentJob.m_Class;
				if (minLevel ~= maxLevel) then
					--
					-- The level range is greater than a single level, so split it in half and submit the two jobs
					--
					local pivot = floor((minLevel + maxLevel) / 2);
					local jobLower = {m_MinLevel = minLevel, m_MaxLevel = pivot};
					InsertJobIntoQueue(jobLower);
					local jobUpper = {m_MinLevel = pivot + 1, m_MaxLevel = maxLevel};
					InsertJobIntoQueue(jobUpper);
				else
					--
					-- We cannot split the level range any more
					--
					local factionGroup = UnitFactionGroup("player");
					local level = minLevel;
					if (race == nil) then
						--
						-- This job does not specify race, so split it that way, making four new jobs
						--
						local thisFactionRaces = GetFactionRaces(factionGroup);
						local numRaces = table.getn(thisFactionRaces);
						for i = 1, numRaces, 1 do
							local job = {m_MinLevel = level, m_MaxLevel = level, m_Race = thisFactionRaces[i]};
							InsertJobIntoQueue(job);
						end					
					else
						if (class == nil) then
							--
							-- This job does not specify class, so split it that way, making more jobs
							--
							local thisRaceClasses = GetRaceClasses(race);
							local numClasses = table.getn(thisRaceClasses);
							for i = 1, numClasses, 1 do
								local job = {m_MinLevel = level, m_MaxLevel = level, m_Race = race, m_Class = thisRaceClasses[i]};
								InsertJobIntoQueue(job);
							end	
						else
							--
							-- There are too many characters with a single level, class and race
							--
							local whoText = Census_CreateWhoText(g_CurrentJob);
							Census_Msg("ERROR: Too many characters matching: "..whoText);
						end
					end
				end
			end
		end
		--
		-- We got the who update
		--
		g_WaitingForWhoUpdate = false;
	end
end

-----------------------------------------------------------------------------------
--
-- Call on the update event
--
-----------------------------------------------------------------------------------
function Census_OnUpdate()
	if (g_IsCensusInProgress == true) then
		local now = GetTime();
		local delta = now - g_LastOnUpdateTime;
		if (delta > CENSUS_UPDATEDELAY) then
			g_LastOnUpdateTime = now;
			if (g_WaitingForWhoUpdate == true) then
				--
				-- Resend /who command
				--
				local whoText = Census_CreateWhoText(g_CurrentJob);
				Census_Msg("Waiting to send who request...");
				SendWho(whoText);
			else
				--
				-- Determine if there is any more work to do
				--
				local numJobs = table.getn(g_JobQueue);
				if (numJobs > 0) then
					--
					-- Remove the top job from the queue and send it
					--
					local job = g_JobQueue[numJobs];
					table.remove(g_JobQueue);
					local whoText = Census_CreateWhoText(job);
					g_CurrentJob = job;
					g_WaitingForWhoUpdate = true;
					Census_Msg("Sending /who "..whoText);
					SendWho(whoText);
				else
					--
					-- We are all done, hide the friends frame and report our results
					--
					HideUIPanel(FriendsFrame);
					Census_Msg("Finished Taking data. Found "..g_NumNewCharacters.." new characters and updated "..g_NumUpdatedCharacters..".");
					g_IsCensusInProgress = false;
					Census_UpdateView();
				end
			end
		end
	end
end

-----------------------------------------------------------------------------------
--
-- Add the contents of the who results to the database
--
-----------------------------------------------------------------------------------
function Census_ProcessWhoResults()
	--
	-- Get the portion of the database for this server
	--
	local realmName = GetCVar("realmName");
	local realmDatabase = Census_Database[realmName];
	if (realmDatabase == nil) then
		Census_Database[realmName] = {};
		realmDatabase = Census_Database[realmName];
	end

	--
	-- Get the portion of the database for this faction
	--
	local factionGroup = UnitFactionGroup("player");
	local factionDatabase = realmDatabase[factionGroup];
	if (factionDatabase == nil) then
		realmDatabase[factionGroup] = {};
		factionDatabase = realmDatabase[factionGroup];
	end
	
	--
	-- Walk through all the who results
	--
	local numWhoResults = GetNumWhoResults();
	Census_Msg("Processing "..numWhoResults.." characters.");
	for i = 1, numWhoResults, 1 do
		--
		-- Get who result entry
		--
		local name, guild, level, race, class, zone, group = GetWhoInfo(i);
		
		--
		-- Get racial database
		--
		local raceDatabase = factionDatabase[race];
		if (raceDatabase == nil) then
			factionDatabase[race] = {};
			raceDatabase = factionDatabase[race];
		end

		--
		-- Get class database
		--
		local classDatabase = raceDatabase[class];
		if (classDatabase == nil) then
			raceDatabase[class] = {};
			classDatabase = raceDatabase[class];
		end

		--
		-- Get this player's entry
		--
		local entry = classDatabase[name];
		if (entry == nil) then
			classDatabase[name] = {};
			entry = classDatabase[name];
			g_NumNewCharacters = g_NumNewCharacters + 1;
		else
			g_NumUpdatedCharacters = g_NumUpdatedCharacters + 1;
		end
		
		--
		-- Update the information
		--
		entry[1] = level;
		entry[2] = guild;
		
	end
end

----------------------------------------------------------------------------------
--
-- Find a guild in the g_Guilds array by name
-- 
---------------------------------------------------------------------------------
local function FindGuildByName(name)
	local i;
	local size = table.getn(g_Guilds);
	for i = 1, size, 1 do
		local entry = g_Guilds[i];
		if (entry.m_Name == name) then
			return i;
		end
	end
	return nil;
end

----------------------------------------------------------------------------------
--
-- Add up the total character XP and count
-- 
---------------------------------------------------------------------------------
local g_AccumulateGuildTotals = true;
local function TotalsAccumulator(name, level, guild)
	local totalCharacterXP = g_TotalCharacterXPPerLevel[level];
	g_TotalCharacterXP = g_TotalCharacterXP + totalCharacterXP;
	g_TotalCount = g_TotalCount + 1;
	if (g_AccumulateGuildTotals and (guild ~= nil)) then
		local index = FindGuildByName(guild);
		if (index == nil) then
			local size = table.getn(g_Guilds);
			index = size + 1;
			g_Guilds[index] = {m_Name = guild, m_TotalCharacterXP = 0, m_Count = 0};
		end
		local entry = g_Guilds[index];
		entry.m_TotalCharacterXP = entry.m_TotalCharacterXP + totalCharacterXP;
		entry.m_Count = entry.m_Count + 1;
	end
end

----------------------------------------------------------------------------------
--
-- Predicate function which can be used to compare two guilds for sorting
-- 
---------------------------------------------------------------------------------
local function GuildPredicate(lhs, rhs)
	--
	-- nil references are always less than
	--
	if (lhs == nil) then
		if (rhs == nil) then
			return false;
		else
			return true;
		end
	elseif (rhs == nil) then
		return false;	
	end
	--
	-- Sort by total XP first
	--
	if (rhs.m_TotalCharacterXP < lhs.m_TotalCharacterXP) then
		return true;
	elseif (lhs.m_TotalCharacterXP < rhs.m_TotalCharacterXP) then
		return false;
	end
	--
	-- Sort by name
	--
	if (lhs.m_Name < rhs.m_Name) then
		return true;
	elseif (rhs.m_Name < lhs.m_Name) then
		return false;
	end
	
	--
	-- identical
	--
	return false;	
end


----------------------------------------------------------------------------------
--
-- Another accumulator for adding up XP and counts
-- 
---------------------------------------------------------------------------------
local g_AccumulatorCount = 0;
local g_AccumulatorXPTotal = 0;
local function Census_Accumulator(name, level, guild)
	local totalCharacterXP = g_TotalCharacterXPPerLevel[level];
	g_AccumulatorXPTotal = g_AccumulatorXPTotal + totalCharacterXP;
	g_AccumulatorCount = g_AccumulatorCount + 1;
end

----------------------------------------------------------------------------------
--
-- Reset the above accumulator
-- 
---------------------------------------------------------------------------------
function Census_ResetAccumulator()
	g_AccumulatorCount = 0;
	g_AccumulatorXPTotal = 0;
end


----------------------------------------------------------------------------------
--
-- Search the character database using the search criteria and update display
-- 
---------------------------------------------------------------------------------
function Census_UpdateView()
	--
	-- Get realm and faction
	--
	local realmName = GetCVar("realmName");
	CensusRealmName:SetText("Realm: "..realmName);
	local factionGroup = UnitFactionGroup("player");
	CensusFactionName:SetText("Faction: "..factionGroup);

	local guildKey = nil;
	local raceKey = nil;
	local classKey = nil;
	g_TotalCharacterXP = 0;
	g_TotalCount = 0;
	
	--
	-- Has the user selected a guild?
	--
	if (g_GuildSelected > 0) then
		guildKey = g_Guilds[g_GuildSelected].m_Name;
	end
	if (g_RaceSelected > 0) then
		local thisFactionRaces = GetFactionRaces(factionGroup);
		raceKey = thisFactionRaces[g_RaceSelected];
	end
	if (g_ClassSelected > 0) then
		local thisFactionClasses = GetFactionClasses(factionGroup);
		classKey = thisFactionClasses[g_ClassSelected];
	end
	
	--
	-- Has the user added any search criteria?
	--
	if ((guildKey ~= nil) or (raceKey ~= nil) or (classKey ~= nil)) then
		--
		-- Get totals for this criteria
		--
		g_AccumulateGuildTotals = false;
		Census_ForAllCharacters(realmName, factionGroup, raceKey, classKey, guildKey, nil, TotalsAccumulator);
	else
		--
		-- Get the overall totals and find guild information
		--
		g_Guilds = {};
		g_AccumulateGuildTotals = true;
		Census_ForAllCharacters(realmName, factionGroup, nil, nil, nil, nil, TotalsAccumulator);
		local size = table.getn(g_Guilds);
		if (size) then
			table.sort(g_Guilds, GuildPredicate);
		end
	end
	CensusTotalCharacters:SetText("Total Characters: "..g_TotalCount);
	CensusTotalCharacterXP:SetText("Total Character XP: "..g_TotalCharacterXP);	
	Census_UpdateGuildButtons();	
	
	--
	-- Accumulate totals for each race
	--
	local maxCount = 0;
	local thisFactionRaces = GetFactionRaces(factionGroup);
	local numRaces = table.getn(thisFactionRaces);
	for i = 1, numRaces, 1 do
		local race = thisFactionRaces[i];
		Census_ResetAccumulator();
		if ((raceKey == nil) or (raceKey == race)) then
			Census_ForAllCharacters(realmName, factionGroup, race, classKey, guildKey, nil, Census_Accumulator);
		end
		if (g_AccumulatorCount > maxCount) then
			maxCount = g_AccumulatorCount;
		end
		g_RaceCount[i] = g_AccumulatorCount;
	end
	--
	-- Update race bars
	--
	for i = 1, numRaces, 1 do
		local race = thisFactionRaces[i];
		local buttonName = "CensusRaceBar"..i;
		local button = getglobal(buttonName);
		local thisCount = g_RaceCount[i];
		if ((thisCount ~= nil) and (thisCount > 0) and (maxCount > 0)) then
			local height = floor((thisCount / maxCount) * CENSUS_MAXBARHEIGHT);
			if (height < 1) then height = 1; end
			button:SetHeight(height);
			button:Show();
		else
			button:Hide();
		end
		local normalTextureName="Interface\\AddOns\\Census\\Census_"..race;
		local legendName = "CensusRaceLegend"..i;
		local legend = getglobal(legendName);
		legend:SetNormalTexture(normalTextureName);
		if (g_RaceSelected == i) then
			legend:LockHighlight();
		else
			legend:UnlockHighlight();
		end
	end	

	--
	-- Accumulate totals for each class
	--
	local maxCount = 0;
	local thisFactionClasss = GetFactionClasses(factionGroup);
	local numClasses = table.getn(thisFactionClasss);
	for i = 1, numClasses, 1 do
		local class = thisFactionClasss[i];
		Census_ResetAccumulator();
		if ((classKey == nil) or (classKey == class)) then
			Census_ForAllCharacters(realmName, factionGroup, raceKey, class, guildKey, nil, Census_Accumulator);
		end
		if (g_AccumulatorCount > maxCount) then
			maxCount = g_AccumulatorCount;
		end
		g_ClassCount[i] = g_AccumulatorCount;
	end
	--
	-- Update class bars
	--
	for i = 1, numClasses, 1 do
		local class = thisFactionClasss[i];
			
		local buttonName = "CensusClassBar"..i;
		local button = getglobal(buttonName);
		local thisCount = g_ClassCount[i];
		if ((thisCount ~= nil) and (thisCount > 0) and (maxCount > 0)) then
			local height = floor((thisCount / maxCount) * CENSUS_MAXBARHEIGHT);
			if (height < 1) then height = 1; end
			button:SetHeight(height);
			button:Show();
		else
			button:Hide();
		end
		
		local normalTextureName="Interface\\AddOns\\Census\\Census_"..class;
		local legendName = "CensusClassLegend"..i;
		local legend = getglobal(legendName);
		legend:SetNormalTexture(normalTextureName);
		if (g_ClassSelected == i) then
			legend:LockHighlight();
		else
			legend:UnlockHighlight();
		end
	end	
	
	--
	-- Accumulate totals for each level
	--
	local maxCount = 0;
	for i = 1, MAX_CHARACTER_LEVEL, 1 do
		Census_ResetAccumulator();
		Census_ForAllCharacters(realmName, factionGroup, raceKey, classKey, guildKey, i, Census_Accumulator);
		if (g_AccumulatorCount > maxCount) then
			maxCount = g_AccumulatorCount;
		end
		g_LevelCount[i] = g_AccumulatorCount;
	end
	--
	-- Update level bars
	--
	for i = 1, MAX_CHARACTER_LEVEL, 1 do
		local buttonName = "CensusLevelBar"..i;
		local button = getglobal(buttonName);
		local thisCount = g_LevelCount[i];
		if ((thisCount ~= nil) and (thisCount > 0) and (maxCount > 0)) then
			local height = floor((thisCount / maxCount) * CENSUS_MAXBARHEIGHT);
			if (height < 1) then height = 1; end
			button:SetHeight(height);
			button:Show();
		else
			button:Hide();
		end
		
	end	
end

----------------------------------------------------------------------------------
--
-- Walk the character database and call the callback function for every entry that matches the search criteria
-- 
---------------------------------------------------------------------------------
function Census_ForAllCharacters(realmKey, factionKey, raceKey, classKey, guildKey, levelKey, callback)
	for realmName, realmDatabase in pairs(Census_Database) do
		if ((realmKey == nil) or (realmKey == realmName)) then
			for factionName, factionDatabase in pairs(realmDatabase) do
				if ((factionKey == nil) or (factionKey == factionName)) then
					for raceName, raceDatabase in pairs(factionDatabase) do
						if ((raceKey == nil) or (raceKey == raceName)) then
							for className, classDatabase in pairs(raceDatabase) do
								if ((classKey == nil) or (classKey == className)) then
									for characterName, character in pairs(classDatabase) do
										local characterLevel = character[1];
										if ((levelKey == nil) or (levelKey == characterLevel)) then
											local characterGuild = character[2];
											if ((guildKey == nil) or (guildKey == characterGuild)) then
												callback(characterName, characterLevel, characterGuild);
											end
										end
									end
								end
							end
						end
					end
				end
			end
		end
	end
end

----------------------------------------------------------------------------------
--
-- Race legend clicked
-- 
---------------------------------------------------------------------------------
function Census_OnClickRace()
	local id = this:GetID();
	if (id == g_RaceSelected) then
		g_RaceSelected = 0;
	else
		g_RaceSelected = id;
	end
	Census_UpdateView();
end

----------------------------------------------------------------------------------
--
-- Class legend clicked
-- 
---------------------------------------------------------------------------------
function Census_OnClickClass()
	local id = this:GetID();
	if (id == g_ClassSelected) then
		g_ClassSelected = 0;
	else
		g_ClassSelected = id;
	end
	Census_UpdateView();
end

----------------------------------------------------------------------------------
--
-- Race tooltip
-- 
---------------------------------------------------------------------------------
function Census_OnEnterRace()
	local factionGroup = UnitFactionGroup("player");
	local thisFactionRaces = GetFactionRaces(factionGroup);
	local id = this:GetID();
	local raceName = thisFactionRaces[id];
	local count = g_RaceCount[id];
	local percent = floor((count / g_TotalCount) * 100);
	GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
	GameTooltip:SetText(raceName.."\n"..count.."\n"..percent.."%", 1.0, 1.0, 1.0);
	GameTooltip:Show();
end

----------------------------------------------------------------------------------
--
-- Class tooltip
-- 
---------------------------------------------------------------------------------
function Census_OnEnterClass()
	local factionGroup = UnitFactionGroup("player");
	local thisFactionClasses = GetFactionClasses(factionGroup);
	local id = this:GetID();
	local className = thisFactionClasses[id];
	local count = g_ClassCount[id];
	local percent = floor((count / g_TotalCount) * 100);
	GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
	GameTooltip:SetText(className.."\n"..count.."\n"..percent.."%", 1.0, 1.0, 1.0);
	GameTooltip:Show();
end

----------------------------------------------------------------------------------
--
-- Level tooltip
-- 
---------------------------------------------------------------------------------
function Census_OnEnterLevel()
	local id = this:GetID();
	local count = g_LevelCount[id];
	if (count ~= nil) then
		local percent = floor((count / g_TotalCount) * 100);
		GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
		GameTooltip:SetText("Level "..id.."\n"..count.."\n"..percent.."%", 1.0, 1.0, 1.0);
		GameTooltip:Show();
	end
end

----------------------------------------------------------------------------------
--
-- Clicked a guild button
-- 
---------------------------------------------------------------------------------
function Census_GuildButton_OnClick()
	local id = this:GetID();
	local offset = FauxScrollFrame_GetOffset(CensusGuildScrollFrame);
	local newSelection = id + offset;
	if (g_GuildSelected ~= newSelection) then
		g_GuildSelected = newSelection;
	else
		g_GuildSelected = 0;
	end
	Census_UpdateView();
end

----------------------------------------------------------------------------------
--
-- Update the guild button contents
-- 
---------------------------------------------------------------------------------
function Census_UpdateGuildButtons()
	--
	-- Determine where the scroll bar is
	--
	local offset = FauxScrollFrame_GetOffset(CensusGuildScrollFrame);
	--
	-- Walk through all the rows in the frame
	--
	local size = table.getn(g_Guilds);
	local i = 1;
	while (i <= CENSUS_NUMGUILDBUTTONS) do
		--
		-- Get the index to the ad displayed in this row
		--
		local iGuild = i + offset;
		--
		-- Get the button on this row
		--
		local button = getglobal("CensusGuildButton"..i);
		--
		-- Is there a valid guild on this row?
		--
		if (iGuild <= size) then
			local guild = g_Guilds[iGuild];
			--
			-- Update the button text
			--
			button:Show();
			local textField = "CensusGuildButton"..i.."Text";
			if (guild.m_Name == "") then
				getglobal(textField):SetText("(Unguilded)");
			else
				getglobal(textField):SetText(guild.m_Name);
			end
			--
			-- If this is the guild, highlight it
			--
			if (g_GuildSelected == iGuild) then
				button:LockHighlight();
			else
				button:UnlockHighlight();
			end
		else
			--
			-- Hide the button
			--
			button:Hide();
		end
		--
		-- Next row
		--
		i = i + 1;
	end
	--
	-- Update the scroll bar
	--
	FauxScrollFrame_Update(CensusGuildScrollFrame, size, CENSUS_NUMGUILDBUTTONS, CENSUS_GUILDBUTTONSIZEY);
end
