Quantcast

local	LFGBSPrivate =
	{
		CommentPlayer = {},
		Map =
			{
				Cmp = {},
			},
		TrackerFrame = {},
		SizeStat = {},
	};

local	function Message(s)
	DEFAULT_CHAT_FRAME:AddMessage(s);
end

--
--

local	function	DaysNum2Str(DAYS)
	local	sDays = "MTWTFSS";
	local	mask, i = 1;
	local	sResult = "";
	for i = 1, 7 do
		if (bit.band(DAYS, mask) == mask) then
			sResult = sResult .. strsub(sDays, i, i);
		else
			sResult = sResult .. "_";
		end

		mask = mask * 2;
	end

	return sResult;
end

--
--
--

local	LookingForGuildBrowseSortListLocal;

local	GUILDLIST = nil;
local	GUILDNAMES = nil;
local	GUILDNAMESOLD = nil;
local	GUILDCOMMENTS = nil;
local	GUILDSCORE = nil;
local	GUILDSCOREOLD = nil;
local	GUILDMEMBERCOUNTS = nil;
local	GUILDRANKS = nil;
local	GUILDTRACKEDCUT = nil;

local	GUILDSORT = "SCORE";

local	Cmd;	-- forward declaration

local	function	MapReset(sSrc)
	-- Message("Recruiting guilds: preparing for list update... (" .. tostring(sSrc) .. ")");
	if ((GUILDSORT or "SCORE") == "SCORE") then
		GUILDNAMESOLD = GUILDNAMES;
		GUILDSCOREOLD = GUILDSCORE;
	end

	GUILDLIST = nil;
	GUILDNAMES = nil;
	GUILDCOMMENTS = nil;
	GUILDSCORE = nil;
	GUILDMEMBERCOUNTS = nil;
	GUILDRANKS = nil;
	GUILDTRACKEDCUT = nil;
end

--
--
--

local	CHOICES_SHORTLONG = { Q = "QUEST", D = "DUNGEON", R = "RAID", P = "PVP", O = "RP", Y = "WEEKDAYS", E = "WEEKENDS" };
local	CHOICES_SHORTINDEX = { Q = 1, D = 2, R = 3, P = 4, O = 5, Y = 6, E = 7, T = 8, H = 9, S = 10 };
local	CHOICES_INDEXLONG = { [1] = "QUEST", [2] = "DUNGEON", [3] = "RAID", [4] = "PVP", [5] = "RP", [6] = "WEEKDAYS", [7] = "WEEKENDS" };

local	oMUST = nil;

function	LFGBSPrivate.CommentPlayer.Show()
	local	cOption, iOption;
	for cOption, iOption in pairs(CHOICES_SHORTINDEX) do
		local	sOption = CHOICES_SHORTLONG[cOption];
		if (sOption) then	-- roles are not in CHOICES_SHORTLONG
			local	sTextKey = _G["GUILD_INTEREST_" .. sOption] or _G["GUILD_AVAILABILITY_" .. sOption];
			if (not sTextKey) then
				Message("LFGBS::CPS: " .. sOption .. " => !Group");
			else
				local	sOptionCamel = strupper(strsub(sOption, 1, 1)) .. strlower(strsub(sOption, 2));
				local	iLen = strlen(sOptionCamel);
				if (strsub(sOptionCamel, iLen, iLen) == 'p') then
					-- RP, PvP: special capitalization
					sOptionCamel = strsub(sOptionCamel, 1, iLen - 1) .. "P";
				end

				local	oButton = _G["LookingForGuild" .. sOptionCamel .. "Text"] or _G["LookingForGuild" .. sOptionCamel .. "ButtonText"];
				if (not oButton) then
					Message("LFGBS::CPS: " .. sOptionCamel .. " => !Button");
				else
					local	sText = oButton:GetText();
					if (oMUST[sOption]) then
						if (strsub(sText, 1, 2) ~= "! ") then
							oButton:SetText("! " .. sText);
						end
					else
						if (strsub(sText, 1, 2) == "! ") then
							oButton:SetText(strsub(sText, 3));
						end
					end
				end
			end
		end
	end
end

function	LFGBSPrivate.CommentPlayer.Parse()
	local	sPComment, bMUSTValid = GetLookingForGuildComment();
	if (sPComment == LFGBSPrivate.CommentPlayer.sPComment) then
		return (oMUST ~= nil) and (next(oMUST) ~= nil);
	end
	LFGBSPrivate.CommentPlayer.sPComment = sPComment;

	local	iLen = strlen(sPComment);
	if (iLen <= 1) then
		oMUST = nil;
		return bMUSTValid;
	end

	oMUST = {};
	if (strsub(sPComment, iLen, iLen) == "$") then
		-- stronger WANTs/DONTWANTs
		local	i = iLen - 1;
		while (i > 0 and strsub(sPComment, i, i) ~= "$") do
			local	c = strsub(sPComment, i, i);
			if (CHOICES_SHORTLONG[c]) then
				oMUST[CHOICES_SHORTLONG[c]] = true;
			end

			if (c >= '0' and c <= '9') then
				oMUST.DAYS = (oMUST.DAYS or 0) * 8 + tonumber(c);
			end

			if ((c == "!") and (i >= 2)) then
				-- next (previous) char defines the minimum size of the guild (raids)
				-- simply taking the alphabetic position as min size
				local	c_next = strbyte(sPComment, i - 1, i - 1) - strbyte("A");
				if (c_next >= 0 and c_next <= 25) then
					i = i - 1;
					oMUST.SIZE = 5 + 2 * c_next;
				end
			end

			if ((c == "#") and (i >= 2)) then
				-- next (previous) char defines the minimum rank of the guild (perks)
				-- simply taking the alphabetic position as min rank
				local	c_next = strbyte(sPComment, i - 1, i - 1) - strbyte("A");
				if (c_next >= 0 and c_next <= 25) then
					i = i - 1;
					oMUST.RANK = c_next;
				end
			end

			if ((c == "?") and (i >= 3)) then
				-- next (previous) TWO char defines the play time (to, from)
				-- A-X = 0:00 .. 23:00, a-x = 0:30 .. 23:30
				local	_A = strbyte("A");
				local	_a = strbyte("a");

				local	c_from, i_from = strbyte(sPComment, i - 2, i - 2);
				if ((c_from >= _A) and (c_from <= _A + 23)) then
					i_from = c_from - _A;
				elseif ((c_from >= _a) and (c_from <= _a + 23)) then
					i_from = c_from - _a + 0.5;
				end

				local	c_to, i_to = strbyte(sPComment, i - 1, i - 1);
				if ((c_to >= _A) and (c_to <= _A + 23)) then
					i_to = c_to - _A;
				elseif ((c_to >= _a) and (c_to <= _a + 23)) then
					i_to = c_to - _a + 0.5;
				end

				if (i_from and i_to) then
					i = i - 2;
					oMUST.TIME = { FROM = i_from, TO = i_to };
				end
			end

			i = i - 1;
		end

		if (i > 0 and strsub(sPComment, i, i) == "$") then
			bMUSTValid = true;
			LookingForGuildBrowseSortListLocal.Config.TIME = oMUST.TIME;
			LFGBSPrivate.CommentPlayer.Show();
		end
	end

	return bMUSTValid;
end

-- create comment blob from oMUST
function	LFGBSPrivate.CommentPlayer.Compile()
	local sResult = "";
	if (oMUST and (next(oMUST) ~= nil)) then
		local k, v;
		for k, v in pairs(CHOICES_SHORTLONG) do
			if (oMUST[v]) then
				sResult = k .. sResult;
			end
		end

		local	iNull = strbyte("0");
		local	iA = strbyte("A");
		local	ia = strbyte("a");

		if ((type(oMUST.DAYS) == "number") and (oMUST.DAYS > 0)) then
			local	iDays = oMUST.DAYS;
			local	sDays = strchar(iNull + bit.band(iDays, 7)) .. strchar(iNull + bit.band(bit.rshift(iDays, 3), 7)) .. strchar(iNull + bit.rshift(iDays, 6));
			sResult = sDays .. sResult;
		end

		if ((type(oMUST.SIZE) == "number") and (oMUST.SIZE >= 5)) then
			sResult = strchar(iA + floor(oMUST.SIZE - 5) / 2) .. "!" .. sResult;
		end

		if ((type(oMUST.RANK) == "number") and (oMUST.RANK > 0)) then
			sResult = strchar(iA + oMUST.RANK) .. "#" .. sResult;
		end

		if (type(oMUST.TIME) == "table") then
			sResult = "?" .. sResult;

			local	i, iTime;
			for i = 1, 2 do
				if (i == 1) then
					iTime = oMUST.TIME.TO;
				else
					iTime = oMUST.TIME.FROM;
				end

				local	bSmall = iTime ~= floor(iTime);
				if (bSmall) then
					sResult = strchar(ia + floor(iTime)) .. sResult;
				else
					sResult = strchar(iA +       iTime ) .. sResult;
				end
			end
		end
	end

	return sResult;
end

-- update comment blob
function	LFGBSPrivate.CommentPlayer.Update()
	local	sPComment = GetLookingForGuildComment();
	LFGBSPrivate.CommentPlayer.sPComment = sPComment;

	local	iCommentDataStartsAt = nil;
	local	iLen = strlen(sPComment);
	if (iLen > 1) then
		if (strsub(sPComment, iLen, iLen) == "$") then
			local	i = iLen - 1;
			while (i > 0 and strsub(sPComment, i, i) ~= "$") do
				i = i - 1;
			end

			if ((i > 0) and (strsub(sPComment, i, i) == "$")) then
				iCommentDataStartsAt = i;
			end
		end
	end

	local	sBlob = LFGBSPrivate.CommentPlayer.Compile();
	if (sBlob ~= "") then
		sBlob = "$" .. sBlob .. "$";
	end

	-- insert, change or delete comment blob
	if (iCommentDataStartsAt) then
		sPComment = strsub(sPComment, 1, iCommentDataStartsAt - 1) .. sBlob;
	else
		if (iLen > 0) then
			sPComment = sPComment .. " " .. sBlob;
		else
			sPComment = sPComment .. sBlob;
		end
	end

	if (LFGBSPrivate.CommentPlayer.sPComment ~= sPComment) then
		LFGBSPrivate.CommentPlayer.sPComment = nil;
		SetLookingForGuildComment(sPComment);
		LookingForGuildCommentEditBox:SetText(sPComment);
		LFGBSPrivate.CommentPlayer.Show();

		return true;
	end
