--------------------------------------------------------------------------------
--                        A L L   T H E   T H I N G S                         --
--------------------------------------------------------------------------------
--               Copyright 2017 Dylan Fortune (Crieve-Sargeras)               --
--------------------------------------------------------------------------------
AllTheThings = CreateFrame("FRAME", "AllTheThings", UIParent);
if not AllTheThingsAD then AllTheThingsAD = { }; end					-- For account-wide data.
if not AllTheThingsPCD then AllTheThingsPCD = { }; end					-- For character specific data.
if not AllTheThingsTempData then AllTheThingsTempData = { }; end		-- For temporary data.
local app, events = AllTheThings, {};	-- Create a local (non global) reference
app.DisplayName = "AllTheThings";
app.Events = events;
app:SetBackdrop({bgFile = "Interface/Tooltips/UI-Tooltip-Background", 
				 edgeFile = "Interface/Tooltips/UI-Tooltip-Border", 
				 tile = true, tileSize = 16, edgeSize = 16, 
				 insets = { left = 4, right = 4, top = 4, bottom = 4 }});
app:SetScript("OnMouseDown", app.StartMoving);
app:SetScript("OnMouseUp", app.StopMovingOrSizing);
app:SetScript("OnEvent", function (_, e, ...) (events[e] or tostringall)(...); end);
app:SetBackdropColor(0, 0, 0, 255);
app:SetClampedToScreen(true);
app:SetMovable(true);
app:SetResizable(false);
app:SetUserPlaced(true);
app:EnableMouse(true);
app:SetPoint("CENTER");
app:SetSize(500, 604);
app:Hide();

-- ReloadUI slash command (for ease of use)
SLASH_RELOADUI1 = "/reloadui";
SLASH_RELOADUI2 = "/rl";
SlashCmdList["RELOADUI"] = ReloadUI;

-- Slash Command List
SLASH_AllTheThings1 = "/allthethings";
SLASH_AllTheThings2 = "/things";
SLASH_AllTheThings3 = "/att";
SlashCmdList["AllTheThings"] = function(...)
	-- Parse the Slash Command
	local message = tostring(...);
	if message == "show" then
		-- Show the Window
		app:Show();
		return;
	elseif message == "hide" then
		-- Hide the Window
		app:Hide();
		return;
	elseif message == "options" then
		-- Show the options window. (Calling this twice actually makes it work correctly)
		InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
		InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
		return;
	end
	if app:IsVisible() then app:Hide(); else app:Show(); end
end;

-- Localization Lib
app.Locales = {};
app.print = function(msg, ...)
	DEFAULT_CHAT_FRAME:AddMessage(app.DisplayName .. ": " .. (msg or "nil"), ...);
end
function app:SetLocale(loc)
	loc = loc or self.Locale or "enUS";
	if self.Locale ~= loc then
		-- Load in the table
		if self.Locales then
			self.Locale = loc;
			self.LL = self.Locales[loc] or self.Locales.enUS;
		end
	end
end
function app:L(name, ...)
	return name and self.LL[name];
end

-- Data Lib
function app.SetDataMember(self, member, data)
	AllTheThingsAD[member] = data;
end
function app.GetDataMember(self, member, default)
	if AllTheThingsAD[member] == nil then AllTheThingsAD[member] = default; end
	return AllTheThingsAD[member];
end

function app.SetTempDataMember(self, member, data)
	AllTheThingsTempData[member] = data;
end
function app.GetTempDataMember(self, member, default)
	if AllTheThingsTempData[member] == nil then AllTheThingsTempData[member] = default; end
	return AllTheThingsTempData[member];
end

function app.SetPersonalDataMember(self, member, data)
	AllTheThingsPCD[member] = data;
end
function app.GetPersonalDataMember(self, member, default)
	if AllTheThingsPCD[member] == nil then AllTheThingsPCD[member] = default; end
	return AllTheThingsPCD[member];
end

function app.SetDataSubMember(self, member, submember, data)
	if AllTheThingsAD[member] then AllTheThingsAD[member][submember] = data; end
end
function app.GetDataSubMember(self, member, submember, default)
	if not AllTheThingsAD[member] then AllTheThingsAD[member] = { }; end
	if AllTheThingsAD[member][submember] == nil then AllTheThingsAD[member][submember] = default; end
	return AllTheThingsAD[member][submember];
end

function app.SetTempDataSubMember(self, member, submember, data)
	if AllTheThingsTempData[member] then
		AllTheThingsTempData[member][submember] = data;
	end
end
function app.GetTempDataSubMember(self, member, submember, default)
	if AllTheThingsTempData[member] == nil then
		AllTheThingsTempData[member] = { };
	end
	if AllTheThingsTempData[member][submember] == nil then
		AllTheThingsTempData[member][submember] = default;
	end
	return AllTheThingsTempData[member][submember];
end

function app.SetPersonalDataSubMember(self, member, submember, data)
	if AllTheThingsPCD[member] then AllTheThingsPCD[member][submember] = data; end
end
function app.GetPersonalDataSubMember(self, member, submember, default)
	if not AllTheThingsPCD[member] then AllTheThingsPCD[member] = { }; end
	if AllTheThingsPCD[member][submember] == nil then AllTheThingsPCD[member][submember] = default; end
	return AllTheThingsPCD[member][submember];
end

function app.SetDataSubSubMember(self, member, submember, subsubmember, data)
	if AllTheThingsAD[member] and AllTheThingsAD[member][submember] then AllTheThingsAD[member][submember][subsubmember] = data; end
end
function app.GetDataSubSubMember(self, member, submember, subsubmember, default)
	if not AllTheThingsAD[member] then
		AllTheThingsAD[member] = 
		{
			[submember] = 
			{
				[subsubmember] = default;
			};
		};
		return default;
	else
		if not AllTheThingsAD[member][submember] then
			AllTheThingsAD[member][submember] = 
			{
				[subsubmember] = default;
			};
			return default;
		else
			if AllTheThingsAD[member][submember][subsubmember] == nil then
				AllTheThingsAD[member][submember][subsubmember] = default;
				return default;
			end
		end
	end
	return AllTheThingsAD[member][submember][subsubmember];
end

function app.SetPersonalDataSubSubMember(self, member, submember, subsubmember, data)
	if AllTheThingsPCD[member] and AllTheThingsPCD[member][submember] then AllTheThingsPCD[member][submember][subsubmember] = data; end
end
function app.GetPersonalDataSubSubMember(self, member, submember, subsubmember, default)
	if not AllTheThingsPCD[member] then
		AllTheThingsPCD[member] = 
		{
			[submember] = 
			{
				[subsubmember] = default;
			};
		};
		return default;
	else
		if not AllTheThingsPCD[member][submember] then
			AllTheThingsPCD[member][submember] = 
			{
				[subsubmember] = default;
			};
			return default;
		else
			if AllTheThingsPCD[member][submember][subsubmember] == nil then
				AllTheThingsPCD[member][submember][subsubmember] = default;
				return default;
			end
		end
	end
	return AllTheThingsPCD[member][submember][subsubmember];
end

function app.SetTempDataSubSubMember(self, member, submember, subsubmember, data)
	if AllTheThingsTempData[member] and AllTheThingsTempData[member][submember] then AllTheThingsTempData[member][submember][subsubmember] = data; end
end
function app.GetTempDataSubSubMember(self, member, submember, subsubmember, default)
	if not AllTheThingsTempData[member] then
		AllTheThingsTempData[member] = 
		{
			[submember] = 
			{
				[subsubmember] = default;
			};
		};
		return default;
	else
		if not AllTheThingsTempData[member][submember] then
			AllTheThingsTempData[member][submember] = 
			{
				[subsubmember] = default;
			};
			return default;
		else
			if AllTheThingsTempData[member][submember][subsubmember] == nil then
				AllTheThingsTempData[member][submember][subsubmember] = default;
				return default;
			end
		end
	end
	return AllTheThingsTempData[member][submember][subsubmember];
end

-- User Interface Lib
function app:CreateMinimapButton()
	-- Create the Button for the Minimap frame. Create a local and non-local copy.
	local button = CreateFrame("BUTTON", app:GetName() .. "-Minimap", Minimap);
	button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight", "ADD");
	button:SetFrameStrata("HIGH");
	button:SetMovable(true);
	button:EnableMouse(true);
	button:SetSize(33, 33);
	button:SetPoint("CENTER", 0, 0, "CENTER", 0, 0);
	button:RegisterForDrag("LeftButton", "RightButton");
	button:RegisterForClicks("LeftButtonUp", "RightButtonUp");
	
	-- Create the Button Texture
	local texture = button:CreateTexture(nil, "BACKGROUND");
	texture:SetTexture("Interface\\Icons\\INV_Axe_106.blp");
	texture:SetSize(21, 21);
	texture:SetTexCoord(0,1,0,1);
	texture:SetPoint("TOPLEFT", 7, -6);
	
	-- Create the Button Tracking Border
	local border = button:CreateTexture(nil, "BORDER");
	border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder");
	border:SetSize(56, 56);
	border:SetPoint("TOPLEFT");
	
	-- Button Configuration
	button.update = function(self)
		local position = app:GetDataMember("Position", -19.9380328877568);
		self:SetPoint("CENTER", "Minimap", "CENTER", -78 * cos(position), 78 * sin(position));
	end
	local update = function(self)
		local w, x = GetCursorPosition();
		local y, z = Minimap:GetLeft(), Minimap:GetBottom();
		local s = UIParent:GetScale();
		w = y - w / s + 70; x = x / s - z - 70;
		app:SetDataMember("Position", math.deg(math.atan2(x, w)));
		self:Raise();
		self:update();
	end

	-- Register for Frame Events
	button:SetScript("OnMouseDown", function(self)
		self.FlagDown = true;
	end);
	button:SetScript("OnMouseUp", function(self)
		self.FlagDown = nil;
	end);
	button:SetScript("OnDragStart", function(self)
		self:SetScript("OnUpdate", update);
		self.FlagDragging = true;
	end);
	button:SetScript("OnDragStop", function(self)
		self:SetScript("OnUpdate", nil);
		self.FlagDragging = nil;
	end);
	button:SetScript("OnClick", function(self)
		app.print(self:GetName() .. " has not been given a proper OnClick script. Contact the technical designer to have this evaluated.");
	end);
	button:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_LEFT");
		GameTooltip:SetText(app.DisplayName);
		GameTooltip:AddLine(app:L("MINIMAP_MOUSEOVER_TEXT"), 1, 1, 1);
		GameTooltip:Show();
	end);
	button:SetScript("OnLeave", function(self)
		GameTooltip:Hide();
	end);
	button:SetScript("OnClick", function(self, button)
		if button == "RightButton" then
			-- Right Button opens the Options menu.  (Calling this twice actually makes it work correctly)
			InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
			InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
		else
			-- Left Button Toggles the App
			if app:IsVisible() then app:Hide(); else app:Show(); end
		end
	end);
	button:update();
	button:Show();
	return button;