end

function	LFGBSPrivate.CommentPlayer.ButtonToggle(self, btn, holding)
	-- hooked, so called after default handler
	-- Message("Btn:OnClick(" .. btn .. ")");
	if (btn == "RightButton") then
		-- checkboxes: toggle MUST|MUSTN'T
		local	sName = self:GetName();
		local	sLong = strsub(sName, 16, -7);
		sLong = strupper(sLong);
		Cmd("toggle " .. sLong, true);

		-- undo change from regular handler
		self:Click("LeftButton");
		SetLookingForGuildSettings(self:GetID(), self:GetChecked());
		MapReset();
	end
end

local	GetNumRecruitingGuilds_original;
local	iGetNumRecruitingGuildsCut;
local	GetRecruitingGuildInfo_original;
local	GetRecruitingGuildSettings_original;

local	GroupGuildnameColors = {
		[1] = { r = 0.5, g = 1,   b = 0.5 },
		[2] = { r = 0.5, g = 0.5, b = 1   },
		[3] = { r = 1,   g = 1,   b = 0.5 },
		[4] = { r = 1,   g = 0.5, b = 0.5 },
		[5] = { r = 0.5, g = 0.5, b = 0.5 },			-- grey: too small
		[9] = { r = 0.5, g = 0.5, b = 0.5 },			-- grey: no members
	};

local	function	MapCollate(oG)
	local	iMaxOld = GUILDNAMESOLD and #GUILDNAMESOLD or -1;

	GUILDLIST = {};
	GUILDNAMES = {};
	GUILDCOMMENTS = {};
	GUILDSCORE = {};

	GUILDMEMBERCOUNTS = {};
	GUILDRANKS = {};

	GUILDTRACKEDCUT = {};
	local	oTracked = {};
	do
		local	iTrackedMax, iTracked = #LookingForGuildBrowseSortListLocal.Tracker;
		for iTracked = 1, iTrackedMax do
			oTracked[LookingForGuildBrowseSortListLocal.Tracker[iTracked].Name] = iTracked;
		end
	end

	iGetNumRecruitingGuildsCut = 0;
	local	iG;
	for iG = 1, #oG do
		local	iMax, i = #oG[iG];
		if (iG <= 3) then
			iGetNumRecruitingGuildsCut = iGetNumRecruitingGuildsCut + iMax;
		end

		for i = 1, iMax do
			tinsert(GUILDLIST, oG[iG][i].index);
			tinsert(GUILDNAMES, oG[iG][i].name);
			tinsert(GUILDCOMMENTS, oG[iG][i].infotext);
			tinsert(GUILDSCORE, oG[iG][i].scoreOuter);

			tinsert(GUILDMEMBERCOUNTS, oG[iG][i].members);
			tinsert(GUILDRANKS, oG[iG][i].rank);
			if (iG > 3) and (next(oTracked) ~= nil) then
				-- if tracked, add to extra list
				if (oTracked[oG[iG][i].name]) then
					tinsert(GUILDTRACKEDCUT, #GUILDLIST);
				end

				oTracked[oG[iG][i].name] = nil;
			end
		end
	end

	if (LookingForGuildBrowseSortListLocal == nil) then
		LookingForGuildBrowseSortListLocal = {};
	end
	if (LookingForGuildBrowseSortListLocal.Debug) then
		LookingForGuildBrowseSortListLocal.Score = { M = oMUST, L = GUILDLIST, N = GUILDNAMES };
	end

	local	iMax, i = #GUILDNAMES;
	if (iMax ~= iMaxOld) then
		return true;
	else
		for i = 1, iMax do
			if (
				(GUILDNAMES[i] ~= GUILDNAMESOLD[i]) or
				(GUILDSCORE[i] ~= GUILDSCOREOLD[i])
			   ) then
				return true;
			end
		end
	end

	return false;
end

local	function	MapCreateByScore()
	local	oSettingsPlayer = { GetLookingForGuildSettings() };

	local	bMUSTValid = LFGBSPrivate.CommentPlayer.Parse();

	local	iCnt, i = GetNumRecruitingGuilds_original();
	local	oG = { [1] = {}, [2] = {}, [3] = {}, [4] = {}, [5] = {} };
	for i = 1, iCnt do
		local	iScore, iG = 0;
		local	name, rank, members, achieved, infotext, cached, requestPending = GetRecruitingGuildInfo_original(i);
		if (members == 0) then
			-- lots of empty guilds on PTR...
			iG = #oG;
			iScore = 1000;
		else
			local	iLen = strlen(infotext);
			local	oREQ, bREQValid = {};
			if (iLen > 1) then
				if (strsub(infotext, iLen, iLen) == "$") then
					-- stronger WANTs/DONTWANTs
					local	i = iLen - 1;
					while (i > 0 and strsub(infotext, i, i) ~= "$") do
						local	c = strsub(infotext, i, i);
						if (c >= '0' and c <= '9') then
							oREQ.DAYS = (oREQ.DAYS or 0) * 8 + tonumber(c);
						end

						i = i - 1;
					end

					if (i > 0 and strsub(sPComment, i, i) == "$") then
						bREQValid = true;
					end
				end
			end

			-- the server already checks that at least one of each group (interest, schedule, role) is selected by player and guild
			local	oSettingsGuild, iOpt = { GetRecruitingGuildSettings_original(i) };
			local	bSame = true;
			for iOpt = 1, 7 do
				if (oSettingsPlayer[iOpt] ~= oSettingsGuild[iOpt]) then
					bSame = false;
					break;
				end
			end

			if (bSame) then
				-- G1: perfect match
				iG = 1;
			else
				-- G2/G3: guild has what we require, and has not what we refuse (MUST|MUSTN'T is valid)
				-- G2: guild has some extras player doesn't have
				-- G3: guild misses an extra player does want
				-- G4: guild has a fundamentally different orientation (violating one or more MUST|MUSTN'T choices)
				-- G5: guild is too small

				iG = 4;
				--[[
					MUST PLAYER GUILD GROUP
					M  P  G  G
					1  0  0  2
					1  1  1  2
					1  0  1  4
					1  1  0  4

					0  0  0  2
					0  0  1  2
					0  1  0  3
					0  1  1  2
				]]--

				if (bMUSTValid) then
					local	bViolation = false;
					local	bMissing = false;
					for iOpt = 1, 7 do
						if (oMUST[CHOICES_INDEXLONG[iOpt]]) then
							if (oSettingsPlayer[iOpt] ~= oSettingsGuild[iOpt]) then
								bViolation = true;
							end
						elseif (oSettingsPlayer[iOpt] and not oSettingsGuild[iOpt]) then
							bMissing = true;
						end
					end
					if (oMUST.DAYS and oREQ.DAYS and (bit.band(oMUST.DAYS, oREQ.DAYS) == 0)) then
						bViolation = true;
					end
					if (oMUST.SIZE and (members < oMUST.SIZE)) then
						bViolation = true;
						iG = 5;
					end
					if (oMUST.RANK and (rank < oMUST.RANK)) then
						bViolation = true;
						iG = 5;
					end

					if (not bViolation) then
						iG = 2;
						if (bMissing) then
							iG = 3;
						end

						if (oMUST.DAYS and oREQ.DAYS and bit.band(oMUST.DAYS, oREQ.DAYS) ~= 0) then
							local	iNum, mask = 0, 1;
							for i = 1, 7 do
								if (bit.band(oMUST.DAYS, mask) ~= bit.band(oREQ.DAYS, mask)) then
									iNum = iNum + 1;
								end

								mask = mask * 2;
							end

							iScore = iScore + iNum * 25;
						end
					end
				else
					local	bExtra = false;
					local	bMissing = false;
					for iOpt = 1, 7 do
						if (not oSettingsPlayer[iOpt] and     oSettingsGuild[iOpt]) then
							bExtra = true;
						end
						if (    oSettingsPlayer[iOpt] and not oSettingsGuild[iOpt]) then
							bMissing = true;
						end
					end

					iG = 1;			-- hm, cannot happen, really...
					if (bExtra) then
						iG = 2;
					end
					if (bMissing) then
						iG = 3;
					end
				end

				local	oMalus = { 10, 10, 10, 25, 10, 15, 15 };
				for iOpt = 1, 7 do
					if (oSettingsPlayer[iOpt] ~= oSettingsGuild[iOpt]) then
						local	iMulti = 1;
						if (oSettingsPlayer[iOpt]) then
							iMulti = 2;
						end
						if (bMUSTValid and oMUST[CHOICES_INDEXLONG[iOpt]]) then
							iMulti = 10;
						end

						iScore = iScore + oMalus[iOpt] * iMulti;
					end
				end
			end
		end

		local	fScore =  iScore + 10000 * (iG - 1) + 1 / (5000 - members);
		tinsert(oG[iG], { index = i, name = name, infotext = infotext, scoreInner = iScore, scoreOuter = fScore, rank = rank, members = members } );
	end

	local	oTracked = {};
	local	iTrackedMax, iTracked = #LookingForGuildBrowseSortListLocal.Tracker;
	for iTracked = 1, iTrackedMax do
		oTracked[LookingForGuildBrowseSortListLocal.Tracker[iTracked].Name] = iTracked;
	end

	local	oColor = GroupGuildnameColors[1];
	local	sColor = format("|cFF%.2X%.2X%.2X", oColor.r * 255, oColor.g * 255, oColor.b * 255);
	local	sTotals = "[Recruiting guilds] " .. sColor .. "perfect matches: ";

	local	iG;
	for iG = 1, #oG do
		local	iMax, i, j = #oG[iG];
		for i = 1, iMax - 1 do
			for j = i + 1, iMax do
				if (
					(oG[iG][i].scoreInner > oG[iG][j].scoreInner) or
					(
						(oG[iG][i].scoreInner == oG[iG][j].scoreInner) and
						(
							(oG[iG][i].rank < oG[iG][j].rank) or
							(oG[iG][i].rank == oG[iG][j].rank) and (oG[iG][i].members < oG[iG][j].members)
						)
					)
				    ) then
					local tmp = oG[iG][i];
					oG[iG][i] = oG[iG][j];
					oG[iG][j] = tmp;
				end
			end
		end
	end

	local	bModified = MapCollate(oG);
	if (bModified) then
		local	nTrackedTotal = 0;
		for iG = 1, #oG do
			local	nTracked = 0;
			local	iMax, i, j = #oG[iG];
			for i = 1, iMax do
				if (oTracked[oG[iG][i].name]) then
					nTracked = nTracked + 1;
				end
			end

			if ((iG ~= 5) or (#oG[5] > 0)) then
				sTotals = sTotals .. (iMax > 0 and iMax or "none");
				if (nTracked > 0) then
					nTrackedTotal = nTrackedTotal + nTracked;
					sTotals = sTotals .. " (tracked: " .. nTracked .. ")";
				end
			end

			if (iG == 1) then
				local	oColor = GroupGuildnameColors[2];
				local	sColor = format("|cFF%.2X%.2X%.2X", oColor.r * 255, oColor.g * 255, oColor.b * 255);
				sTotals = sTotals .. "|r, " .. sColor .. "must-and-mustn't-plus-extras matches: ";
			elseif (iG == 2) then
				local	oColor = GroupGuildnameColors[3];
				local	sColor = format("|cFF%.2X%.2X%.2X", oColor.r * 255, oColor.g * 255, oColor.b * 255);
				sTotals = sTotals .. "|r, " .. sColor .. "missing-optional-choices matches: ";
			elseif (iG == 3) then
				local	oColor = GroupGuildnameColors[4];
				local	sColor = format("|cFF%.2X%.2X%.2X", oColor.r * 255, oColor.g * 255, oColor.b * 255);
				sTotals = sTotals .. "|r, " .. sColor .. "strong-differences (mis)matches: ";
			elseif (iG == 4) and (#oG[5] > 0) then
				local	oColor = GroupGuildnameColors[5];
				local	sColor = format("|cFF%.2X%.2X%.2X", oColor.r * 255, oColor.g * 255, oColor.b * 255);
				sTotals = sTotals .. "|r, " .. sColor .. "too small/too low to consider: ";
			end
		end

		sTotals = sTotals .. "|r";
		if (LookingForGuildBrowseSortListLocal.Config.HideRedGrey) then
			if ((type(GUILDTRACKEDCUT) == "table") and (#GUILDTRACKEDCUT > 0)) then
				sTotals = sTotals .. " - red/grey tracked: " .. #GUILDTRACKEDCUT;
			end
		end
		if (iTrackedMax > nTrackedTotal) then
			sTotals = sTotals .. " - tracked unlisted: " .. (iTrackedMax - nTrackedTotal);
		end

		Message(sTotals);
	end
end

function	LFGBSPrivate.Map.Cmp.NAME(oA, oB)
	return strlower(oA.name) > strlower(oB.name);
end

function	LFGBSPrivate.Map.Cmp.SIZE(oA, oB)
	return (oA.members < oB.members) or (oA.members == oB.members) and (oA.rank < oB.rank);
end

function	LFGBSPrivate.Map.Cmp.RANK(oA, oB)
	return (oA.rank < oB.rank) or (oA.rank == oB.rank) and (oA.members < oB.members);
end

local	function	MapCreateByNameOrSizeOrRank(bName)
	local	iCnt, i = GetNumRecruitingGuilds_original();
	local	oG, iG = { [1] = {}, [2] = {} };

	local	iSizeMin = oMUST and oMUST.SIZE or 0;
	local	iRankMin = oMUST and oMUST.RANK or 0;
	for i = 1, iCnt do
		local	name, rank, members, achieved, infotext, cached, requestPending = GetRecruitingGuildInfo_original(i);

		iG = 1;
		if (GUILDSORT == "SIZE") then
			if (members < iSizeMin) then
				iG = 2;
			end
		elseif (GUILDSORT == "RANK") then
			if (rank < iRankMin) then
				iG = 2;
			end
		end

		local	fScore =  40000.001 * (iG - 1);
		tinsert(oG[iG], { index = i, name = name, rank = rank, members = members, infotext = infotext, scoreOuter = fScore } );
	end

	local	fSort = LFGBSPrivate.Map.Cmp[GUILDSORT];

	local	iG;
	for iG = 1, #oG do
		local	iMax, i, j = #oG[iG];
		for i = 1, iMax - 1 do
			for j = i + 1, iMax do
				if (fSort(oG[iG][i], oG[iG][j])) then
					local tmp = oG[iG][i];
					oG[iG][i] = oG[iG][j];
					oG[iG][j] = tmp;
				end
			end
		end
	end

	MapCollate(oG);
	iGetNumRecruitingGuildsCut = nil;
end

local	function	MapCreate()
	if (GUILDSORT == "NAME") then
		MapCreateByNameOrSizeOrRank();
	elseif (GUILDSORT == "SIZE") then
		MapCreateByNameOrSizeOrRank();
	elseif (GUILDSORT == "RANK") then
		MapCreateByNameOrSizeOrRank();
	else
		GUILDSORT = "SCORE";
		MapCreateByScore();
	end
end

local	function	MapDo(iNum)
	if (GUILDLIST == nil) then
		MapCreate();
	end

	if (GUILDLIST ~= nil) then
		if (LookingForGuildBrowseSortListLocal.Config.HideRedGrey and (iGetNumRecruitingGuildsCut ~= nil)) then
			if (iNum > iGetNumRecruitingGuildsCut) then
				local	iSub = iNum - iGetNumRecruitingGuildsCut;
				if (type(GUILDTRACKEDCUT) == "table") and (iSub <= #GUILDTRACKEDCUT) then
					iSub = GUILDTRACKEDCUT[iSub];
					return GUILDLIST[iSub];
				end
			end
		end

		return GUILDLIST[iNum];
	else
		return iNum;
	end
end

local	function	GetNumRecruitingGuilds_ordered()
	if (LookingForGuildBrowseSortListLocal.Config.HideRedGrey and (iGetNumRecruitingGuildsCut ~= nil)) then
		local	iTotal = iGetNumRecruitingGuildsCut or 0;
		if (type(GUILDTRACKEDCUT) == "table") then
			iTotal = iTotal + #GUILDTRACKEDCUT;
		end

		return iTotal;
	end

	return GetNumRecruitingGuilds_original();
end

local	function	GetRecruitingGuildSettings_ordered(iNum)
	local	_iNum = MapDo(iNum);
	if (not _iNum) then
		Message("[LFGBS] GRGS: Index " .. iNum .. " -> nil");
		return GetRecruitingGuildSettings_original(iNum);
	else
		local	bQuetes = GetRecruitingGuildSettings_original(_iNum);
		if (bQuetes == nil) then
			Message("[LFGBS] GRGS: Index " .. _iNum .. " (from " .. iNum .. ") -> nil");
			return GetRecruitingGuildSettings_original(iNum);
		end
	end

	return GetRecruitingGuildSettings_original(_iNum);
end

local	function	GetRecruitingGuildInfo_ordered(iNum)
	local	_iNum = MapDo(iNum);
	if (not _iNum) then
		Message("[LFGBS] GRGI: Index " .. iNum .. " -> nil");
		return GetRecruitingGuildInfo_original(iNum);
	else
		local	sName = GetRecruitingGuildInfo_original(_iNum);
		if (sName == nil) then
			Message("[LFGBS] GRGI: Index " .. _iNum .. " (from " .. iNum .. ") -> nil");
			return GetRecruitingGuildInfo_original(iNum);
		end
	end

	return GetRecruitingGuildInfo_original(_iNum);
end

local	function 	GuildnameColorize()
	local	sIndices = "";
	local	oButtons = LookingForGuildBrowseFrameContainer.buttons;
	local	iCnt, i = #oButtons;
	for i = 1, iCnt do
		local	oButton = oButtons[i];
		if (oButton:IsShown()) then
			local	iIndex, oColor = oButton.index;
			sIndices = sIndices .. ", " .. iIndex;
			if (not GUILDSCORE or (next(GUILDSCORE) == nil) or (GUILDSCORE[iIndex] == 0)) then
				-- sorted by name/size
				oColor = { r = 1, g = 1, b = 0.5 };
			else
				local	iScore = GUILDSCORE[iIndex];
				local	iMembers = GUILDMEMBERCOUNTS[iIndex];
				oColor = GroupGuildnameColors[9];
				if (iMembers >= 0.5) then
					local	iColorIndex = 1 + math.floor(iScore / 10000);
					if (GroupGuildnameColors[iColorIndex]) then
						oColor = GroupGuildnameColors[iColorIndex];
					end
				end
			end

			local	oName = oButton.name;
			if (oColor) then
				oName:SetTextColor(oColor.r, oColor.g, oColor.b, 1);
			else
				oName:SetTextColor(1, 1, 0.5, 1);
			end
		end
	end

	if (GameTooltip:IsShown()) then
		local	oFrame = GetMouseFocus();
		if (oFrame and (type(oFrame) == "table") and (type(oFrame.GetName) == "function")) then
			local	sName = oFrame:GetName();
			if (strsub(sName, 1, 41) == "LookingForGuildBrowseFrameContainerButton") then
				-- Message("[LFGBS] Refreshing tooltip...");
				local	OnLeave = oFrame:GetScript("OnLeave");
				if (type(OnLeave) == "function") then
					OnLeave(oFrame);
				end
				local	OnEnter = oFrame:GetScript("OnEnter");
				if (type(OnEnter) == "function") then
					OnEnter(oFrame);
				end
			end
		end
	end

	-- Message("[LFGBS] GnC -> " .. strsub(sIndices, 2));
end

local	bLookingForGuildGuild_ShowTooltip_Hooked;
local	function	LookingForGuildGuild_ShowTooltip_Hook(self)
	if (LookingForGuildBrowseSortListLocal == nil) then
		LookingForGuildBrowseSortListLocal = {};
	end
	if (LookingForGuildBrowseSortListLocal.Debug) then
		LookingForGuildBrowseSortListLocal.Tip = {};
	end

	local	bShow;
	local	oPLFG = { GetLookingForGuildSettings() };
	local	oMissing = {};
	local	iLineCnt, i = GameTooltip:NumLines();
	for i = 1, iLineCnt do
		-- recolorize
		local	oText = _G["GameTooltipTextLeft" .. i];
		local	sLine = oText:GetText();
		if ((strfind(sLine, GUILD_INTEREST, 1, true) == 1) or (strfind(sLine, GUILD_AVAILABILITY, 1, true) == 1)) then
			local	sPrefix;
			if (strfind(sLine, GUILD_INTEREST, 1, true) == 1) then
				sPrefix = "GUILD_INTEREST_";
			elseif (strfind(sLine, GUILD_AVAILABILITY, 1, true) == 1) then
				sPrefix = "GUILD_AVAILABILITY_";
			end
			if (sPrefix) then
				local	k, v;
				if (oMUST ~= nil) then
					for k, v in pairs(oMUST) do
						local	sSub, iPosA, iPosB
						if (type(v) == "boolean") then
							sSub = _G[sPrefix .. k];
						end

						if (sSub) then
							iPosA, iPosB = strfind(sLine, sSub, 1, true);
							if (iPosA and iPosB) then
								local	iChoice, l, w;
								for l, w in pairs(CHOICES_SHORTLONG) do
									if (w == k) then
										iChoice = CHOICES_SHORTINDEX[l];
									end
								end
								if (iChoice) then
									local	sFlag = "";
									local	sColor = "30FF30";
									if (not oPLFG[iChoice]) then
										sColor = "FF5050";
										sFlag = " (MUSTN'T)"
									end
									oPLFG[iChoice] = nil;

									sLine = strsub(sLine, 1, iPosA - 1) ..
										"|r|cFF" .. sColor .. strsub(sLine, iPosA, iPosB) .. sFlag .. "|r" ..
										HIGHLIGHT_FONT_COLOR_CODE .. strsub(sLine, iPosB + 1);
								end
							else
								-- Message("LFGBS::LFGG_ST: " .. sPrefix .. "::" .. sSub .. " !€ " .. sLine);
								-- if (LookingForGuildBrowseSortListLocal.Debug) then
									-- LookingForGuildBrowseSortListLocal.Tip[100] = { sSub = sSub, sLine = sLine };
								-- end
								local	iChoice, l, w;
								for l, w in pairs(CHOICES_SHORTLONG) do
									if (w == k) then
										iChoice = CHOICES_SHORTINDEX[l];
									end
								end
								if (iChoice and oPLFG[iChoice]) then
									oMissing[k] = true;
									oPLFG[iChoice] = nil;
								end
							end
						end
					end
				end

				for k, v in pairs(CHOICES_SHORTLONG) do
					local	iChoice = CHOICES_SHORTINDEX[k];
					if (oPLFG[iChoice]) then
						local	sSub, iPosA, iPosB = _G[sPrefix .. v];
						if (sSub) then
							local	iPosA, iPosB = strfind(sLine, sSub, 1, true);
							if (iPosA and iPosB) then
								sLine = strsub(sLine, 1, iPosA - 1) ..
									"|r|cFFA0A0FF" .. strsub(sLine, iPosA, iPosB) .. "|r" ..
									HIGHLIGHT_FONT_COLOR_CODE .. strsub(sLine, iPosB + 1);
							else
								sLine = sLine .. "\n" .. QUEST_DASH .. "|r|cFFFFFF00no " .. sSub .. " (wanted)|r";
							end
						end
					end
				end

				for k, v in pairs(oMissing) do
					local	sMissing = _G[sPrefix .. k];
					if (sMissing) then
						sLine = sLine .. "\n" .. QUEST_DASH .. "|r|cFFFF5050no " .. sMissing .. "(MUST)|r";
					end
				end
			end
		elseif (strfind(sLine, CLASS_ROLES, 1, true) == 1) then
			-- TODO.
		end

		if (sLine ~= oText:GetText()) then
			if (LookingForGuildBrowseSortListLocal.Debug) then
				LookingForGuildBrowseSortListLocal.Tip[i] = { A = oText:GetText(), B = sLine };
			end
			oText:SetText(sLine);
			bShow = true;
		else
			if (LookingForGuildBrowseSortListLocal.Debug) then
				LookingForGuildBrowseSortListLocal.Tip[i] = sLine;
			end
		end
	end

	local	name, rank, members, achieved, infotext, cached, requestPending = GetRecruitingGuildInfo(self.index);
	if (infotext ~= "") then
		-- Message("Infotext: " .. infotext);
		GameTooltip:AddLine("Information\n"..HIGHLIGHT_FONT_COLOR_CODE..infotext, nil, nil, nil, true);
		bShow = true;
	end

	if ((GUILDSCORE ~= nil) and (oMUST ~= nil)) then
		local	iMembers = GUILDMEMBERCOUNTS[self.index];
		local	iRank = GUILDRANKS[self.index];
		if ((oMUST.SIZE ~= nil) and (oMUST.SIZE > iMembers)) then
			GameTooltip:AddLine("Minimum size not reached\n" .. QUEST_DASH .. "|r|cFFFF5050" .. iMembers .. " < " .. oMUST.SIZE .. "|r", nil, nil, nil, true);
			bShow = true;
		end
		if ((oMUST.RANK ~= nil) and (oMUST.RANK > iRank)) then
			GameTooltip:AddLine("Minimum rank not reached\n" .. QUEST_DASH .. "|r|cFFFF5050" .. iRank .. " < " .. oMUST.SIZE .. "|r", nil, nil, nil, true);
			bShow = true;
		end
	end

	local	sName = GameTooltipTextLeft1:GetText();
	local	iMax, i = #LookingForGuildBrowseSortListLocal.Tracker;
	for i = 1, iMax do
		if (LookingForGuildBrowseSortListLocal.Tracker[i].Name == sName) then
			local	oGuild = LookingForGuildBrowseSortListLocal.Tracker[i];
			local	sSub = QUEST_DASH .. "no data yet";
			if (oGuild.SeenMax) then
				sSub = QUEST_DASH .. "max. online: " .. oGuild.SeenMax .. "/" .. members;
				if (oGuild.SeenAt) then
					sSub = sSub .. date(" @ %Y-%m-%d %H:%M", oGuild.SeenAt);
				end

				if (oGuild.SeenStatLog) then
					local	oSizes = LFGBSPrivate.SizeStat.Sizes;
					local	iSlotHigh, iSlot = LFGBSPrivate.SizeStat.NumToSlot(oGuild.SeenMax);
					local	iSum = 0;
					local	iGot = 0;
					for iSlot = iSlotHigh, 1, -1 do
						local	iCount = oGuild.SeenStatLog[iSlot];
						if (iCount and iCount > 0) then
							iSum = iSum + iCount;
							sSub = sSub .. "\n" .. QUEST_DASH .. oSizes[iSlot] .. "+: " .. format("%.1f", 100.0 * iSum / oGuild.SeenCount) .. "% of the time";
							iGot = iGot + 1;
							if (iGot >= 5) then
								break
							end
						end
					end
				end
			end
			GameTooltip:AddLine("Guild is being tracked\n"  .. HIGHLIGHT_FONT_COLOR_CODE .. sSub .. "|r", nil, nil, nil, true);
			bShow = true;
			break;
		end
	end

	if (bShow) then
		GameTooltip:Show();
	end
end

local	function time2str(time)
	local	m = time - math.floor(time);
	local	h = time - m;
	return format("%02d:%02d", h, m * 60);
end

function	Cmd(sCmd, bSilent)
	-- Split commands
	-- %w+: alphanumeric chars, does not include e.g. accented letters
	-- => only sufficient for command, but not for guild names
	-- %S+: all but space chars
	local	args, w = {};
	for w in string.gmatch(sCmd, "%S+") do
		tinsert(args, w);
	end

	local	iCnt = #args;
	if (iCnt > 0) then
		if (args[1] == "help") then
			if ((args[2] == "quick") or (args[2] == "short")) then
				Message("LookingForGuildBrowseSort: Valid commands are help (sic!), status, toggle, size, findname, findcomment, sort, days, size.");
			elseif (args[2] == "info") then
				Message("[LFGBS] The server checks your selections in the settings tab and lists you all the guilds which offer at least one of your choices. But the list isn't ordered in any way, shape or form, nor are better matching guilds which do *exactly* what you want listed before dead guilds which happen to *also* pvp. [LFGBS] is secretly sneaking behind the scenes and reorders the guild list, so better matches are coming before worse. It also allows a more fine-grained matching by marking options as mandatory (MUST/MUSTN'T) vs. optional (default). It allows string search in names and comments, jumping from match to match. And it allows to sort by name or size, should you really feel the urge to.");
			else
				Message("LookingForGuildBrowseSort: LFGBS brings additional sort/search/filter functionality to the guild browser.");
				Message("[LFBGS] 'help' accepts as option 'quick' or 'short' for just a list of the valid commands, and 'info' for why");
				Message("[LFBGS] 'status' shows your current MUST/MUSTN'T information (in your comment) in a readable form");
				Message("[LFBGS] 'toggle' allows you to modify your MUST/MUSTN'T information");
				Message("[LFBGS] 'days' allows you to modify the list of days you plan to play with a guild");
				Message("[LFBGS] 'times' allows you to modify the time window you plan to play with a guild");
				Message("[LFBGS] 'size' allows you to define a minimum acceptable guild size for top listing.");
				Message("[LFBGS] 'rank' allows you to define a minimum acceptable guild rank for top listing.");
				Message("[LFBGS] 'findname' and 'findcomment' accept a search string and jump to the next guild name/comment containing that string");
				Message("[LFBGS] 'sort' allows you to choose a different way of sorting");
				Message("[LFBGS] 'tracktoggle[full]' toggles tracking status of the currently selected guild (full generates more data)");
				Message("[LFBGS] 'tracklist' lists all currently tracked guilds and their max. actually seen size");
				Message("[LFBGS] 'trackreset [<guild name>]' resets the data tracked from the given guild or all guilds if none given");
				Message("[LFBGS] 'hideredgrey' toggles if the list of guilds (sorted by score) hides red/grey guilds (default: show all)");
			end

			return
		end
		if (args[1] == "status") then
			if (oMUST) then
				local	sSub, k, v, tt = "";
				for k, v in pairs(oMUST) do
					tt = type(v);
					if (tt == "boolean") then
						sSub = sSub .. ", " .. k;
					elseif (tt == "number") then
						if (k == "DAYS") then
							sSub = sSub .. ", [" .. DaysNum2Str(v) .. "]";
						else
							sSub = sSub .. ", " .. k .. ": " .. v;
						end
					elseif (tt == "table") then
						if (k == "TIME") then
							sSub = sSub .. ", " .. k .. ": " .. time2str(v.FROM) .. " => " .. time2str(v.TO);
						end
					end
				end

				Message("LookingForGuildBrowseSort: Status = { " .. strsub(sSub, 2) .. " }");
			else
				Message("LookingForGuildBrowseSort: Didn't find a valid MUST|N'T marker in your comment (yet).");
			end

			return
		end

		if (args[1] == "sort") then
			if (iCnt == 1) then
				Message("LookingForGuildBrowseSort: Missing option to sort. Valid options are { NAME, SIZE, RANK, SCORE (default) }");
				return
			end

			if (args[2] == "NAME" or args[2] == "SIZE" or args[2] == "SCORE" or args[3] == "RANK") then
				MapReset();
				GUILDSORT = args[2];
				LookingForGuild_Update();
			end

			Message("LookingForGuildBrowseSort: Sort routine used now is <" .. GUILDSORT .. ">");
			return
		end

		if ((args[1] == "findname") or (args[1] == "findcomment")) then
			if (iCnt == 1) then
				args[2] = "";
			end
			local	x, i = args[2];
			for i = 3, iCnt do
				x = x .. " " .. args[i];
			end

			if (strlen(x) < 3) then
				Message("LookingForGuildBrowseSort: Pattern to find* must be 3+ characters long.");
				return
			end
			x = strlower(x);

			local	oData, sIs;
			if (args[1] == "findname") then
				oData = GUILDNAMES;
				sIs = "name";
			else
				oData = GUILDCOMMENTS;
				sIs = "comment";
			end

			local	iGuildCnt = #oData;
			local	oFound = {};
			for i = 1, iGuildCnt do
				if (strfind(strlower(oData[i]), x, 1, true)) then
					tinsert(oFound, i);
				end
			end

			if (#oFound == 0) then
				Message("LookingForGuildBrowseSort: No guild " .. sIs .. " with that pattern found.");
				return
			end

			local	offset, iPos = HybridScrollFrame_GetOffset(LookingForGuildBrowseFrameContainer) + 1;
			if (offset > #oData - 4) then
				-- get away from the last 4 slots...
				offset = 0;
			end

			for i = 1, #oFound do
				if (offset < oFound[i]) then
					iPos = oFound[i];
					break;
				end
			end

			if (iPos) then
				if (#oFound > 1) then
					Message("LookingForGuildBrowseSort: " .. #oFound .. " guild " .. sIs .. "s with that pattern found. Scrolling to next...");
				end
			else
				iPos = oFound[1];
				if (#oFound > 1) then
					Message("LookingForGuildBrowseSort: " .. #oFound .. " guild " .. sIs .. "s with that pattern found. Scrolling to first...");
				end
			end

			-- Message("LookingForGuildBrowseSort: Trying to scroll to guild named <" .. GUILDNAMES[iPos] .. "> (" .. iPos .. ")");

			LookingForGuildBrowseFrameContainerScrollBar:SetValue((iPos - 1) * 84);
			HybridScrollFrame_SetOffset(LookingForGuildBrowseFrameContainer, (iPos - 1) * 84);

			return
		end

		if (args[1] == "toggle") then
			if (iCnt == 1) then
				local	sOptions, k, v = "";
				for k, v in pairs(CHOICES_SHORTLONG) do
					sOptions = sOptions .. ", " .. v;
				end

				Message("LookingForGuildBrowseSort: Missing option to toggle. Valid options are { " .. strsub(sOptions, 2) .. " }");
				return
			end

			local	choice, k, v;
			for k, v in pairs(CHOICES_SHORTLONG) do
				if (v == args[2]) then
					choice = k;
				end
			end

			if (not choice) then
				if (not bSilent) then
					Message("LookingForGuildBrowseSort: Unknown choice to toggle.");
				end

				return
			end

			if ((type(oMUST) == "table") and (next(oMUST) ~= nil)) then
				local	bValue = oMUST[args[2]];
				if (bValue) then
					oMUST[args[2]] = nil;
					if (next(oMUST) == nil) then
						oMUST = nil;
					end
				else
					oMUST[args[2]] = true;
				end
			else
				oMUST = {};
				oMUST[args[2]] = true;
			end

			LFGBSPrivate.CommentPlayer.Update();
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			return
		end

		if (args[1] == "days") then
			if (not args[2] or strlen(args[2]) ~= 7) then
				Message("LookingForGuildBrowseSort: This command must be followed by a 7-letter argument, one for each day either upper-case for yes or lower-case for no. (E.g.: mtwtFSs for only on Fridays/Saturdays)");
				return
			end

			local	sDays = args[2];
			local	sDaysUpper = "MTWTFSS";
			local	sDaysLower = "mtwtfss";

			local	iDays, iMulti, i = 0, 1;
			for i = 1, 7 do
				local	c = strsub(sDays, i, i);
				if (c == strsub(sDaysUpper, i, i)) then
					iDays = iDays + iMulti;
				elseif (c ~= strsub(sDaysLower, i, i)) then
					Message("LookingForGuildBrowseSort: Unknown letter <" .. c .. "> for day " .. i .. ". Correct sequence is [" .. sDaysUpper .. "]");
					return
				end

				iMulti = iMulti * 2;
			end

			if (iDays == 0) then
				iDays = nil;
			end

			if ((type(oMUST) == "table") and (next(oMUST) ~= nil)) then
				oMUST.DAYS = iDays;
				if (next(oMUST) == nil) then
					oMUST = nil;
				end
			else
				oMUST = { DAYS = iDays };
			end

			local	bChanged = LFGBSPrivate.CommentPlayer.Update();
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			if (bChanged) then
				if (iDays) then
					Message("LookingForGuildBrowseSort: Updated comment to include days [" .. DaysNum2Str(iDays) .. "]");
				else
					Message("LookingForGuildBrowseSort: Updated comment to not include days any longer.");
				end
			else
				Message("LookingForGuildBrowseSort: Days remain unchanged [" .. DaysNum2Str(iDays) .. "]");
			end

			return
		end

		if (args[1] == "times") then
			local	oTIMES;
			if ((args[2] == "--") and not args[3]) then
				args[2] = "--";
				args[3] = "--";
				args[4] = "";
				args[5] = "";
			else
				if (not args[3] or args[4]) then
					Message("LookingForGuildBrowseSort: This command must be followed by two time arguments in the format HH:MM, MM (minutes) may only be 00 or 30  (e.g. \"/LFGBS times 19:00 23:30\"), or by one argument to reset: --");
					return
				end

				oTIMES = {};
				local	ok, j = true;
				for j = 2, 3 do
					local	s = args[j];
					if (strlen(s) < 5) then
						ok = false;
						break;
					end

					-- 20:30 => 2, 20, 20 * 60, 20 * 60 * 10 + 3, 20 * 60 * 10 * 10 + 30
					local	v = 0;
					local	i, c;
					for i = 1, 5 do
						c = strsub(s, i, i);
						if (i < 3) then
							if (c < "0" or c > "9") then
								ok = false;
								break;
							else
								v = v * 10 + strbyte(c) - strbyte("0");
							end
						elseif (i == 3) then
							if (c ~= ":") then
								ok = false;
								break;
							end
						elseif (i == 4) then
							if (c ~= "0" and c ~= "3") then
								ok = false;
								break;
							elseif (c == "3") then
								v = v + 0.5;
							end
						elseif (c ~= "0") then
							ok = false;
							break;
						end
					end

					if (j == 2) then
						oTIMES.FROM = v;
					else
						oTIMES.TO = v;
					end
				end

				if (not ok) then
					Message("LookingForGuildBrowseSort: This command must be followed by two time arguments in the format HH:MM, MM (minutes) may only be 00 or 30  (e.g. \"/LFGBS times 19:00 23:30\").");
					return
				end
			end

			local	sOld = "--:-- -> --:--";
			if ((type(oMUST) == "table") and (next(oMUST) ~= nil)) then
				if (type(oMUST.TIME) == "table") then
					Message("[LFGBS][DEBUG] TIME was set.");
					sOld = format("%02d:%02d -> %02d:%02d",
						floor(oMUST.TIME.FROM), 60 * (oMUST.TIME.FROM - floor(oMUST.TIME.FROM)),
						floor(oMUST.TIME.TO  ), 60 * (oMUST.TIME.TO   - floor(oMUST.TIME.TO  )));
				end

				oMUST.TIME = oTIMES;
				if (next(oMUST) == nil) then
					oMUST = nil;
				end
			else
				oMUST = { TIME = oTIMES };
			end

			local	bChanged = LFGBSPrivate.CommentPlayer.Update();
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			if (bChanged) then
				if (oTIMES) then
					Message("LookingForGuildBrowseSort: Updated comment to include times " .. args[2] .. " - " .. args[3] .. " (from " .. (sOld or "<??>") .. ").");
				else
					Message("LookingForGuildBrowseSort: Updated comment to not include times any longer (times were " .. (sOld or "<??>") .. ").");
				end
			else
				Message("LookingForGuildBrowseSort: Times remain unchanged: " .. args[2] .. " - " .. args[3] .. ".");
			end

			return
		end

		if (args[1] == "size") then
			local	iSize = tonumber(args[2] or "0");
			if (((iSize == 0) and (args[2] ~= "0")) or (iSize > 0) and (iSize < 5) or (iSize > 55)) then
				Message("LookingForGuildBrowseSort: Size argument must be 0 (zero, to clear/reset) or between 5 and 55 (odd, inclusive).");
				return
			end

			if (bit.band(iSize, 1) == 0) then
				iSize = iSize + 1;
			end

			if (iSize < 5) then
				iSize = nil;
			end

			if ((type(oMUST) == "table") and (next(oMUST) ~= nil)) then
				oMUST.SIZE = iSize;
				if (next(oMUST) == nil) then
					oMUST = nil;
				end
			else
				oMUST = { SIZE = iSize };
			end

			local	bChanged = LFGBSPrivate.CommentPlayer.Update();
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			if (bChanged) then
				if (iSize) then
					Message("LookingForGuildBrowseSort: Updated comment to include min. size " .. iSize .. ".");
				else
					Message("LookingForGuildBrowseSort: Updated comment to not include a min. size any longer.");
				end
			else
				Message("LookingForGuildBrowseSort: Min. size remains unchanged: " .. iSize .. ".");
			end

			return
		end

		if (args[1] == "rank") then
			local	iRank = tonumber(args[2] or "0");
			if (((iRank == 0) and (args[2] ~= "0")) or (iRank < 1) or (iRank > 30)) then
				Message("LookingForGuildBrowseSort: Rank argument must be 0 (zero, to clear/reset) or between 1 and 30.");
				return
			end

			if (iRank < 0) then
				iRank = nil;
			end

			if ((type(oMUST) == "table") and (next(oMUST) ~= nil)) then
				oMUST.RANK = iRank;
				if (next(oMUST) == nil) then
					oMUST = nil;
				end
			else
				oMUST = { RANK = iRank };
			end

			local	bChanged = LFGBSPrivate.CommentPlayer.Update();
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			if (bChanged) then
				if (iRank) then
					Message("LookingForGuildBrowseSort: Updated comment to include min. guild rank " .. iRank .. ".");
				else
					Message("LookingForGuildBrowseSort: Updated comment to not include a min. guild rank any longer.");
				end
			else
				Message("LookingForGuildBrowseSort: Min. guild rank remains unchanged: " .. iRank .. ".");
			end

			return
		end

		if (strsub(args[1], 1, 11)  == "tracktoggle") then
			local	iSelected = GetRecruitingGuildSelection();
			if (not iSelected) then
				Message("LookingForGuildBrowseSort: No guild selected to toggle tracking status.");
				return
			end

			local	sName = GetRecruitingGuildInfo(iSelected);
			if (not sName) then
				Message("LookingForGuildBrowseSort: Failed to find name of selected guild. Hm!");
			else
				local	oTracker = LookingForGuildBrowseSortListLocal.Tracker;
				local	iMax, i, bFound, bData = #oTracker;
				for i = 1, iMax do
					if (oTracker[i].Name == sName) then
						Message("[LFGBS] Cmd::tt: " .. sName .. " => " .. i);
						bFound = true;
						if (args[1] == "tracktogglefull") then
							oTracker[i].SeenCount = 0;
							bData = oTracker[i].Data ~= nil;
							if (bData) then
								oTracker[i].Data = nil;
								local	k, v;
								for k, v in pairs(oTracker[i]) do
									if (strsub(k, 1, 7) == "SEENMAX") then
										oTracker[i].k = nil;
									end
								end
							else
								oTracker[i].Data = {};
							end
						else
							tremove(oTracker, i);
						end

						break
					end
				end

				if (not bFound) then
					if (args[1] == "tracktogglefull") then
						bData = false;
						oTracker[iMax + 1] = { Name = sName, SeenCount = 0, SeenStatLog = {}, Data = {} };
					else
						oTracker[iMax + 1] = { Name = sName, SeenCount = 0, SeenStatLog = {} };
					end
				end

				-- not found: bData ~= nil ? full : seen, found: bData == nil ? off : bData && seen || full
				local	sStatus = "<error>";
				if (bFound) then
					if (bData ~= nil) then
						sStatus = bData and "seen" or "full";
					else
						sStatus = "off";
					end
				else
					if (bData ~= nil) then
						sStatus = "full";
					else
						sStatus = "seen";
					end
				end

				Message("LookingForGuildBrowseSort: Tracking status for guild <" .. sName .. "> toggled to " .. sStatus .. ".");
			end

			return
		end

		if (args[1] == "tracklist") then
			local	sSub = "";
			local	oTracker = LookingForGuildBrowseSortListLocal.Tracker;
			if (oTracker) then
				local	iMax, i = #oTracker;
				for i = 1, iMax do
					local	sSubSub = oTracker[i].SeenMax and (": " .. oTracker[i].SeenMax) or "";
					if (oTracker[i].Data) then
						sSubSub = sSubSub .. "!";
					end
					sSub = sSub .. " [" .. oTracker[i].Name .. sSubSub .. "]";
				end
			end
			if (sSub == "") then
				sSub = " none.";
			end

			Message("LookingForGuildBrowseSort: Tracked guilds are " .. strsub(sSub, 2));
			return
		end

		if (args[1] == "trackreset") then
			local	sName;
			if (args[2]) then
				sName = args[2];
				local	iSub = 3;
				while (args[iSub]) do
					sName = sName .. " " .. args[iSub];
					iSub = iSub + 1;
				end
			end

			local	oTracker = LookingForGuildBrowseSortListLocal.Tracker;
			local	iMax, i = #oTracker;
			for i = 1, iMax do
				if ((sName == nil) or (oTracker[i].Name == sName)) then
					oTracker[i].SeenCount = 0;
					oTracker[i].SeenStat = nil;
					oTracker[i].SeenStatLog = nil;
					if (type(oTracker[i].Data) == "table") then
						oTracker[i].Data = {};
					end

					local	k, v;
					for k, v in pairs(oTracker[i]) do
						if (strsub(k, 1, 7) == "SEENMAX") then
							oTracker[i].k = nil;
						end
					end
				end
			end

			Message("LookingForGuildBrowseSort: Tracking statistics haben been re-initialized.");
			return
		end

		if (args[1] == "hideredgrey") then
			if (not LookingForGuildBrowseSortListLocal.Config) then
				LookingForGuildBrowseSortListLocal.Config = {};
			end
			LookingForGuildBrowseSortListLocal.Config.HideRedGrey = not LookingForGuildBrowseSortListLocal.Config.HideRedGrey;
			if (LookingForGuildFrameTab2:IsShown()) then
				LookingForGuild_Update();
			end

			Message("LookingForGuildBrowseSort: Hiding red/grey guilds (in the score-list) is now "
							.. (LookingForGuildBrowseSortListLocal.Config.HideRedGrey and "on." or "off."));

			return
		end
	end

	Message("LookingForGuildBrowseSort: Missing or unknown command. Try '/LFGBS help'");
end

local	function	LookingForGuildBrowseSortButton_OnClick(self)
	MapReset();

	local	oNext = { SCORE = "NAME", NAME = "SIZE", SIZE = "RANK", RANK = "SCORE" };
	GUILDSORT = oNext[GUILDSORT or "SCORE"];
	self:SetText("by " .. GUILDSORT);

	LookingForGuild_Update();
end

local	bGuildnameColorize_Hooked;
local	bGetRecruitingGuildSettings_Hooked;
local	bLookingForGuildFooButton_Hooked;
local	bSortButton_Added;
local	bSlashCommand_Added;
local	bLookingForGuild_Update_Hooked;
local	function	GetRecruitingGuildSettings_Overwrite()
	if (not bGetRecruitingGuildSettings_Hooked) then
		bGetRecruitingGuildSettings_Hooked = true;

		-- hooksecurefunc("RequestRecruitingGuildsList", MapReset);
		-- hooksecurefunc("SetLookingForGuildComment", MapReset);
		LookingForGuildCommentEditBox:HookScript("OnEditFocusLost", LFGBSPrivate.CommentPlayer.Parse);
		MapReset();
	end

	--
	--

	if (GetNumRecruitingGuilds_original == nil) then
		GetNumRecruitingGuilds_original = GetNumRecruitingGuilds;
		GetNumRecruitingGuilds = GetNumRecruitingGuilds_ordered;
	end

	if (GetRecruitingGuildSettings_original == nil) then
		GetRecruitingGuildSettings_original = GetRecruitingGuildSettings;
		GetRecruitingGuildSettings = GetRecruitingGuildSettings_ordered;
	end
	if (GetRecruitingGuildInfo_original == nil) then
		GetRecruitingGuildInfo_original = GetRecruitingGuildInfo;
		GetRecruitingGuildInfo = GetRecruitingGuildInfo_ordered;
	end

	if (not bLookingForGuildGuild_ShowTooltip_Hooked) then
		bLookingForGuildGuild_ShowTooltip_Hooked = true;
		local	i = 1;
		while (_G["LookingForGuildBrowseFrameContainerButton" .. i] ~= nil) do
			local	oButton = _G["LookingForGuildBrowseFrameContainerButton" .. i];
			oButton:HookScript("OnEnter", LookingForGuildGuild_ShowTooltip_Hook);
			i = i + 1;
		end
	end

	if (not bGuildnameColorize_Hooked) then
		bGuildnameColorize_Hooked = true;
		hooksecurefunc("LookingForGuild_Update", GuildnameColorize);
		hooksecurefunc(LookingForGuildBrowseFrameContainer, "update", GuildnameColorize);
	end

	--
	--

	if (not bLookingForGuildFooButton_Hooked) then
		bLookingForGuildFooButton_Hooked = true;
		local	k, v;
		for k, v in pairs(CHOICES_SHORTLONG) do
			local	sCamel = strupper(strsub(v, 1, 1)) .. strlower(strsub(v, 2));
			local	iLen = strlen(sCamel);
			if (strsub(sCamel, iLen, iLen) == "p") then
				sCamel = strsub(sCamel, 1, iLen - 1) .. "P";
			end

			local	oButton = _G["LookingForGuild" .. sCamel .. "Button"];
			if (oButton) then
				oButton:HookScript("OnClick", LFGBSPrivate.CommentPlayer.ButtonToggle);
				oButton:RegisterForClicks("LeftButtonUp", "RightButtonUp");
			end
		end
	end

	if (not bSortButton_Added) then
		bSortButton_Added = true;
		local	oBtn = CreateFrame("Button", "LookingForGuildBrowseSortButton", LookingForGuildRequestButton:GetParent(), "MagicButtonTemplate");
		oBtn:SetPoint("TOPLEFT", LookingForGuildRequestButton, "TOPRIGHT", 3, 0);
		oBtn:SetText("by SCORE");
		oBtn:SetScript("OnClick", LookingForGuildBrowseSortButton_OnClick);
		oBtn:Show();
	end

	if (not bSlashCommand_Added) then
		bSlashCommand_Added = true;
		SLASH_LFGBS1 = "/LFGBS";
		SlashCmdList["LFGBS"] = Cmd;
		Message("LookingForGuildBrowseSort initialized. Try '/LFGBS help' for a list of commands.");
	end

	if (not bLookingForGuild_Update_Hooked) then
		bLookingForGuild_Update_Hooked = true;
		hooksecurefunc("LookingForGuild_Update", LFGBSPrivate.LookingForGuild_Update2ActualMemberCount);
		hooksecurefunc(LookingForGuildBrowseFrameContainer, "update", LFGBSPrivate.LookingForGuild_Update2ActualMemberCount);
	end

	LFGBSPrivate.CommentPlayer.Parse();
end

hooksecurefunc("LookingForGuildFrame_LoadUI", GetRecruitingGuildSettings_Overwrite);

--
--
--

local	iFrequency = 1200;

function	LFGBSFrequencySet(iValue)
	if ((type(iValue) == "number") and (iValue > 60) and (iValue <= 1200)) then
		iFrequency = iValue;
		Message("[LFGBS] Frequency set to " .. iValue .. ".");
	else
		Message("[LFGBS] Frequency NOT set to [" .. tostring(iValue) .. "] - invalid input.");
	end
end

function	LFGBSPrivate.TrackerFrame.OnUpdate(oFrame, iElapsed)
	oFrame.At = oFrame.At - iElapsed;
	if (oFrame.At < 0) then
		if (#LookingForGuildBrowseSortListLocal.Tracker > 0) then
			if (FriendsFrame:IsVisible()) then
				oFrame.At = oFrame.At + 30;
			else
				if (LookingForGuildBrowseSortListLocal.Config and LookingForGuildBrowseSortListLocal.Config.TIME) then
					local	iSecondsNow = time();
					if (oFrame.iDayBreak == nil) then
						local	iSecondsSinceDayBreak = tonumber(date("%H", iSecondsNow)) * 3600 + tonumber(date("%M", iSecondsNow)) * 60 + tonumber(date("%S", iSecondsNow));
						oFrame.iDayBreak = math.floor((iSecondsNow - iSecondsSinceDayBreak) / 60 + 0.5) * 60;
					end

					local	iHoursNow = 1.0 * (iSecondsNow - oFrame.iDayBreak) / 3600;

					-- times are in hours since midnight, fractional
					local	oTIME = LookingForGuildBrowseSortListLocal.Config.TIME;
					local	tFrom = oTIME.FROM;
					local	tTo = oTIME.TO;
					if (tTo < tFrom) then
						-- 20:30 - 0:30 => 20.5 - 0.5
						if ((tTo < iHoursNow) and (tFrom > iHoursNow)) then
							oFrame.At = iFrequency / 2;
							-- Message("[LFGBS] Current time outside preferred raid time (" .. format("%.1f - %.1f", tFrom, tTo) .. ") - no tracking.");
							return
						end
					else
						-- 20:30 - 23:30 => 20.5 - 23.5
						if ((tTo < iHoursNow) or (tFrom > iHoursNow)) then
							oFrame.At = iFrequency / 2;
							-- Message("[LFGBS] Current time outside preferred raid time (" .. format("%.1f - %.1f", tFrom, tTo) .. ") - no tracking.");
							return
						end
					end
				end

				oFrame.At = oFrame.At + iFrequency / (0.5 + math.min(#LookingForGuildBrowseSortListLocal.Tracker, 5));

				oFrame.Pos = oFrame.Pos + 1;
				local	i = oFrame.Pos;
				if (i > #LookingForGuildBrowseSortListLocal.Tracker) then
					oFrame.Pos = 1;
					i = 1;
				end

				local	oGuild = LookingForGuildBrowseSortListLocal.Tracker[i];
				oGuild.SeenCount = (oGuild.SeenCount or 0) + 1;
				local	sWho = "g-\"" .. oGuild.Name .. "\"";
				oFrame.sWho = sWho;

				-- order seems to be illogical at first, but compensates for other addons potentially re-registering the event on the SendWho, if *they* took it away
				SendWho(sWho, "LookingForGuildBrowseSort");
				FriendsFrame:UnregisterEvent("WHO_LIST_UPDATE");
				SetWhoToUI(1);

				-- Message("[LFGBS] TF::OU: Sent [" .. sWho .. "]");
			end
		else
			oFrame.At = iFrequency / 4;
		end
	end
end

function	LFGBSPrivate.TrackerFrame.OnEvent(oFrame, sEvent, arg1, ...)
	if (sEvent == "LF_GUILD_BROWSE_UPDATED") then
		MapReset("OnEvent:" .. sEvent);
	elseif (sEvent == "WHO_LIST_UPDATE") then
		FriendsFrame:RegisterEvent("WHO_LIST_UPDATE");
		if (FriendsFrame:IsVisible()) then
			SetWhoToUI(1);
		else
			SetWhoToUI(0);
		end

		local	_, iMax = GetNumWhoResults();
		if (iMax == 0) then
			return
		end

		local	bWrong = oFrame.sWho == nil;
		local	sGuild = not bWrong and strsub(oFrame.sWho, 4, -2);
		local	sGuildOops = "";
		local	oDataBin, oData = {};
		local	iNow = time();
		local	oSpots, i, j = {};
		for i = 1, iMax do
			local	sName, guild, level, race, class, zone, classEN = GetWhoInfo(i);
			if (guild ~= sGuild) then
				sGuildOops = sGuildOops .. ", [" .. tostring(sName) .. "] = <" .. tostring(guild) .. ">";
				bWrong = true;
			end

			if (guild ~= "") then
				local	jMax, j = #LookingForGuildBrowseSortListLocal.Tracker;
				for j = 1, jMax do
					local	oGuild = LookingForGuildBrowseSortListLocal.Tracker[j];
					if (guild == oGuild.Name) then
						if (oSpots[j] == nil) then
							oSpots[j] = {};
						end

						local	oData;
						if (oGuild.Data) then
							oData = oGuild.Data;
						else
							oDataBin[j] = oDataBin[j] or {};
							oData = oDataBin[j];
						end

						local	kMax, k, bFound = #oData;
						for k = 1, kMax do
							if (oData[k].Name == sName) then
								oData[k].Hit = (oData[k].Hit or 1) + 1;
								tinsert(oSpots[j], k);
								bFound = true;
							end
						end

						if (not bFound) then
							tinsert(oData, { Name = sName, Class = classEN, Level = level, Time = iNow, Hit = 1 } );
							tinsert(oSpots[j], #oData);
						end
					end
				end
			end
		end

		if (next(oSpots)) then
			local	sOut, k, v = "";
			for k, v in pairs(oSpots) do
				local	oGuild, oCollisions = LookingForGuildBrowseSortListLocal.Tracker[k];
				if (oGuild.Data) then
					oCollisions = {};

					local	iMax = #v;
					if (iMax > 1) then
						local	i, iGroup, iBit, iMask;
						for i = 1, iMax do
							iBit = v[i] % 32;
							iGroup = v[i] - iBit;
							iMask = bit.lshift(1, iBit);
							oCollisions[iGroup] = bit.bor(oCollisions[iGroup] or 0, iMask);
						end

						for i = 1, iMax do
							local	oEntry, l, w = oGuild.Data[v[i]];
							for l, w in pairs(oCollisions) do
								local	sKey = "COLL" .. l;
								oEntry[sKey] = bit.bor(oEntry[sKey] or 0, w);
							end
						end
					end
				end

				if (not bWrong) then
					sOut = sOut .. ", [" .. oGuild.Name .. ": " .. #v .. "]";
					LFGBSPrivate.SizeStat.SeenMaxUpdate(oGuild, oCollisions, iMax);
				end
			end

			if (bWrong) then
				-- Message("[LFGBS] TF::OE(WLU) - " .. strsub(sOut, 3) .. " == {" .. tostring(sGuildOops) .. "}");
			else
				Message("[LFGBS] TF::OE(WLU) + " .. strsub(sOut, 3));
			end
		end

		oFrame.sWho = nil;
	elseif (sEvent == "ADDON_LOADED") then
		if (arg1 == "LookingForGuildBrowseSort") then
			if (LookingForGuildBrowseSortList == nil) then
				LookingForGuildBrowseSortList = {};
			end

			local	sServer;
			if (GetRealmName ~= nil) then
				-- 5.4: realmname is no longer a global variable
				sServer = GetRealmName();
			else
				sServer = GetCVar("RealmName");
			end
			if (LookingForGuildBrowseSortList[sServer] == nil) then
				LookingForGuildBrowseSortList[sServer] = { Config = {} };
			end

			LookingForGuildBrowseSortListLocal = LookingForGuildBrowseSortList[sServer];
			if (LookingForGuildBrowseSortListLocal.Config == nil) then
				if (LookingForGuildBrowseSortList.Config) then
					LookingForGuildBrowseSortListLocal.Config = LookingForGuildBrowseSortList.Config;
					LookingForGuildBrowseSortList.Config = nil;
				else
					LookingForGuildBrowseSortListLocal.Config = {};
				end
			end
			if (LookingForGuildBrowseSortListLocal.Tracker == nil) then
				if (LookingForGuildBrowseSortList.Tracker) then
					LookingForGuildBrowseSortListLocal.Tracker = LookingForGuildBrowseSortList.Tracker;
					LookingForGuildBrowseSortList.Tracker = nil;
				else
					LookingForGuildBrowseSortListLocal.Tracker = {};
				end
			end

			-- legacy cleanup
			local	k, v;
			for k, v in pairs(LookingForGuildBrowseSortList) do
				if ((type(v) == "table") and (type(v.Tracker) == "table")) then
					local	iMax, i = #v.Tracker;
					for i = 1, iMax do
						if (v.Tracker[i].Data == nil) then
							local	l, w;
							for l, w in pairs(v.Tracker[i]) do
								if (strsub(l, 1, 7) == "SEENMAX") then
									v.Tracker[i].l = nil;
								end
							end
						end

						local	oSeenStat = v.Tracker[i].SeenStat;
						if (type(oSeenStat) == "table") then
							if (type(v.Tracker[i].SeenStatLog) ~= "table") then
								v.Tracker[i].SeenStatLog = {};
							end

							local	oSeenStatLog = v.Tracker[i].SeenStatLog;
							local	l, w;
							for l, w in pairs(oSeenStat) do
								if ((type(l) == "number") and (l > 0)) then
									local	k = LFGBSPrivate.SizeStat.NumToSlot(l);
									oSeenStatLog[k] = (oSeenStatLog[k] or 0) + w;
								end
							end

							v.Tracker[i].SeenStat = nil;
						end
					end
				end
			end

			oFrame.At = 60;
			oFrame.Pos = time() % (1 + #LookingForGuildBrowseSortListLocal.Tracker);

			oFrame:UnregisterEvent("ADDON_LOADED");
			oFrame:RegisterEvent("WHO_LIST_UPDATE");
			oFrame:RegisterEvent("LF_GUILD_BROWSE_UPDATED");
			oFrame:SetScript("OnUpdate", LFGBSPrivate.TrackerFrame.OnUpdate);

			if (#LookingForGuildBrowseSortListLocal.Tracker > 0) then
				Message("LookingForGuildBrowseSort: Tracker initialized with " .. #LookingForGuildBrowseSortListLocal.Tracker .. " guilds to look for.");
			end
		end
	end
end

function	LFGBSPrivate.LookingForGuild_Update2ActualMemberCount()
	if (#LookingForGuildBrowseSortListLocal.Tracker == 0) then
		return
	end

	local	oName2Max = {};
	local	iMax, i = #LookingForGuildBrowseSortListLocal.Tracker;
	for i = 1, iMax do
		local	oGuild = LookingForGuildBrowseSortListLocal.Tracker[i];
		oName2Max[oGuild.Name] = LFGBSPrivate.SizeStat.AverageMax(oGuild);
	end

	local	sFormat = gsub(BROWSE_GUILDS_NUM_MEMBERS, "%%d", "%%s");

	local	oButtons = LookingForGuildBrowseFrameContainer.buttons;
	local	iMax, i = #oButtons;
	for i = 1, iMax do
		local	oButton = oButtons[i];
		if (oButton:IsShown()) then
			local name, _, numMembers = GetRecruitingGuildInfo(oButton.index);
			if (oName2Max[name]) then
				oButton.numMembers:SetFormattedText(sFormat, oName2Max[name] .. " (" .. numMembers .. ")");
			end
		end
	end
end

function	LFGBSPrivate.TrackerFrame.SendWhoHook(sWho)
	local	oFrame = LFGBSPrivate.TrackerFrame.oFrame;
	if (not sWho or (sWho ~= oFrame.sWho)) then
		oFrame.At = math.max(oFrame.At, 7);
	end
end

function	LFGBSPrivate.SizeStat.NumToSlot(iNum, iFirst, iLast)
	if (iNum == nil) then
		Message("[LFGBS] Oops in P:SS:NTS(nil).");
	end

	local	oSizes = LFGBSPrivate.SizeStat.Sizes;
	local	iMax, i = #oSizes;
	iFirst = math.max(iFirst or 1, 1);
	iLast = math.min(iLast or iMax, iMax);
	if (iLast - iFirst < 5) then
		local	iContainer;
		for i = iFirst, iLast do
			if (oSizes[i] > iNum) then
				iContainer = i - 1;
				break;
			end
		end
		if (not iContainer or not oSizes[iContainer]) then
			Message("[LFGBS] Oops in P:SS:NTS(" .. iNum .. ") =?> " .. (iContainer or "<nil>"));
		end
		if ((oSizes[iContainer] > iNum) or ((oSizes[iContainer + 1] ~= nil) and (oSizes[iContainer + 1] <= iNum))) then
			Message("[LFGBS] Oops in P:SS:NTS(" .. iNum .. ") =!> " .. iContainer);
		end

		return iContainer;
	else
		iMiddle = math.floor(iFirst + (iLast - iFirst) / 3);
		if (oSizes[iMiddle] == nil) then
			Message("[LFGBS] Oops in P:SS:NTS(" .. iNum .. ") =^ { " .. iMiddle .. " }");
		end

		if (oSizes[iMiddle] > iNum) then
			return LFGBSPrivate.SizeStat.NumToSlot(iNum, iFirst, iMiddle);
		else
			return LFGBSPrivate.SizeStat.NumToSlot(iNum, iMiddle, iLast);
		end
	end
end

function	LFGBSPrivate.SizeStat.AverageMax(oGuild)
	local	fAverage, fPercentage;
	if ((oGuild.SeenStat or oGuild.SeenStatLog) and oGuild.SeenMax and oGuild.SeenMax > 0) then
		local	iMax = oGuild.SeenMax;
		if (oGuild.SeenStatLog) then
			local	iContainer, i = LFGBSPrivate.SizeStat.NumToSlot(iMax);
			local	iCountTotal, iSumWeighted = 0, 0;
			local	iFirst = math.max(1, iContainer - 3);
			for i = iFirst, iContainer do
				local	iCount = oGuild.SeenStatLog[i];
				if (iCount) then
					iCountTotal = iCountTotal + iCount;
					iSumWeighted = iSumWeighted + LFGBSPrivate.SizeStat.Sizes[i] * iCount;
				end
			end

			if (iCountTotal > 0) then
				fAverage = 1.0 * iSumWeighted / iCountTotal;
				fPercentage = 100.0 * iCountTotal / oGuild.SeenCount;
			end
		else
			local	oMax = {};
			oMax[iMax    ] = (oGuild.SeenStat[iMax    ] or 0);
			oMax[iMax - 1] = (oGuild.SeenStat[iMax - 1] or 0);
			oMax[iMax - 2] = (oGuild.SeenStat[iMax - 2] or 0);

			local	iCount = oMax[iMax] + oMax[iMax - 1] + oMax[iMax - 2];
			if (iCount > 0) then
				local	iSum = iMax * oMax[iMax] + (iMax - 1) * oMax[iMax - 1] + (iMax - 2) * oMax[iMax - 2];
				fAverage = 1.0 * iSum / iCount;
				fPercentage = 100.0 * iCount / oGuild.SeenCount;
			end
		end
	end

	if (fAverage and fPercentage) then
		return format("%.1f <%d%%>", fAverage, math.floor(fPercentage + 0.5));
	else
		return oGuild.SeenMax;
	end
end

function	LFGBSPrivate.SizeStat.SeenMaxUpdate(oGuild, oCollisions, iMax)
	local	iMaxOld = oGuild.SeenMax or 0;

	if (not oGuild.SeenStat and not oGuild.SeenStatLog) then
		oGuild.SeenStatLog = {};
		if (iMaxOld > 0) then
			local	iContainer = LFGBSPrivate.SizeStat.NumToSlot(iMaxOld);
			oGuild.SeenStatLog[iContainer] = 1;
			if ((oGuild.SeenCount or 0) < 2) then
				oGuild.SeenCount = 2;	-- the current /who and the /who who got us the old max.
			end
		end
	end

	if (iMax >= iMaxOld) then
		oGuild.SeenAt = time();
		oGuild.SeenMax = iMax;

		if (oGuild.Data and oCollisions) then
			local	l, w;
			for l, w in pairs(oCollisions) do
				local	sKey = "SEENMAX" .. l;
				oGuild[sKey] = w;
			end
		end
	end

	if (oGuild.SeenStatLog) then
		local	iContainer = LFGBSPrivate.SizeStat.NumToSlot(iMax);
		oGuild.SeenStatLog[iContainer] = (oGuild.SeenStatLog[iContainer] or 0) + 1;
	else
		if (iMax > iMaxOld) then
			local	oSeenStatOld = oGuild.SeenStat;
			oGuild.SeenStat = { [ iMax ] = oSeenStatOld[iMax] or 0 };
			if (iMax > 1) then
				oGuild.SeenStat[iMax - 1] = oSeenStatOld[iMax - 1] or 0;
				if (iMax > 2) then
					oGuild.SeenStat[iMax - 2] = oSeenStatOld[iMax - 2] or 0;
				end
			end
		elseif (oGuild.SeenMax - iMax < 3) then
			oGuild.SeenStat[iMax] = (oGuild.SeenStat[iMax] or 0) + 1;
		end
	end
end

do
	local	oFrame = CreateFrame("Frame", nil, UIParent);
	oFrame:SetScript("OnEvent", LFGBSPrivate.TrackerFrame.OnEvent);
	oFrame:RegisterEvent("ADDON_LOADED");
	hooksecurefunc("SendWho", LFGBSPrivate.TrackerFrame.SendWhoHook);

	LFGBSPrivate.TrackerFrame.oFrame = oFrame;

	LFGBSPrivate.SizeStat.Sizes = { [0] = 0 };
	local	oSizes, i, j = LFGBSPrivate.SizeStat.Sizes;
	for i = 1, 10 do
		for j = 1, 5 do
			oSizes[(i - 1) * 5 + j] = oSizes[(i - 1) * 5 + j - 1 ] + i;
		end
	end
end