end
function app:CreateSettingsMenu()
	local name = app:GetName();
	local frame = CreateFrame("FRAME", name .. "-Settings", UIParent);
	frame.name = app:L("TITLE_OPTIONS");
	InterfaceOptions_AddCategory(frame);
	
	frame.Title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge");
    frame.Title:SetPoint("TOPLEFT", 16, -16)
    frame.Title:SetText(app:L("TITLE_OPTIONS"));
	
	frame.Separator = frame:CreateTexture(nil, "ARTWORK");
	frame.Separator:SetPoint("LEFT", frame.Title, "BOTTOMLEFT", 0, -2);
	frame.Separator:SetColorTexture(0.2, 0.2, 0.2);
	frame.Separator:SetSize(590, 1);
	
	-- Preferences
	frame.PreferencesTitle = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal");
    frame.PreferencesTitle:SetPoint("TOPLEFT", frame.Separator, "BOTTOMLEFT", 10, -10);
    frame.PreferencesTitle:SetText(app:L("PREFERENCES"));
	
	frame.ShowMinimapButton = CreateFrame("CheckButton", name .. "-ShowMinimapButton", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.ShowMinimapButton:SetPoint("TOPLEFT", frame.PreferencesTitle, "BOTTOMLEFT", 10, -4);
	frame.ShowMinimapButton.Label = _G[frame.ShowMinimapButton:GetName() .. "Text"];
	frame.ShowMinimapButton.Label:SetText("Show Minimap Button");
	frame.ShowMinimapButton.Label:SetWidth(200);
	frame.ShowMinimapButton:SetChecked(app:GetDataMember("ShowMinimapButton"));
	frame.ShowMinimapButton:SetScript("OnClick", function(self)
		local checked = self:GetChecked();
		app:SetDataMember("ShowMinimapButton", checked);
		if (checked == true) then
			if not app.Minimap then app.Minimap = app:CreateMinimapButton(); end
			app.Minimap:Show();
		elseif app.Minimap then
			app.Minimap:Hide();
		end
	end);
	
	frame.ShowCompletedGroups = CreateFrame("CheckButton", name .. "-ShowCompletedGroups", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.ShowCompletedGroups:SetPoint("LEFT", frame.PreferencesTitle, "LEFT", 10, 0);
	frame.ShowCompletedGroups:SetPoint("TOP", frame.ShowMinimapButton, "BOTTOM", 0, 6);
	frame.ShowCompletedGroups.Label = _G[frame.ShowCompletedGroups:GetName() .. "Text"];
	frame.ShowCompletedGroups.Label:SetText("Show Completed Groups");
	frame.ShowCompletedGroups.Label:SetWidth(200);
	frame.ShowCompletedGroups:SetChecked(app:GetDataMember("ShowCompletedGroups"));
	frame.ShowCompletedGroups:SetScript("OnClick", function(self)
		app:SetDataMember("ShowCompletedGroups", self:GetChecked());
		if self:GetChecked() then
			app.GroupVisibilityFilter = app.NoFilter;
		else
			app.GroupVisibilityFilter = app.FilterGroupsByCompletion;
		end
		app:RefreshScrollbar();
	end);
	
	frame.ShowCollectedItems = CreateFrame("CheckButton", name .. "-ShowCollectedItems", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.ShowCollectedItems:SetPoint("TOPLEFT", frame.ShowCompletedGroups, "BOTTOMLEFT", 0, 6);
	frame.ShowCollectedItems.Label = _G[frame.ShowCollectedItems:GetName() .. "Text"];
	frame.ShowCollectedItems.Label:SetText("Show Collected Items");
	frame.ShowCollectedItems.Label:SetWidth(200);
	frame.ShowCollectedItems:SetChecked(app:GetDataMember("ShowCollectedItems"));
	frame.ShowCollectedItems:SetScript("OnClick", function(self)
		app:SetDataMember("ShowCollectedItems", self:GetChecked());
		if self:GetChecked() then
			app.CollectedItemVisibilityFilter = app.NoFilter;
		else
			app.CollectedItemVisibilityFilter = app.Filter;
		end
		app:RefreshScrollbar();
	end);
	
	frame.ShowMissingItems = CreateFrame("CheckButton", name .. "-ShowMissingItems", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.ShowMissingItems:SetPoint("TOPLEFT", frame.ShowCollectedItems, "BOTTOMLEFT", 0, 6);
	frame.ShowMissingItems.Label = _G[frame.ShowMissingItems:GetName() .. "Text"];
	frame.ShowMissingItems.Label:SetText("Show Missing Items");
	frame.ShowMissingItems.Label:SetWidth(200);
	frame.ShowMissingItems:SetChecked(app:GetDataMember("ShowMissingItems"));
	frame.ShowMissingItems:SetScript("OnClick", function(self)
		app:SetDataMember("ShowMissingItems", self:GetChecked());
		if self:GetChecked() then
			app.MissingItemVisibilityFilter = app.NoFilter;
		else
			app.MissingItemVisibilityFilter = app.Filter;
		end
		app:RefreshScrollbar();
	end);
	
	frame.UniqueAppearances = CreateFrame("CheckButton", name .. "-UniqueAppearances", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.UniqueAppearances:SetPoint("TOPLEFT", frame.ShowMissingItems, "BOTTOMLEFT", 10, 2);
	frame.UniqueAppearances.Label = _G[frame.UniqueAppearances:GetName() .. "Text"];
	frame.UniqueAppearances.Label:SetText("Filter by Appearance ID rather than by Source ID");
	frame.UniqueAppearances.Label:SetWidth(200);
	frame.UniqueAppearances:SetChecked(app:GetDataMember("UniqueAppearances"));
	frame.UniqueAppearances:SetScript("OnClick", function(self)
		app:SetDataMember("UniqueAppearances", self:GetChecked());
		app:SetDataMember("Collected", {});	-- This option causes a caching issue, so we have to purge the collected data cache.
		if self:GetChecked() then
			app.ItemSourceFilter = app.FilterItemSourceUnique;
		else
			app.ItemSourceFilter = app.FilterItemSource;
		end
		app:RefreshScrollbar(false, true);
	end);
	
	frame.PlayFanfare = CreateFrame("CheckButton", name .. "-PlayFanfare", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.PlayFanfare:SetPoint("LEFT", frame.ShowMinimapButton.Label, "RIGHT", 50, 6);
	frame.PlayFanfare.Label = _G[frame.PlayFanfare:GetName() .. "Text"];
	frame.PlayFanfare.Label:SetText("Play Fanfare");
	frame.PlayFanfare.Label:SetWidth(200);
	frame.PlayFanfare:SetChecked(app:GetDataMember("PlayFanfare"));
	frame.PlayFanfare:SetScript("OnClick", function(self)
		app:SetDataMember("PlayFanfare", self:GetChecked());
	end);
	
	frame.PlayRemoveSound = CreateFrame("CheckButton", name .. "-PlayRemoveSound", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.PlayRemoveSound:SetPoint("TOPLEFT", frame.PlayFanfare, "BOTTOMLEFT", 0, 6);
	frame.PlayRemoveSound.Label = _G[frame.PlayRemoveSound:GetName() .. "Text"];
	frame.PlayRemoveSound.Label:SetText("Play Remove Sound");
	frame.PlayRemoveSound.Label:SetWidth(200);
	frame.PlayRemoveSound:SetChecked(app:GetDataMember("PlayRemoveSound"));
	frame.PlayRemoveSound:SetScript("OnClick", function(self)
		app:SetDataMember("PlayRemoveSound", self:GetChecked());
	end);
	
	frame.AutoRefreshSaves = CreateFrame("CheckButton", name .. "-AutoRefreshSaves", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.AutoRefreshSaves:SetPoint("TOPLEFT", frame.PlayRemoveSound, "BOTTOMLEFT", 0, 6);
	frame.AutoRefreshSaves.Label = _G[frame.AutoRefreshSaves:GetName() .. "Text"];
	frame.AutoRefreshSaves.Label:SetText("Auto Refresh Saves");
	frame.AutoRefreshSaves.Label:SetWidth(200);
	frame.AutoRefreshSaves:SetChecked(app:GetDataMember("AutoRefreshSaves"));
	frame.AutoRefreshSaves:SetScript("OnClick", function(self)
		app:SetDataMember("AutoRefreshSaves", self:GetChecked());
		if self:GetChecked() then
			app:RegisterEvent("BOSS_KILL");
		else
			app:UnregisterEvent("BOSS_KILL");
		end
	end);
	
	frame.FilterGroupsByLevel = CreateFrame("CheckButton", name .. "-FilterGroupsByLevel", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.FilterGroupsByLevel:SetPoint("TOPLEFT", frame.AutoRefreshSaves, "BOTTOMLEFT", 0, 6);
	frame.FilterGroupsByLevel.Label = _G[frame.FilterGroupsByLevel:GetName() .. "Text"];
	frame.FilterGroupsByLevel.Label:SetText("Filter Groups By Level");
	frame.FilterGroupsByLevel.Label:SetWidth(200);
	frame.FilterGroupsByLevel:SetChecked(app:GetDataMember("FilterGroupsByLevel"));
	frame.FilterGroupsByLevel:SetScript("OnClick", function(self)
		app:SetDataMember("FilterGroupsByLevel", self:GetChecked());
		if self:GetChecked() then
			app:RegisterEvent("PLAYER_LEVEL_UP");
			app.GroupRequirementsFilter = app.FilterGroupsByLevel;
		else
			app:UnregisterEvent("PLAYER_LEVEL_UP");
			app.GroupRequirementsFilter = app.NoFilter;
		end
		app:RefreshScrollbar();
	end);

	frame.FilterItemsByClass = CreateFrame("CheckButton", name .. "-FilterItemsByClass", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.FilterItemsByClass:SetPoint("TOPLEFT", frame.FilterGroupsByLevel, "BOTTOMLEFT", 0, 6);
	frame.FilterItemsByClass.Label = _G[frame.FilterItemsByClass:GetName() .. "Text"];
	frame.FilterItemsByClass.Label:SetText("Filter Items By Class");
	frame.FilterItemsByClass.Label:SetWidth(200);
	frame.FilterItemsByClass:SetChecked(app:GetDataMember("FilterItemsByClass"));
	frame.FilterItemsByClass:SetScript("OnClick", function(self)
		app:SetDataMember("FilterItemsByClass", self:GetChecked());
		if self:GetChecked() then
			app.ClassRequirementFilter = app.FilterItemClass_RequireClasses;
		else
			app.ClassRequirementFilter = app.NoFilter;
		end
		app:RefreshScrollbar();
	end);

	frame.FilterItemsByRace = CreateFrame("CheckButton", name .. "-FilterItemsByRace", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.FilterItemsByRace:SetPoint("TOPLEFT", frame.FilterItemsByClass, "BOTTOMLEFT", 0, 6);
	frame.FilterItemsByRace.Label = _G[frame.FilterItemsByRace:GetName() .. "Text"];
	frame.FilterItemsByRace.Label:SetText("Filter Items By Race");
	frame.FilterItemsByRace.Label:SetWidth(200);
	frame.FilterItemsByRace:SetChecked(app:GetDataMember("FilterItemsByRace"));
	frame.FilterItemsByRace:SetScript("OnClick", function(self)
		app:SetDataMember("FilterItemsByRace", self:GetChecked());
		if self:GetChecked() then
			app.RaceRequirementFilter = app.FilterItemClass_RequireRaces;
		else
			app.RaceRequirementFilter = app.NoFilter;
		end
		app:RefreshScrollbar();
	end);

	
	
	
	-- Item Class Filters
	local itemClassFilters = app:GetPersonalDataMember("ItemClassFilters");
	frame.FiltersTitle = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal");
    frame.FiltersTitle:SetPoint("LEFT", frame.PreferencesTitle, "LEFT", 0, 0);
    frame.FiltersTitle:SetPoint("TOP", frame.UniqueAppearances, "BOTTOM", 0, -10);
    frame.FiltersTitle:SetText(app:L("FILTERS"));
	
	local lastFilter = frame.FiltersTitle;
	for itemClassID, subClassIDs in pairs({
		[4] = { 0, 1, 2, 3, 4, 5, 6 },
		[15] = { 0, 1, 2, 3, 4, 5 },
		[0] = { 8 },
	}) do
		local itemClassFilter = CreateFrame("CheckButton", name .. "-" .. itemClassID, frame, "InterfaceOptionsCheckButtonTemplate");
		itemClassFilter:SetPoint("LEFT", frame.ShowMissingItems, "LEFT", 0, 0);
		itemClassFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 0);
		itemClassFilter.itemClassID = itemClassID;
		itemClassFilter.Label = _G[itemClassFilter:GetName() .. "Text"];
		itemClassFilter.Label:SetText(GetItemClassInfo(itemClassID) .. " (" .. itemClassID .. ")");
		itemClassFilter.Label:SetWidth(200);
		itemClassFilter.groups = {};
		itemClassFilter.filters = itemClassFilters[itemClassID];
		if not itemClassFilter.filters or type(itemClassFilter.filters) ~= "table" then
			itemClassFilter.filters = {};
			itemClassFilters[itemClassID] = itemClassFilter.filters;
		end
		itemClassFilter:SetScript("OnClick", function(self)
			for i, f in ipairs(self.groups) do
				local checked = not self.filters[f.subClassID];
				self.filters[f.subClassID] = checked;
				f:SetChecked(checked);
			end
			self:SetChecked(false);
			app:RefreshScrollbar();
		end);
		lastFilter = itemClassFilter;
		for i, subClassID in ipairs(subClassIDs) do
			local filter = CreateFrame("CheckButton", itemClassFilter:GetName() .. "-" .. subClassID, frame, "InterfaceOptionsCheckButtonTemplate");
			filter:SetPoint("LEFT", frame.ShowMissingItems, "LEFT", 10, 0);
			filter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
			filter.itemClassID = itemClassID;
			filter.subClassID = subClassID;
			filter.Label = _G[filter:GetName() .. "Text"];
			filter.Label:SetText(GetItemSubClassInfo(itemClassID, subClassID) .. " (" .. subClassID .. ")");
			filter.Label:SetWidth(200);
			lastFilter = filter;
			tinsert(itemClassFilter.groups, filter);
			filter:SetChecked(itemClassFilter.filters[subClassID]);
			filter:SetScript("OnClick", function(self)
				itemClassFilters[self.itemClassID][self.subClassID] = self:GetChecked();
				app:RefreshScrollbar();
			end);
		end
	end
	
	-- Weapons
	local weaponItemClassID = 2;
	local weaponFilters = CreateFrame("CheckButton", name .. "-" .. weaponItemClassID, frame, "InterfaceOptionsCheckButtonTemplate");
	weaponFilters:SetPoint("LEFT", frame.ShowMissingItems, "LEFT", 200, 0);
	weaponFilters:SetPoint("TOP", frame.FiltersTitle, "BOTTOM", 0, 0);
	weaponFilters.itemClassID = weaponItemClassID;
	weaponFilters.Label = _G[weaponFilters:GetName() .. "Text"];
	weaponFilters.Label:SetText(GetItemClassInfo(weaponItemClassID) .. " (" .. weaponItemClassID .. ")");
	weaponFilters.Label:SetWidth(200);
	weaponFilters.groups = {};
	weaponFilters.filters = itemClassFilters[weaponItemClassID];
	if not weaponFilters.filters or type(weaponFilters.filters) ~= "table" then
		weaponFilters.filters = {};
		itemClassFilters[weaponItemClassID] = weaponFilters.filters;
	end
	weaponFilters:SetScript("OnClick", function(self)
		for i, f in ipairs(self.groups) do
			local checked = not self.filters[f.subClassID];
			self.filters[f.subClassID] = checked;
			f:SetChecked(checked);
		end
		self:SetChecked(false);
		app:RefreshScrollbar();
	end);
	
	lastFilter = weaponFilters;
	for i, subClassID in ipairs({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) do
		local filter = CreateFrame("CheckButton", weaponFilters:GetName() .. "-" .. subClassID, frame, "InterfaceOptionsCheckButtonTemplate");
		filter:SetPoint("LEFT", weaponFilters, "LEFT", 10, 0);
		filter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		filter.itemClassID = weaponItemClassID;
		filter.subClassID = subClassID;
		filter.Label = _G[filter:GetName() .. "Text"];
		filter.Label:SetText(GetItemSubClassInfo(weaponItemClassID, subClassID) .. " (" .. subClassID .. ")");
		filter.Label:SetWidth(200);
		lastFilter = filter;
		tinsert(weaponFilters.groups, filter);
		filter:SetChecked(weaponFilters.filters[subClassID]);
		filter:SetScript("OnClick", function(self)
			itemClassFilters[self.itemClassID][self.subClassID] = self:GetChecked();
			app:RefreshScrollbar();
		end);
	end
	
	lastFilter = weaponFilters;
	for i, subClassID in ipairs({ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }) do
		local filter = CreateFrame("CheckButton", weaponFilters:GetName() .. subClassID, frame, "InterfaceOptionsCheckButtonTemplate");
		filter:SetPoint("LEFT", weaponFilters, "LEFT", 210, 0);
		filter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		filter.itemClassID = weaponItemClassID;
		filter.subClassID = subClassID;
		filter.Label = _G[filter:GetName() .. "Text"];
		filter.Label:SetText(GetItemSubClassInfo(weaponItemClassID, subClassID) .. " (" .. subClassID .. ")");
		filter.Label:SetWidth(200);
		lastFilter = filter;
		tinsert(weaponFilters.groups, filter);
		filter:SetChecked(weaponFilters.filters[subClassID]);
		filter:SetScript("OnClick", function(self)
			itemClassFilters[self.itemClassID][self.subClassID] = self:GetChecked();
			app:RefreshScrollbar();
		end);
	end
end

-- audio lib
function app.PlayFanfare(self)
	if app:GetDataMember("PlayFanfare", true) then
		-- Play a random fanfare from the locale table
		local t = app:L("AUDIO_FANFARE_TABLE");
		if t and type(t) == "table" then
			local id = math.random(1, #t);
			if t[id] then PlaySoundFile(t[id]); end
		end
	end
end
function app.PlayRemoveSound(self)
	if app:GetDataMember("PlayRemoveSound", true) then
		-- Play a random fanfare from the locale table
		local t = app:L("AUDIO_REMOVE_TABLE");
		if t and type(t) == "table" then
			local id = math.random(1, #t);
			if t[id] then PlaySoundFile(t[id]); end
		end
	end
end

-- Color Lib
local CS = CreateFrame("ColorSelect", nil, app);
CS:Hide();
function ConvertColorRgbToHsv(r, g, b)
  CS:SetColorRGB(r, g, b);
  local h,s,v = CS:GetColorHSV()
  return {h=h,s=s,v=v}
end
local red = ConvertColorRgbToHsv(1,0,0);
local green = ConvertColorRgbToHsv(0,1,0);
function GetProgressColor(p)
	local h;
	if abs(red.h - green.h) > 180 then
	local angle = (360 - abs(red.h - green.h)) * p;
	if red.h < green.h then
		h = floor(red.h - angle);
		if h < 0 then h = 360 + h end
		else
			h = floor(red.h + angle);
			if h > 360 then h = h - 360 end
		end
	else
		h = floor(red.h-(red.h-green.h)*p)
	end
	CS:SetColorHSV(h, red.s-(red.s-green.s)*p, red.v-(red.v-green.v)*p);
	local r,g,b = CS:GetColorRGB();
	return app.RGBToHex(r * 255, g * 255, b * 255);
end
app.Colorize = function(str, color)
	return "|c" .. color .. str .. "|r";
end
app.RGBToHex = function(r, g, b)
	return string.format("ff%02x%02x%02x", 
		r <= 255 and r >= 0 and r or 0, 
		g <= 255 and g >= 0 and g or 0, 
		b <= 255 and b >= 0 and b or 0);
end

-- class lib
app.instanciate = function(baseItem,t)
	return setmetatable(t or {}, { __index = baseItem });
end

-- Tier Lib
app.TierDB = {
	[1] = { texture = [[Interface\Glues\Common\Glues-WoW-ClassicLogo]] },
	[2] = { texture = [[Interface\Glues\Common\Glues-WoW-BCLogo]] },
	[3] = { texture = [[Interface\Glues\Common\Glues-WoW-WotLKLogo]] },
	[4] = { texture = [[Interface\Glues\Common\Glues-WoW-CCLogo]] },
	[5] = { texture = [[Interface\Glues\Common\Glues-WoW-MPLogo]] },
	[6] = { texture = [[Interface\Glues\Common\GLUES-WOW-WODLOGO]] },
	[7] = { atlas = "Glues-WoW-LegionLogo" },
};
app.BaseTier = {
	__index = function(t, key)
		if key == "expanded" then
			-- TODO: Use a data setting.
			return false;
		elseif key == "text" then
			return EJ_GetTierInfo(t.tierID);
		else
			-- Something that isn't dynamic.
			return table[key];
		end
	end
};
app.CreateTier = function(id, t)
	if not t.groups then t = { ["groups"] = t }; end
	t.tierID = id;
	setmetatable(t, app.BaseTier);
	return t;
end

-- Instance Lib
app.InstanceDB = {};
app.BaseInstance = {
	__index = function(t, key)
		if key == "expanded" then
			-- TODO: Use a data setting.
			return false;
		elseif key == "text" then
			return select(1, EJ_GetInstanceInfo(t.instanceID)) or "";
		elseif key == "description" then
			return select(2, EJ_GetInstanceInfo(t.instanceID)) or "";
		elseif key == "icon" then
			return select(6, EJ_GetInstanceInfo(t.instanceID)) or "";
		elseif key == "link" then
			return select(8, EJ_GetInstanceInfo(t.instanceID)) or "";
		else
			-- Something that isn't dynamic.
			return table[key];
		end
	end
};
app.CreateInstance = function(id, t)
	if not t.groups then t = { ["groups"] = t }; end
	t.instanceID = id;
	app.InstanceDB[id] = setmetatable(t, app.BaseInstance);
end
app.CreateItem = function(id, t)
	local item = { ["itemID"] = id };
	if t then item.groups = t; end
	return item;
end

-- Difficulty Lib
app.BaseDifficulty = {
	__index = function(t, key)
		if key == "expanded" then
			-- TODO: Use a data setting.
			return false;
		elseif key == "text" then
			return GetDifficultyInfo(t.difficultyID);
		else
			-- Something that isn't dynamic.
			return table[key];
		end
	end
};
app.CreateDifficulty = function(id, t)
	if not t.groups then t = { ["groups"] = t }; end
	t.difficultyID = id;
	setmetatable(t, app.BaseDifficulty);
	return t;
end

-- Encounter Lib
app.EncounterDB = {};
app.BaseEncounter = {
	__index = function(t, key)
		if key == "expanded" then
			-- TODO: Use a data setting.
			return true;
		elseif key == "text" or key == "description" or key == "link" then
			local name, description, _, _, link = EJ_GetEncounterInfo(t.encounterID);
			if name then
				rawset(t, "text", name);
				rawset(t, "description", description or "");
				rawset(t, "link", link or "");
				return t[key];
			end
		else
			-- Something that isn't dynamic.
			return table[key];
		end
	end
};
app.CreateEncounter = function(id, t)
	if not t.groups then t = { ["groups"] = t }; end
	t.encounterID = id;
	app.EncounterDB[id] = t;
	setmetatable(t, app.BaseEncounter);
	return t;
end

-- Source ID Harvesting
app.DressUpModel = CreateFrame('DressUpModel');

-- table lib
table.contains = function(t, value)
	for i, entry in pairs(t) do
		if entry == value then return true; end
	end
end

-- Taken directly from CanIMogIt (Thanks!)
local inventorySlotsMap = {
    ["INVTYPE_HEAD"] = {1},
    ["INVTYPE_SHOULDER"] = {3},
    ["INVTYPE_BODY"] = {4},
    ["INVTYPE_CHEST"] = {5},
    ["INVTYPE_ROBE"] = {5},
    ["INVTYPE_WAIST"] = {6},
    ["INVTYPE_LEGS"] = {7},
    ["INVTYPE_FEET"] = {8},
    ["INVTYPE_WRIST"] = {9},
    ["INVTYPE_HAND"] = {10},
    ["INVTYPE_CLOAK"] = {15},
    ["INVTYPE_WEAPON"] = {16, 17},
    ["INVTYPE_SHIELD"] = {17},
    ["INVTYPE_2HWEAPON"] = {16, 17},
    ["INVTYPE_WEAPONMAINHAND"] = {16},
    ["INVTYPE_RANGED"] = {16},
    ["INVTYPE_RANGEDRIGHT"] = {16},
    ["INVTYPE_WEAPONOFFHAND"] = {17},
    ["INVTYPE_HOLDABLE"] = {17},
    ["INVTYPE_TABARD"] = false,
};

-- Title Element
local title = app:CreateFontString(nil, "ARTWORK", "GameFontHighlightMedium");
title:SetPoint("TOPLEFT", app, "TOPLEFT", 0, 0);
title:SetPoint("BOTTOMRIGHT", app, "TOPRIGHT", 0, -24);
title:SetJustifyH("MIDDLE");
title:SetText("???");
title:SetAlpha(0.5);
local c = app:CreateTexture(nil, "BACKGROUND");
c:SetColorTexture(0, 0, 0, 0.4);
c:SetPoint("TOPLEFT", title, "TOPLEFT", 3, -3);
c:SetPoint("BOTTOMRIGHT", title, "TOPRIGHT", -3, -22);
title.c = c;
app.Title = title;

-- Close and Options Button (on the main window)
local CloseButton = CreateFrame("Button", nil, app, "UIPanelCloseButton");
CloseButton:SetPoint("TOPRIGHT", app, "TOPRIGHT", 3, 3);
CloseButton:SetScript("OnClick", function() app:Hide(); end);

local OptionsButton = CreateFrame("Button", nil, app, "UIPanelInfoButton");
OptionsButton:SetPoint("RIGHT", c, "RIGHT", -22, 0);
OptionsButton:SetScript("OnClick", function()
	-- Show the options window. (Calling this twice actually makes it work correctly)
	InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
	InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
end);

-- Scrollbar
local scrollbar = CreateFrame("Slider", nil, app, "UIPanelScrollBarTemplate");
scrollbar:SetPoint("TOP", c, "BOTTOM", 0, -20);
scrollbar:SetPoint("BOTTOMRIGHT", app, "BOTTOMRIGHT", -4, 20);
scrollbar:SetMinMaxValues(1, 1);
scrollbar:SetValueStep(1);
scrollbar.CurrentValue = 1;
scrollbar:SetWidth(16);
scrollbar:SetScript("OnValueChanged", function(self, value)
	local un = math.floor(value);
    local up = un + 1;
	scrollbar.CurrentValue = (up - value) > (-(un - value)) and un or up;
	app:UpdateVisibleRowData();
end);
local c = scrollbar:CreateTexture(nil, "BACKGROUND");
c:SetColorTexture(0,0,0,0.4)
c:SetAllPoints(scrollbar);
scrollbar.c = c;
app.ScrollBar = scrollbar;
app:EnableMouseWheel(true);
app:SetScript("OnMouseWheel", function(self, delta)
	self.ScrollBar:SetValue(self.ScrollBar.CurrentValue - delta);
end);

-- Game Tooltip Icon
local GameTooltipIcon = CreateFrame("FRAME", nil, GameTooltip);
GameTooltipIcon:SetBackdrop({bgFile = "Interface/Tooltips/UI-Tooltip-Background", 
				 edgeFile = "Interface/Tooltips/UI-Tooltip-Border", 
				 tile = true, tileSize = 16, edgeSize = 16, 
				 insets = { left = 4, right = 4, top = 4, bottom = 4 }});
GameTooltipIcon:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
GameTooltipIcon:SetSize(96, 96);
GameTooltipIcon.icon = GameTooltipIcon:CreateTexture(nil, "ARTWORK");
GameTooltipIcon.icon:SetAllPoints(GameTooltipIcon);
GameTooltipIcon.icon:Show();
GameTooltipIcon:Hide();

-- Row Container
local container = CreateFrame("FRAME", nil, app);
container:SetPoint("TOPLEFT", title.c, "BOTTOMLEFT", 0, 0);
container:SetPoint("RIGHT", scrollbar, "LEFT", 0, 0);
container:SetPoint("BOTTOM", app, "BOTTOM", 0, 6);
container.rowHeight = select(2, GameFontNormal:GetFont()) + 4;
container.rowCount = math.ceil(container:GetHeight() / container.rowHeight);
container.stackCount = 0;
container.stack = {};
container.rows = {};
function container:OnUpdate()
	if container.stackCount > 0 then
		for i=container.stackCount,1,-1 do
			if not container.stack[i]() then
				table.remove(container.stack, i);
				container.stackCount = container.stackCount - 1;
				if container.stackCount < 1 then
					-- Unregister for OnUpdate (I don't want my addon eating up your CPU if it doesn't have to; even if a little bit.)
					container:SetScript("OnUpdate", nil);
				end
			end
		end
	end
end

local function ItemRefreshAll(self)
	-- Call to GetItemInfo to harvet the data about the item.
	local name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice, itemClassID, itemSubClassID = GetItemInfo(self.itemLink);
	if link then
		self.refresh = nil;
		self.itemLink = nil;
		self.link = link;
		self.Lvl = reqLevel;
		--self.iLvl = iLevel;
		self.class = itemClassID;
		self.subclass = itemSubClassID;
		--self.slot = equipSlot; -- Most likely will not need this information.
		self.texture = texture;
		
		-- Harvest the Source ID.
		if not self.sourceID then
			self.sourceID, dressable = app:GetSourceID(link);
			if not self.sourceID and dressable then return false; end
		end
		
		-- I guess harvesting data into all all powerful ItemDB file is harder than I thought.
		-- Send your AllTheThings.lua file to Crieve so he can parse it and add it to the ItemDB.
		--app:SetDataMember("ItemDB", app:GetTempDataMember("ItemDB"));
		return true;
	end
	return false;
end
local function ItemRefreshLink(self)
	local name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice, itemClassID, itemSubClassID = GetItemInfo(self.itemLink);
	if link then
		-- Harvest the Source ID.
		if not self.sourceID then
			self.sourceID, dressable = app:GetSourceID(link);
			if self.sourceID then
				-- I guess harvesting data into all all powerful ItemDB file is harder than I thought.
				-- Send your AllTheThings.lua file to Crieve so he can parse it and add it to the ItemDB.
				-- This is really the only property that matters. Everything else can be acquired afterwards.
				--app:SetDataMember("ItemDB", app:GetTempDataMember("ItemDB"));
			elseif dressable then
				return false;
			end
		end
		
		-- You might look at this and ask yourself, "What is Crieve thinking?"
		-- The answer is, "metatables". If the base item already has this information, 
		-- there's no reason to duplicate it in memory. Also, if you give me the saved data
		-- file, I can update my database to include more accurate information. :)
		if self.texture ~= texture then self.texture = texture; end
		if self.Lvl ~= reqLevel then self.Lvl = reqLevel; end
		--if self.iLvl ~= iLevel then self.iLvl = iLevel; end
		if self.class ~= itemClassID then self.class = itemClassID; end
		if self.subclass ~= itemSubClassID then self.subclass = itemSubClassID; end
		--if self.slot ~= equipSlot then self.slot = equipSlot; end -- Most likely will not need this information.
		
		-- Set the link (this part is always dynamic)
		self.link = link;
		self.refresh = nil;
		self.itemLink = nil;
		return true;
	end
	return false;
end
function app:Harvest(instanceID, difficultyID, itemID, itemLink)
	-- If the item is already cached, don't bother updating the cache.
	if not difficultyID then difficultyID = 0; end
	
	-- We use a temporary object to house the item link. By proxy, this can also be turned into a Saved Variable set and harvested later.
	local itemRef = app:GetTempDataSubSubMember("ItemDB", difficultyID, itemID);
	if not itemRef then
		-- Cache the base ItemDB table for this item / difficulty pair.
		local item = app.ItemDB[difficultyID] and app.ItemDB[difficultyID][itemID];
		if item then
			-- Create a local container to house useful information about the item.
			itemRef = item;--app.instanciate(baseItem);
			itemRef.refresh = ItemRefreshLink;
		else
			local baseItem = app.ItemDB[0][itemID];
			if baseItem then
				-- Create a local container to house useful information about the item.
				itemRef = app.instanciate(baseItem);
				itemRef.refresh = ItemRefreshLink;
			else
				-- Create a local container to house useful information about the item.
				itemRef = {};
				itemRef.refresh = ItemRefreshAll;
			end
		end
		
		-- Build the Item Link based on the instance and difficulty.
		-- NOTE: Someday, build the bonus ID value right into the instance difficulty object.
		if not itemLink then
			local bonusIDGroup = app:L("INSTANCE_BONUS_ID_PRESETS")[instanceID];
			if bonusIDGroup then
				local bonusIDs = bonusIDGroup[difficultyID];
				if bonusIDs then
					itemLink = string.format("item:%d:::::::::::%s", itemID, difficultyID);
					local count = #bonusIDs;
					itemLink = itemLink .. ":" .. count;
					for i=1,count do itemLink = itemLink .. ":" .. bonusIDs[i]; end
				else
					itemLink = string.format("item:%d:::::::::::%s", itemID, difficultyID) .. ":0:";
				end
			elseif difficultyID then
				itemLink = string.format("item:%d:::::::::::%s", itemID, difficultyID) .. ":0:";
			else
				itemLink = itemID;
			end
		end
		
		-- Assign the item link
		itemRef.itemID = itemID;
		itemRef.itemLink = itemLink;
		app:SetTempDataSubSubMember("ItemDB", difficultyID, itemID, itemRef);
	end
	return itemRef;
end
function app:HarvestItemLink(itemLink)
	if itemLink then
		-- Parse the link and get the itemID and difficultyID.
		local itemString = string.match(itemLink, "item[%-?%d:]+") or itemLink;
		local _, itemID, enchantId, gemId1, gemId2, gemId3, gemId4, suffixId, uniqueId, linkLevel, 
			specializationID, upgradeId, difficultyID, numBonusIds, bonusId1, bonusId2, upgradeValue = strsplit(":", itemString);
		
		-- Attempt to harvest the data.
		return app:Harvest(0, tonumber(difficultyID or "0"), tonumber(itemID or "0"), itemLink);
	end
end
function app:GetIllusionCollected(illusionID)
	-- The Blizzard development team really screwed this part up. They use the same sourceID notation, but for COMPLETELY DIFFERENT object types.
	-- The sourceID collisions are causing some pretty significant issues, so this extra work should fix the issue. (I don't like it, but nothing I can do.)
	local illusionData = C_TransmogCollection.GetIllusions(); -- NOTE: The order is different based on class, otherwise I would simplify this. :(
	for i,illusion in ipairs(illusionData) do
		-- It's better to return than to iterate through all of the illusions.
		if illusion.sourceID == illusionID then return illusion.isCollected; end
	end
end
function app:GetMountCollected(spellID)
	return select(11, app:GetMountInfo(spellID));
end
function app:GetMountInfo(spellID)	-- NOTE: MountID is ACTUALLY the SpellID. Addon developers: Do not confuse this as I did, mountID can be different per class/faction.
	local cache = app:GetTempDataMember("MOUNT_SPELLID_TO_MOUNTID");
	if not cache then
		-- Let's build the cache.
		local collected = app:GetDataMember("CollectedMounts", {});
		cache = {};
		
		local numMounts = C_MountJournal.GetNumMounts();
		for mountID=1,numMounts do
			-- The 2nd value in the list is the spellID for the mount.
			local _, spellID, _, _, _, _, _, _, _, _, isCollected = C_MountJournal.GetMountInfoByID(mountID);
			if spellID then
				collected[spellID] = isCollected;
				cache[spellID] = mountID;
			end
		end
		
		-- Assign to the temporary data container.
		app:SetTempDataMember("MOUNT_SPELLID_TO_MOUNTID", cache);
	end
	
	-- Return the mount information
	return C_MountJournal.GetMountInfoByID(cache[spellID]);
end
function app:GetSourceID(itemLink)
    if IsDressableItem(itemLink) then
		local itemID, _, _, slotName = GetItemInfoInstant(itemLink);
		if slotName then
			local slots = inventorySlotsMap[slotName];
			if slots then
				app.DressUpModel:SetUnit('player');
				app.DressUpModel:Undress();
				for i, slot in pairs(slots) do
					app.DressUpModel:TryOn(itemLink, slot);
					local sourceID = app.DressUpModel:GetSlotTransmogSources(slot);
					if sourceID ~= 0 then return sourceID, true; end
				end
			end
		end
		return nil, true;
	end
	return nil, false;
end
function app:Push(method)
	if container.stackCount < 1 then 
		-- While this pains me to do, it's the best way to guarantee that it gets done. (Sorry, I unregister this event when there's no work to do.)
		container:SetScript("OnUpdate", container.OnUpdate);
	end
	container.stackCount = container.stackCount + 1;
	table.insert(container.stack, method);
end
function app:StartCoroutine(instance)
	-- NOTE: "instance" as in a coroutine instance. I would use the word "coroutine", but that's a keyword in Lua. :(
	if instance then
		app:Push(function()
			-- Check the status of the coroutine
			if instance and coroutine.status(instance) ~= "dead" then
				local ok, err = coroutine.resume(instance);
				if ok then return true;	-- This means more work is required.
				else
					-- Show the error. Returning nothing is the same as cancelling the work.
					app.print("Error processing coroutine: ");
					app.print(err);
				end
			end
		end);
	end
end
app.RowContainer = container;
container:Show();

-- Row Helper Functions
local function CreateRow(parent, i)
	local row = CreateFrame("Button", nil, parent);
	row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD");
	row:SetHeight(parent.rowHeight);
	row.ClickHandler = nil;
	row:EnableMouse(true);
	row:RegisterForClicks("LeftButtonDown","RightButtonDown");
	row:SetScript("OnClick", function(self)
		if self.ClickHandler then self:ClickHandler(); end
	end);
	row:SetScript("OnEnter", function(self)
		local reference = self.ref; -- NOTE: This is the good ref value, not the parasitic one.
		if reference and GameTooltip then
			GameTooltipIcon:Hide();
			GameTooltip:ClearLines();
			GameTooltip:SetOwner(self, "ANCHOR_LEFT");
			
			-- NOTE: Order matters, we "fall-through" certain values in order to pass this information to the item ID section.
			if reference.item then
				-- This is an item reference. :)
				if reference.item.link then
					GameTooltip:SetHyperlink(reference.item.link);
				else
					GameTooltip:SetText(self.Label:GetText());
				end
				if reference.itemID then GameTooltip:AddDoubleLine(app:L("ITEM_ID"), tostring(reference.itemID)); end
				local classID, subClassID = reference.item.class, reference.item.subclass;
				if classID and subClassID then
					GameTooltip:AddDoubleLine(tostring(GetItemClassInfo(classID)) .. " (".. tostring(classID) .. ")", 
						tostring(GetItemSubClassInfo(classID, subClassID)) .. " (" .. tostring(subClassID) .. ")");
				end
				if reference.item.visualID then GameTooltip:AddDoubleLine(app:L("VISUAL_ID"), tostring(reference.item.visualID)); end
				if reference.item.slot and reference.item.slot ~= "" then GameTooltip:AddDoubleLine(app:L("ITEM_SLOT"), _G[reference.item.slot]); end
				if reference.item.isToy then
					GameTooltip:AddDoubleLine(app:L("TOY_ID"), tostring(reference.itemID));
					GameTooltip:AddDoubleLine(" ", app:L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
				elseif reference.item.illusionID then
					GameTooltip:AddDoubleLine(app:L("ILLUSION_ID"), tostring(reference.item.illusionID));
					GameTooltip:AddDoubleLine(" ", app:L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
				elseif reference.item.mountID then
					GameTooltip:AddDoubleLine(app:L("MOUNT_ID"), tostring(reference.item.mountID)); -- NOTE: This is a spellID base type.
					GameTooltip:AddDoubleLine(" ", app:L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
					local creatureName, spellID, icon, active, isUsable, sourceType, isFavorite, isFactionSpecific, faction, hideOnChar, isCollected, mountID = app:GetMountInfo(reference.item.mountID);
					if creatureName then
						local creatureDisplayID, descriptionText, sourceText, isSelfMount, mountType = C_MountJournal.GetMountInfoExtraByID(mountID);
						GameTooltip:AddLine(" ");
						GameTooltip:AddLine(tostring(creatureName));
						GameTooltip:AddLine(tostring(sourceText));
						GameTooltip:AddLine(tostring(descriptionText), 1, 1, 1, 1);
					end
				elseif reference.item.speciesID then
					GameTooltip:AddDoubleLine(app:L("PET_ID"), tostring(reference.item.speciesID));
					if reference.item.speciesID > 0 then
						GameTooltip:AddDoubleLine(" ", app:L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
						local speciesName, speciesIcon, petType, companionID, tooltipSource, tooltipDescription  = C_PetJournal.GetPetInfoBySpeciesID(reference.item.speciesID);
						if speciesName then
							GameTooltip:AddLine(" ");
							GameTooltip:AddLine(tostring(speciesName));
							GameTooltip:AddLine(tostring(petType));
							GameTooltip:AddLine(tostring(tooltipDescription), 1, 1, 1, 1);
						end
					else
						GameTooltip:AddLine(app:L("SPECIES_ID_MISSING"), 1, 0, 0, 0.5);
					end
				elseif reference.item.sourceID then
					GameTooltip:AddDoubleLine(app:L("SOURCE_ID"), tostring(reference.item.sourceID));
					GameTooltip:AddDoubleLine(" ", app:L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
				elseif not reference.rows and not reference.collected then
					GameTooltip:AddLine(app:L("SOURCE_ID_MISSING"), 1, 0, 0, 0.5);
				end
			elseif reference.instanceID then
				-- Contains information about an Instance (Raid or Dungeon)
				-- Is the player's character saved to this instance?
				if self.ref.saved and self.ref.saved then
					GameTooltip:SetInstanceLockEncountersComplete(self.ref.saved);
					GameTooltip:AddLine(" ");
				end
				
				-- Show information about the instance
				GameTooltip:AddDoubleLine(reference.text or "???", app:L("INSTANCE"));
				GameTooltip:AddDoubleLine(app:L("INSTANCE_ID"), tostring(reference.instanceID));
			end
			
			-- Standalone Fields
			if reference.tierID then
				-- Contains information about the Expansion. (called "Tier" in the WoW API)
				GameTooltip:AddDoubleLine(EJ_GetTierInfo(reference.tierID), app:L("EXPANSION"));
				GameTooltip:AddDoubleLine(app:L("EXPANSION_ID"), tostring(reference.tierID));
				local tier = app.TierDB[reference.tierID];
				if tier then
					if tier.texture then
						GameTooltipIcon:SetSize(128,64);
						GameTooltipIcon.icon:SetTexture(tier.texture);
						GameTooltipIcon:Show();
					elseif tier.atlas then
						GameTooltipIcon:SetSize(128,64);
						GameTooltipIcon.icon:SetAtlas(tier.atlas);
						GameTooltipIcon:Show();
					end
				end
			end
			if reference.difficultyID then
				-- Contains information about a Difficulty Setting (only useful information is the name really)
				GameTooltip:AddDoubleLine(GetDifficultyInfo(reference.difficultyID), app:L("DIFFICULTY"));
				GameTooltip:AddDoubleLine(app:L("DIFFICULTY_ID"), tostring(reference.difficultyID));
			end
			if reference.encounterID then
				-- Contains information about an Encounter. (TODO: Show the models for the bosses?)
				--local name, description, _, _, link = EJ_GetEncounterInfo(reference.encounterID);
				--self.link = link;	-- TODO: Clicking on this opens Adventure Guide?
				GameTooltip:AddDoubleLine(reference.text or "???", app:L("ENCOUNTER"));
				GameTooltip:AddDoubleLine(app:L("ENCOUNTER_ID"), tostring(reference.encounterID));
			end
			
			-- Miscellaneous fields
			if reference.description then GameTooltip:AddLine(reference.description, 1, 1, 1, 1); end
			if reference.icon then
				if reference.itemID then
					GameTooltipIcon:SetSize(64,64);
				else
					GameTooltipIcon:SetSize(96,96);
				end
				GameTooltipIcon.icon:SetTexture(reference.icon);
				GameTooltipIcon:Show();
			end
			if reference.mapID then
				GameTooltip:AddDoubleLine(app:L("MAP_ID"), tostring(reference.mapID));
				if reference.mapID == 0 then
					-- Looks like Blizzard goofed and didn't include the map ID. Way to go!
					GameTooltip:AddLine(app:L("MAP_ID_INVALID_WARNING"), 1, 0, 0, 0.5);
					local mapID = app:GetPersonalDataMember("MapID");
					if mapID then
						local mapName = GetMapNameByID(mapID);
						if reference.text == mapName then
							app.print("Updated Map ID to #" .. tostring(mapID) .. " for " .. reference.text);
							reference.mapID = mapID;
							app:RefreshScrollbar();
						else
							GameTooltip:AddDoubleLine("Map ID (Current)", tostring(mapID));
							GameTooltip:AddLine(mapName);
						end
					end
				end
			end
			if not reference.item and reference.Lvl and reference.Lvl > 0 then
				GameTooltip:AddDoubleLine(app:L("REQUIRES_LEVEL"), tostring(reference.Lvl));
			end
			-- No reference data, so let's display the label's text.
			if GameTooltip:NumLines() < 1 then GameTooltip:SetText(self.Label:GetText()); end
			if reference.rows then GameTooltip:AddLine(app:L("CLICK_TO_EXPAND"), 1, 1, 1); end
			GameTooltip:Show();
		end
	end);
	row:SetScript("OnLeave", function(self)
		if self.ref and GameTooltip then
			GameTooltip:Hide();
			GameTooltipIcon:Hide();
		end
	end);
	if #parent.rows == 0 then
		row:SetPoint("TOPLEFT");
		row:SetPoint("TOPRIGHT");
	else
		row:SetPoint("TOPLEFT", parent.rows[#parent.rows], "BOTTOMLEFT");
		row:SetPoint("TOPRIGHT", parent.rows[#parent.rows], "BOTTOMRIGHT");
	end
	table.insert(parent.rows, row);
end
local function ClearRowData(self)
	self.ref = nil;
	if self.Background then self.Background:Hide(); end
	if self.Texture then self.Texture:Hide(); end
	if self.Indicator then
		self.Indicator:Hide();
		self.Indicator.instanceID = nil;
	end
	if self.Summary then self.Summary:Hide(); end
	if self.Label then self.Label:Hide(); end
	self:Hide();
end
local function RowClickHandler(self)
	local reference = self.ref;
	if reference then
		if IsModifiedClick("CHATLINK") then
			app.print(reference.link or self.Label:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(reference.link or self.Label:GetText());
		elseif reference.rows then
			reference.expanded = not reference.expanded;
			app:RefreshScrollbar(true);
		end
	else
		if IsModifiedClick("CHATLINK") then
			app.print(self.Label:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(self.Label:GetText());
		end
	end
end
local function SetRowData(row, data)
	if data then
		if data.item then
			if data.item.link or (data.item.refresh and data.item:refresh()) then
				data.icon = data.item.texture;
				data.text = data.item.link;
			else
				data.icon = "Interface\\Icons\\INV_Misc_QuestionMark";
				data.text = "Fetching Item #" .. tostring(data.itemID);
				
				-- A refresh will be necessary on the next update. :(
				if not app.processingLinks then
					app.processingLinks = true;
					app:Push(function()
						app.processingLinks = nil;
						app:UpdateVisibleRowData();
					end);
				end
			end
		end
		if data.text then
			local leftmost = row;
			local relative = "LEFT";
			local x = (((data.indent or 0) + 1) * 8) or 0;
			if data.back then
				if not row.Background then
					row.Background = row:CreateTexture(nil, "BACKGROUND");
					row.Background:SetAllPoints(row);
					row.Background:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight");
				end
				row.Background:SetAlpha(data.back or 0.2);
				row.Background:Show();
			end
			if data.instanceID then
				local saved = app:GetPersonalDataSubMember("Saves", data.text); -- TODO: Convert this to an int-based system.... maybe?
				if saved then
					if not row.Indicator then
						row.Indicator = row:CreateTexture(nil, "ARTWORK");
						row.Indicator:SetPoint("TOP");
						row.Indicator:SetPoint("BOTTOM");
						row.Indicator:SetWidth(row:GetHeight());
					end
					row.Indicator:SetTexture("Interface\\FriendsFrame\\StatusIcon-DnD");
					row.Indicator:SetPoint("RIGHT", leftmost, relative, x, 0);
					row.Indicator.instanceID = saved;
					row.Indicator:Show();
					data.saved = saved;
				end
			elseif data.unobtainable then
				if not row.Indicator then
					row.Indicator = row:CreateTexture(nil, "ARTWORK");
					row.Indicator:SetPoint("TOP");
					row.Indicator:SetPoint("BOTTOM");
					row.Indicator:SetWidth(row:GetHeight());
				end
				local reason = app:L("UNOBTAINABLE_ITEM_REASONS")[data.unobtainable or 1];
				row.Indicator:SetTexture(app:L("UNOBTAINABLE_ITEM_TEXTURES")[reason[1]] or "Interface\\FriendsFrame\\StatusIcon-DnD");
				row.Indicator:SetPoint("RIGHT", leftmost, relative, x, 0);
				row.Indicator.instanceID = nil;
				row.Indicator:Show();
			end
			if data.icon then
				if not row.Texture then
					row.Texture = row:CreateTexture(nil, "ARTWORK");
					row.Texture:SetPoint("TOP");
					row.Texture:SetPoint("BOTTOM");
					row.Texture:SetWidth(row:GetHeight());
				end
				row.Texture:SetPoint("LEFT", leftmost, relative, x, 0);
				row.Texture:SetTexture(data.icon);
				row.Texture:Show();
				leftmost = row.Texture;
				relative = "RIGHT";
				x = 4;
			end
			if not row.Label then
				row.Label = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
				row.Label:SetJustifyH("CENTER");
				row.Label:SetPoint("TOP");
				row.Label:SetPoint("BOTTOM");
			end
			row.Label:SetPoint("LEFT", leftmost, relative, x, 0);
			row.Label:SetText(data.text);
			row.Label:Show();
			leftmost = row.Label;
			relative = "RIGHT";
			row:Show();
			row.ref = data;
			row.ClickHandler = RowClickHandler;
			if data.total and data.total > 0 then
				if not row.Summary then
					row.Summary = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
					row.Summary:SetJustifyH("CENTER");
					row.Summary:SetPoint("TOP");
					row.Summary:SetPoint("RIGHT");
					row.Summary:SetPoint("BOTTOM");
				end
				local percent = data.collected / data.total;
				row.Summary:SetText("|c" .. GetProgressColor(percent) .. tostring(data.collected) .. " / " .. tostring(data.total) .. " (" .. tostring(floor(percent * 100)) .. "%)|r");
				row.Summary:Show();
			elseif data.itemID and not data.collected then
				if not row.Summary then
					row.Summary = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
					row.Summary:SetJustifyH("CENTER");
					row.Summary:SetPoint("TOP");
					row.Summary:SetPoint("RIGHT");
					row.Summary:SetPoint("BOTTOM");
				end
				row.Summary:SetText("|cFFFF0000X|r");
				row.Summary:Show();
			end
		end
	end
end
for i=1,container.rowCount do CreateRow(container, i); end
function app:UpdateVisibleRowData()
	-- Get the Cached Row Data
	local rowData = app:GetTempDataMember("ROW_DATA");
	if not rowData then return; end
	
	local totalRowCount = #rowData + 3;
	local rowCount = self.RowContainer.rowCount;
	local current = math.max(1, math.min(self.ScrollBar.CurrentValue, totalRowCount - rowCount));
	for i=1,rowCount do
		local row = self.RowContainer.rows[i];
		ClearRowData(row);
		if current <= totalRowCount then
			SetRowData(row, rowData[current]);
		end
		current = current + 1;
	end
end

-- Filtering
function app.Filter()
	-- Meaning "Don't display."
	return false;
end
function app.NoFilter()
	-- Meaning "Display as expected."
	return true;
end
function app.FilterGroupsByLevel(group)
	return app.Level >= (group.Lvl or 0);
end
function app.FilterGroupsByCompletion(group)
	return group.collected < group.total;
end
function app.FilterItemClass(item)
	return app.FilterItemClass_RequireItemFilter(item.class, item.subclass)
	   and app.ClassRequirementFilter(item.classes)
	   and app.RaceRequirementFilter(item.races);
end
function app.FilterItemClass_RequireClasses(classes)
	if classes then
		for key,value in pairs(classes) do
			if value == app.ClassIndex then return true; end
		end
	else
		return true;
	end
end
function app.FilterItemClass_RequireItemFilter(class, subclass)
	if class then
		local filters = app:GetPersonalDataSubMember("ItemClassFilters", class);
		if filters and not filters[subclass or 0] then return false; end
	end
	return true;
end
function app.FilterItemClass_RequireRaces(races)
	if races then
		for key,value in pairs(races) do
			if value == app.RaceIndex then return true; end
		end
	else
		return true;
	end
end
function app.FilterItemSource(sourceInfo)
	return sourceInfo.isCollected;
end
function app.FilterItemSourceUnique(sourceInfo)
	if sourceInfo.isCollected then
		-- NOTE: This makes it so that the loop isn't necessary.
		return true;
	else
		-- If at least one of the sources of this visual ID was collected, that means that we've acquired the base appearance.
		-- NOTE: Some of the messaging provided by Blizzard "You haven't collected this appearance." is actually wrong.
		-- In some cases, one or more of the sources for that visual ID was collected already, just not currently
		-- available to your faction / class / character level. (This is kinda silly, but that's how it works)
		-- Please do not file bugs regarding this, I've already identified that this was the reason for the message discrepency.
		for i, sourceID in ipairs(C_TransmogCollection.GetAllAppearanceSources(sourceInfo.visualID)) do
			local otherSource = C_TransmogCollection.GetSourceInfo(sourceID);
			if otherSource.isCollected then return true; end
		end
		return false;
	end
end

-- Default Filter Settings (changed in VARIABLES_LOADED and in the Options Menu)
app.GroupRequirementsFilter = app.NoFilter;
app.GroupVisibilityFilter = app.NoFilter;
app.ItemSourceFilter = app.FilterItemSource;
app.CollectedItemVisibilityFilter = app.NoFilter;
app.MissingItemVisibilityFilter = app.NoFilter;
app.ClassRequirementFilter = app.NoFilter;
app.RaceRequirementFilter = app.NoFilter;

-- Refresh Functions (Coroutines)
function app:RefreshLocation()
	if not self.refreshingLocation then
		self.refreshingLocation = true;
		app:StartCoroutine(coroutine.create(function()
			-- While the player is in combat, wait for combat to end.
			while InCombatLockdown() do coroutine.yield(); end
			
			-- This is necessary because the map area ID for instances
			-- is -1 when you initially enter them for a few moments. (not even a full second)
			local mapID = GetCurrentMapAreaID();
			while not mapID or mapID < 0 do
				coroutine.yield();
				mapID = GetCurrentMapAreaID();
			end
			
			-- Cache that we're in the current map ID.
			self:SetPersonalDataMember("MapID", mapID);
			self:RefreshScrollbar();
			self.refreshingLocation = false;
		end));
	end
end
function app:RefreshSaves()
	if not self.refreshingSaves then
		self.refreshingSaves = true;
		app:StartCoroutine(coroutine.create(function()
			-- While the player is in combat, wait for combat to end.
			while InCombatLockdown() do coroutine.yield(); end
			
			-- Update Saved Instances (I have to clear it because the saves do eventually reset)
			local saves, newSave, oldSaves = {}, false, self:GetPersonalDataMember("Saves", {});
			local converter = app:L("SAVED_TO_DJ_INSTANCES");
			for instanceIter=1,GetNumSavedInstances() do
				local name, id, reset, difficulty, locked, extended, instanceIDMostSig = GetSavedInstanceInfo(instanceIter);
				if locked then
					--app.print(tostring(instanceIter) .. ": " .. tostring(name) .. ", " .. tostring(id) .. ", " .. tostring(difficulty) .. ", " .. tostring(instanceIDMostSig));
					name = converter[name] or name;
					saves[name] = instanceIter;
					
					-- If this instance was not previously saved, mark it.
					if not oldSaves[name] then newSave = true; end
				end
			end
			
			-- Check the old saves vs. the new saves
			if not newSave then
				for key, value in pairs(oldSaves) do
					if not saves[key] then newSave = true; end
				end
			end
			
			-- If there's a new save, then also refresh the location. (this is used to apply the saved icon and tooltip)
			self.refreshingSaves = false;
			if newSave then
				self:SetPersonalDataMember("Saves", saves);
				app:RefreshLocation();
			end
		end));
	end
end
function app:RefreshScrollbar(lazy, safely)
	self.refreshScrollbarForce = self.refreshScrollbarForce or not lazy;
	if not self.refreshingScrollbar then
		self.refreshingScrollbar = true;
		app:StartCoroutine(coroutine.create(function()
			-- This method can be triggered by an event, if so, we want to safely wait for combat to end.
			if safely then
				-- While the player is in combat, wait for combat to end.
				while InCombatLockdown() do coroutine.yield(); end
			end
			
			-- Update the Row Data by filtering raw data
			local allData = app:GetTempDataMember("CACHE");
			if not allData or self.refreshScrollbarForce then
				self.refreshScrollbarForce = nil;
				allData = {};
				allData.rows = {};
				allData.expanded = true;
				allData.text = app:L("TOTAL_COMPLETION");
				allData.visible = true;
				allData.collected = 0;
				allData.total = 0;
				local groups = {};
				
				-- Dungeons & Raids
				local db = {};
				db.expanded = true;
				db.text = "Dungeons & Raids";
				db.groups = app.SourceDB[1];
				table.insert(groups, db);
				
				-- Everything (for debugging)
				--[[
				local everything = {};
				everything.expanded = false;
				everything.text = "EVERRRRYTTHING";
				everything.groups = {};
				table.insert(groups, everything);
				for difficultyID,items in pairs(app.ItemDB) do
					local g = {};
					g.expanded = true;
					g.text = tostring(difficultyID);
					g.difficultyID = difficultyID;
					local h = {};
					g.groups = h;
					table.insert(everything.groups, g);
					for itemID, item in pairs(items) do
						table.insert(h, { ["itemID"] = itemID });
					end
				end
				]]--
				
				local missingData = {};
				missingData.visible = true;
				missingData.expanded = false;
				missingData.text = "Items without Sources";
				missingData.rows = self:GetTempDataMember("Missing", {});
				table.wipe(missingData.rows);
				table.insert(groups, missingData);
				
				local missingLinkData = {};
				missingLinkData.visible = true;
				missingLinkData.expanded = false;
				missingLinkData.text = "Items without Links";
				missingLinkData.rows = self:GetTempDataMember("MissingLinks", {});
				table.wipe(missingLinkData.rows);
				table.insert(groups, missingLinkData);
				self:UpdateGroups(allData, groups, 0, 0);
				missingLinkData.groups = {};
				missingData.groups = {};
				app:SetTempDataMember("CACHE", allData);
				--collectgarbage();
			end
			
			-- Post Process all of the Data
			local data = app:GetTempDataMember("ROW_DATA");
			wipe(data);
			if allData.collected < allData.total then
				self:ProcessGroup(data, allData, 0, 0);
			else
				--[[
				if allData.total > 1 then
					tinsert(data, {["i"]=2,["text"] = app:L("CONGRATULATIONS")});
				else
					tinsert(data, {["i"]=2,["text"] = app:L("INSTRUCTIONS")});
				end
				]]--
			end
			
			-- Update the Row Container and Scroll Bar
			local rowCount = self.RowContainer.rowCount;
			scrollbar:SetMinMaxValues(1, math.max(1, (#data + 3) - rowCount));
			self:UpdateVisibleRowData();
			self.refreshScrollbarForce = false;
			self.refreshingScrollbar = false;
		end));
	end
end
function app:ProcessGroup(data, parent, indent, back)
	if parent.visible then
		if parent.mapID and parent.mapID == self:GetPersonalDataMember("MapID") then
			parent.back = 1;
		else 
			parent.back = back or parent.back;
		end
		parent.indent = indent;
		tinsert(data, parent);
		if parent.rows and parent.expanded then
			indent = indent + 1;
			back = parent.back * 0.5;
			for j, group in ipairs(parent.rows) do
				self:ProcessGroup(data, group, indent, back);
			end
		end
	end
end
function app:UpdateGroups(parent, groups, instanceID, difficultyID)
	if groups then
		-- Iterate through the groups
		for key, group in ipairs(groups) do
			-- Determine if this user can enter the instance or acquire the item.
			local meetsRequirements = app.GroupRequirementsFilter(group);
			if meetsRequirements then
				-- Create a Row Data Object
				group.visible = true;
				tinsert(parent.rows, group);
				
				-- Get additional information, if necessary.
				if group.itemID then
					local missingSource = false;
					if not group.item then group.item = app:Harvest(instanceID, difficultyID, group.itemID); end
					group.unobtainable = app:L("UNOBTAINABLE_ITEMS")[group.itemID];
					if group.item.mountID then
						group.collected = self:GetDataSubMember("CollectedMounts", group.item.mountID);
						if not group.collected then
							group.collected = app:GetMountCollected(group.item.mountID);
							self:GetDataSubMember("CollectedMounts", group.item.mountID, group.collected);
						end
					elseif group.item.illusionID then
						group.collected = self:GetDataSubMember("CollectedIllusions", group.item.illusionID);
						if not group.collected then
							group.collected = app:GetIllusionCollected(group.item.illusionID);
							self:SetDataSubMember("CollectedIllusions", group.item.illusionID, group.collected);
						end
					elseif group.item.isToy then -- Toys    or (group.item.class == 4 and group.item.subclass == 0)
						group.collected = self:GetDataSubMember("CollectedToys", group.itemID);
						if not group.collected then
							group.collected = C_ToyBox.IsToyUsable(group.itemID);
							self:SetDataSubMember("CollectedToys", group.itemID, group.collected);
						end
					elseif group.item.class == 15 and group.item.subclass == 2 then -- Companion Pets
						group.collected = self:GetDataSubMember("CollectedPets", group.itemID);
						if not group.collected then
							if group.item.speciesID then
								local numCollected, limit = C_PetJournal.GetNumCollectedInfo(group.item.speciesID);
								group.collected = (numCollected or 0) > 0;
								self:SetDataSubMember("CollectedPets", group.itemID, group.collected);
							else
								missingSource = true;
								group.item.speciesID = 0;
							end
						end
					elseif group.item.sourceID then
						group.collected = self:GetDataSubMember("Collected", group.item.sourceID);
						if not group.collected then
							local sourceInfo = C_TransmogCollection.GetSourceInfo(group.item.sourceID);
							if sourceInfo then
								group.collected = app.ItemSourceFilter(sourceInfo);
								self:SetDataSubMember("Collected", group.item.sourceID, group.collected);
							end
						end
					elseif not group.groups then missingSource = true; end
					
					-- If the 'can equip' filter says true
					if app.FilterItemClass(group.item) then
						if group.collected then
							group.visible = app.CollectedItemVisibilityFilter(group);
						else
							group.visible = app.MissingItemVisibilityFilter(group);
						end
						
						-- Increment the parent group's totals. (but only if it is visible)
						if not group.groups then
							parent.total = (parent.total or 0) + 1;
							if group.collected then parent.collected = (parent.collected or 0) + 1; end
						end
						
						-- Items without sources list (for debugging)
						--[[
						if missingSource and group.visible then
							table.insert(self:GetTempDataMember("Missing"), group);
							if not group.item.link then table.insert(self:GetTempDataMember("MissingLinks"), group); end
						end
						]]--
					else
						group.visible = false;
					end
				elseif group.npcID then
					-- TODO: Move this to an NPC constructor.
					group.text = app:L("NPC_ID_NAMES")[group.npcID] or ("Unknown NPC ID #" .. group.npcID);
				end
			
				-- Does this group have subgroups? (if so, it will bypass the filtering set above)
				if group.groups then
					if not group.rows then
						group.rows = {};
					else
						wipe(group.rows);
					end
					if group.item and group.item.sourceID then
						group.total = 1;
						group.collected = (group.collected and 1) or 0;
					else
						group.total = 0;
						group.collected = 0;
					end
					self:UpdateGroups(group, group.groups, group.instanceID or instanceID, group.difficultyID or difficultyID);
					parent.collected = (parent.collected or 0) + group.collected;
					parent.total = (parent.total or 0) + group.total;
					group.visible = group.total > 0 and app.GroupVisibilityFilter(group);
				elseif group.rows and group.visible then
					group.rows = group.rows;
					group.total = #group.rows;
					group.collected = 0;
					parent.collected = (parent.collected or 0) + group.collected;
					parent.total = (parent.total or 0) + group.total;
					group.visible = group.total > 0;
				end
			end
		end
	end
end

-- Register Events required at the start
app:RegisterEvent("PLAYER_LOGIN");
app:RegisterEvent("VARIABLES_LOADED");
app:RegisterEvent("TOYS_UPDATED");
app:RegisterEvent("ZONE_CHANGED_NEW_AREA");
app:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_ADDED");
app:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_REMOVED");

-- Define Event Behaviours
events.VARIABLES_LOADED = function()
	-- Setup the localization and interpret the Display Information.
	app:SetLocale(GetLocale());
	app.DisplayName = app.Colorize(app:L("TITLE"), app.RGBToHex(180, 180, 255));
	app.Title:SetText(app.DisplayName .. " - " .. app:L("LIST_FRAME_TITLE"));
	
	-- Cache information about the player.
	local _, class, classIndex = UnitClass("player");
	local raceName, race = UnitRace("player");
	app.Class = class;
	app.ClassIndex = classIndex;
	app.Level = UnitLevel("player");
	local raceIndex = app.RaceDB[race];
	if type(raceIndex) == "table" then
		local factionGroup = UnitFactionGroup("player");
		raceIndex = raceIndex[factionGroup];
	end
	--print(raceName .. ": " .. race .. " [" .. tostring(raceIndex) .. "]");
	app.Race = race;
	app.RaceIndex = raceIndex;
	
	-- Load in the Presets if they exist for this character.
	-- Default values should fallback to their class presets.
	local filters = app:GetPersonalDataMember("ItemClassFilters");
	if not filters then
		-- If a preset exist, we need to cleanly duplicate the preset data.
		-- If we don't do that, changing settings will change the preset itself. (until a restart)
		local presets, data = app.Presets[app.Class], {};
		if presets then
			for itemClassID, itemClass in pairs(presets) do
				local subdata = {};
				data[itemClassID] = subdata;
				for subClassID, preset in pairs(itemClass) do
					subdata[subClassID] = preset;
				end
			end
		end
		app:SetPersonalDataMember("ItemClassFilters", data);
	end
	
	-- Register for Dynamic Events and Assign Filters
	if app:GetDataMember("AutoRefreshSaves", true) then app:RegisterEvent("BOSS_KILL"); end
	if app:GetDataMember("FilterGroupsByLevel", false) then
		app:RegisterEvent("PLAYER_LEVEL_UP");
		app.GroupRequirementsFilter = app.FilterGroupsByLevel;
	else
		app.GroupRequirementsFilter = app.NoFilter;
	end
	if app:GetDataMember("ShowCompletedGroups", false) then
		app.GroupVisibilityFilter = app.NoFilter;
	else
		app.GroupVisibilityFilter = app.FilterGroupsByCompletion;
	end
	if app:GetDataMember("ShowCollectedItems", false) then
		app.CollectedItemVisibilityFilter = app.NoFilter;
	else
		app.CollectedItemVisibilityFilter = app.Filter;
	end
	if app:GetDataMember("ShowMissingItems", true) then
		app.MissingItemVisibilityFilter = app.NoFilter;
	else
		app.MissingItemVisibilityFilter = app.Filter;
	end
	if app:GetDataMember("ShowMinimapButton", true) then
		app.Minimap = app:CreateMinimapButton(); -- NOTE: Create this if they turn it on.
		app.Minimap:Show();
	end
	if app:GetDataMember("UniqueAppearances", false) then
		app.ItemSourceFilter = app.FilterItemSourceUnique;
	else
		app.ItemSourceFilter = app.FilterItemSource;
	end
	if app:GetDataMember("FilterItemsByClass", true) then
		app.ClassRequirementFilter = app.FilterItemClass_RequireClasses;
	else
		app.ClassRequirementFilter = app.NoFilter;
	end
	if app:GetDataMember("FilterItemsByRace", true) then
		app.RaceRequirementFilter = app.FilterItemClass_RequireRaces;
	else
		app.RaceRequirementFilter = app.NoFilter;
	end
	
	-- Create the Settings Menu
	app.Settings = app:CreateSettingsMenu();
	app.print(format(app:L("LOADING"), GetAddOnMetadata("AllTheThings", "Version")));
	
	-- Set the scrollbar data
	app:SetTempDataMember("ROW_DATA", {});
	app:RefreshSaves();	-- Since this is index-based, it can be wrong when you login.
	app:RefreshScrollbar(false, true);
	scrollbar:SetValue(1);
end
events.PLAYER_LOGIN = function()
	LibStub:GetLibrary("LibDataBroker-1.1"):NewDataObject(app.DisplayName, {
		type = "launcher",
		icon = "Interface\\Icons\\INV_Axe_106",
		OnTooltipShow = function(self)
			GameTooltip:SetText(app.DisplayName);
			GameTooltip:AddLine(app:L("MINIMAP_MOUSEOVER_TEXT"), 1, 1, 1);
			GameTooltip:Show();
		end,
		OnClick = function(clickedframe, button)
			if button == "RightButton" then
				-- Right Button opens the Options menu.  (Calling this twice actually makes it work correctly)
				InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
				InterfaceOptionsFrame_OpenToCategory(app:L("TITLE_OPTIONS"));
			else
				-- Left Button Toggles the App
				if app:IsVisible() then app:Hide(); else app:Show(); end
			end
		end,
	})
end
events.ZONE_CHANGED_NEW_AREA = function()
	-- NOTE: This will only lag a little if you have the addon window shown.
	-- Also note: It will automatically run the coroutine when you show the window.
	app:RefreshLocation();
	app:RefreshSaves();
end
events.PLAYER_LEVEL_UP = function(newLevel)
	app.Level = newLevel;
	-- app.print("NEW LEVEL: " .. newLevel);
	app:RefreshScrollbar(false, true);
end
events.BOSS_KILL = function(id, name)
	-- This is so that when you kill a boxx, you can trigger 
	-- an automatic update of your saved instance cache. 
	-- (It does lag a little, but you can disable this if you want.)
	-- Waiting until the LOOT_CLOSED occurs will prevent the failed Auto Loot bug.
	--app.print("BOSS_KILL: " .. tostring(id) .. ", " .. tostring(name));
	app:UnregisterEvent("LOOT_CLOSED");
	app:RegisterEvent("LOOT_CLOSED");
end
events.LOOT_CLOSED = function()
	-- Once the loot window closes after killing a boss, THEN trigger the update.
	app:UnregisterEvent("LOOT_CLOSED");
	app:UnregisterEvent("UPDATE_INSTANCE_INFO");
	app:RegisterEvent("UPDATE_INSTANCE_INFO");
	RequestRaidInfo();
end
events.UPDATE_INSTANCE_INFO = function()
	-- We got new information, not refresh the saves. :D
	app:UnregisterEvent("UPDATE_INSTANCE_INFO");
	app:RefreshSaves();
end
events.TOYS_UPDATED = function(itemID, new)
	if itemID then
		local name, link = GetItemInfo(itemID);
		if link then
			print(format(app:L("ITEM_ID_ADDED"), link, itemID));
			app:PlayFanfare();
			app:RefreshScrollbar(false, true);
		end
	end
end
events.TRANSMOG_COLLECTION_SOURCE_ADDED = function(sourceID)
	-- NOTE: The itemlink that gets passed is BASE ITEM LINK, not the full item link.
	-- So this may show green items where an epic was obtained. (particularly with Legion drops)
	-- This is okay since items of this type share their appearance regardless of the power of the item.
	local itemLink = select(6, C_TransmogCollection.GetAppearanceSourceInfo(sourceID));
	if itemLink then
		local data = app:HarvestItemLink(itemLink);
		if data and not app:GetDataSubMember("Collected", sourceID) then
			print(format(app:L("ITEM_ID_ADDED"), itemLink, data.itemID));
			app:SetDataSubMember("Collected", sourceID, true);
			app:PlayFanfare();
			app:RefreshScrollbar(false, true);
		end
	end
end
events.TRANSMOG_COLLECTION_SOURCE_REMOVED = function(sourceID)
	-- Oh shucks, that was nice of you to give this item to your friend.
	-- WAIT, WHAT? A VENDOR?! OH GOD NO! TODO: Warn a user when they vendor an appearance?
	local itemLink = select(6, C_TransmogCollection.GetAppearanceSourceInfo(sourceID));
	if itemLink then
		local data = app:HarvestItemLink(itemLink);
		if data and app:GetDataSubMember("Collected", sourceID) then
			print(format(app:L("ITEM_ID_REMOVED"), itemLink, data.itemID));
			app:SetDataSubMember("Collected", sourceID, false);
			app:PlayRemoveSound();
			app:RefreshScrollbar(false, true);
		end
	end
end