--------------------------------------------------------------------------------
--                        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, 6);
	frame.UniqueAppearances.Label = _G[frame.UniqueAppearances:GetName() .. "Text"];
	frame.UniqueAppearances.Label:SetText("Unique Appearances");
	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);

	-- 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
		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
			local name = EJ_GetTierInfo(t.tierID);
			if name then
				rawset(t, "text", name);
				return t[key];
			end
		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" or key == "description" or key == "icon" or key == "link" then
			local name, description, _, _, _, backgroundImage, _, link = EJ_GetInstanceInfo(t.instanceID);
			if name then
				rawset(t, "text", name);
				rawset(t, "description", description or "");
				if backgroundImage then rawset(t, "icon", backgroundImage); end
				rawset(t, "link", link or "");
				return t[key];
			end
		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
			local name = GetDifficultyInfo(t.difficultyID);
			if name then
				rawset(t, "text", name);
				return t[key];
			end
		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(table, value)
	for i, entry in ipairs(table) do
		if entry == value then return true; end
	end
end

-- Taken from the encounter journal repo (https://github.com/Resike/BlizzardInterfaceCode/blob/live/Interface/AddOns/Blizzard_EncounterJournal/Blizzard_EncounterJournal.lua)
local EJ_DIFFICULTIES =
{
	{ size = "5", prefix = PLAYER_DIFFICULTY1, difficultyID = 1 },
	{ size = "5", prefix = PLAYER_DIFFICULTY2, difficultyID = 2 },
	{ size = "5", prefix = PLAYER_DIFFICULTY6, difficultyID = 23 },
	{ size = "5", prefix = PLAYER_DIFFICULTY_TIMEWALKER, difficultyID = 24 },
	{ size = "25", prefix = PLAYER_DIFFICULTY3, difficultyID = 7 },
	{ size = "10", prefix = PLAYER_DIFFICULTY1, difficultyID = 3 },
	{ size = "10", prefix = PLAYER_DIFFICULTY2, difficultyID = 5 },
	{ size = "25", prefix = PLAYER_DIFFICULTY1, difficultyID = 4 },
	{ size = "25", prefix = PLAYER_DIFFICULTY2, difficultyID = 6 },
	{ prefix = PLAYER_DIFFICULTY3, difficultyID = 17 },
	{ prefix = PLAYER_DIFFICULTY1, difficultyID = 14 },
	{ prefix = PLAYER_DIFFICULTY2, difficultyID = 15 },
	{ prefix = PLAYER_DIFFICULTY6, difficultyID = 16 },
};

-- This was directly taken from mOnar's Wardrobe Helper. (Thanks man, your addon helped me learn a lot about how the poorly documented Blizzard code worked)
local TYPES = { 'Boss drop', 'Quests', 'Vendor', 'World/Instance Drop', 'Achievements', 'Professions' };

-- 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 anser 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 baseItem = app.ItemDB[difficultyID] and app.ItemDB[difficultyID][itemID] or 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
		
		-- 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)
		if self.text then
			GameTooltipIcon:Hide();
			GameTooltip:ClearLines();
			GameTooltip:SetOwner(self, "ANCHOR_LEFT");
			if self.ref then
				-- NOTE: Order matters, we "fall-through" certain values in order to pass this information to the item ID section.
				if self.ref.item then
					-- This is an item reference. :)
					if self.ref.item.link then
						GameTooltip:SetHyperlink(self.ref.item.link);
					else
						GameTooltip:SetText(self.text:GetText());
					end
					if self.ref.itemID then GameTooltip:AddDoubleLine(app:L("ITEM_ID"), tostring(self.ref.itemID)); end
					local classID, subClassID = self.ref.item.class, self.ref.item.subclass;
					if classID and subClassID then
						GameTooltip:AddDoubleLine(tostring(GetItemClassInfo(classID)) .. " (".. tostring(classID) .. ")", 
							tostring(GetItemSubClassInfo(classID, subClassID)) .. " (" .. tostring(subClassID) .. ")");
					end
					if self.ref.item.visualID then GameTooltip:AddDoubleLine(app:L("VISUAL_ID"), tostring(self.ref.item.visualID)); end
					if self.ref.item.slot and self.ref.item.slot ~= "" then GameTooltip:AddDoubleLine(app:L("ITEM_SLOT"), _G[self.ref.item.slot]); end
					if self.ref.item.isToy then
						GameTooltip:AddDoubleLine(app:L("TOY_ID"), tostring(self.ref.itemID));
						GameTooltip:AddDoubleLine(" ", app:L(self.ref.collected and "COLLECTED" or "NOT_COLLECTED"));
					elseif self.ref.item.illusionID then
						GameTooltip:AddDoubleLine(app:L("ILLUSION_ID"), tostring(self.ref.item.illusionID));
						GameTooltip:AddDoubleLine(" ", app:L(self.ref.collected and "COLLECTED" or "NOT_COLLECTED"));
					elseif self.ref.item.mountID then
						GameTooltip:AddDoubleLine(app:L("MOUNT_ID"), tostring(self.ref.item.mountID)); -- NOTE: This is a spellID base type.
						GameTooltip:AddDoubleLine(" ", app:L(self.ref.collected and "COLLECTED" or "NOT_COLLECTED"));
						local creatureName, spellID, icon, active, isUsable, sourceType, isFavorite, isFactionSpecific, faction, hideOnChar, isCollected, mountID = app:GetMountInfo(self.ref.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 self.ref.item.speciesID then
						GameTooltip:AddDoubleLine(app:L("PET_ID"), tostring(self.ref.item.speciesID));
						if self.ref.item.speciesID > 0 then
							GameTooltip:AddDoubleLine(" ", app:L(self.ref.collected and "COLLECTED" or "NOT_COLLECTED"));
							local speciesName, speciesIcon, petType, companionID, tooltipSource, tooltipDescription  = C_PetJournal.GetPetInfoBySpeciesID(self.ref.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 self.ref.item.sourceID then
						GameTooltip:AddDoubleLine(app:L("SOURCE_ID"), tostring(self.ref.item.sourceID));
						GameTooltip:AddDoubleLine(" ", app:L(self.ref.collected and "COLLECTED" or "NOT_COLLECTED"));
					elseif not self.ref.groups and not self.ref.collected then
						GameTooltip:AddLine(app:L("SOURCE_ID_MISSING"), 1, 0, 0, 0.5);
					end
				elseif self.ref.instanceID then
					-- Contains information about an Instance (Raid or Dungeon)
					-- Is the player's character saved to this instance?
					if self.saved and self.saved.instanceID then
						GameTooltip:SetInstanceLockEncountersComplete(self.saved.instanceID);
						GameTooltip:AddLine(" ");
					end
					
					-- Show information about the instance
					GameTooltip:AddDoubleLine(self.ref.text or "???", app:L("INSTANCE"));
					GameTooltip:AddDoubleLine(app:L("INSTANCE_ID"), tostring(self.ref.instanceID));
				end
				
				-- Standalone Fields
				if self.ref.tierID then
					-- Contains information about the Expansion. (called "Tier" in the WoW API)
					GameTooltip:AddDoubleLine(EJ_GetTierInfo(self.ref.tierID), app:L("EXPANSION"));
					GameTooltip:AddDoubleLine(app:L("EXPANSION_ID"), tostring(self.ref.tierID));
					local tier = app.TierDB[self.ref.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 self.ref.difficultyID then
					-- Contains information about a Difficulty Setting (only useful information is the name really)
					GameTooltip:AddDoubleLine(GetDifficultyInfo(self.ref.difficultyID), app:L("DIFFICULTY"));
					GameTooltip:AddDoubleLine(app:L("DIFFICULTY_ID"), tostring(self.ref.difficultyID));
				end
				if self.ref.encounterID then
					-- Contains information about an Encounter. (TODO: Show the models for the bosses?)
					--local name, description, _, _, link = EJ_GetEncounterInfo(self.ref.encounterID);
					--self.link = link;	-- TODO: Clicking on this opens Adventure Guide?
					GameTooltip:AddDoubleLine(self.ref.text or "???", app:L("ENCOUNTER"));
					GameTooltip:AddDoubleLine(app:L("ENCOUNTER_ID"), tostring(self.ref.encounterID));
				end
				
				-- Miscellaneous fields
				if self.ref.description then GameTooltip:AddLine(self.ref.description, 1, 1, 1, 1); end
				if self.ref.icon then
					if self.ref.itemID then
						GameTooltipIcon:SetSize(64,64);
					else
						GameTooltipIcon:SetSize(96,96);
					end
					GameTooltipIcon.icon:SetTexture(self.ref.icon);
					GameTooltipIcon:Show();
				end
				if self.ref.mapID then
					GameTooltip:AddDoubleLine(app:L("MAP_ID"), tostring(self.ref.mapID));
					if self.ref.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 self.ref.text == mapName then
								app.print("Updated Map ID to #" .. tostring(mapID) .. " for " .. self.ref.text);
								self.ref.mapID = mapID;
								app:RefreshScrollbar();
							else
								GameTooltip:AddDoubleLine("Map ID (Current)", tostring(mapID));
								GameTooltip:AddLine(mapName);
							end
						end
					end
				end
				if not self.ref.item and self.ref.Lvl and self.ref.Lvl > 0 then
					GameTooltip:AddDoubleLine(app:L("REQUIRES_LEVEL"), tostring(self.ref.Lvl));
				end
				if self.ref.groups then GameTooltip:AddLine(app:L("CLICK_TO_EXPAND"), 1, 1, 1); end
			else
				-- No reference object, so let's display the raw data.
				GameTooltip:SetText(self.text:GetText());
			end
			GameTooltip:Show();
		end
	end);
	row:SetScript("OnLeave", function(self)
		if self.text 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(row)
	row.ref = nil;
	if row.back then row.back:Hide(); end
	if row.icon then row.icon:Hide(); end
	if row.saved then
		row.saved:Hide();
		row.saved.instanceID = nil;
	end
	if row.summary then row.summary:Hide(); end
	if row.text then row.text:Hide(); end
	row:Hide();
end
local function RowClickHandler(self)
	if self.ref then
		if IsModifiedClick("CHATLINK") then
			app.print(self.ref.link or self.text:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(self.ref.link or self.text:GetText());
		elseif self.ref.groups then
			self.ref.expanded = not self.ref.expanded;
			app:RefreshScrollbar(true);
		end
	else
		if IsModifiedClick("CHATLINK") then
			app.print(self.text:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(self.text:GetText());
		end
	end
end
local function SetRowData(row, data)
	if data and data.ref then
		if data.ref.item then
			if data.ref.item.link or (data.ref.item.refresh and data.ref.item:refresh()) then
				data.ref.icon = data.ref.item.texture;
				data.ref.text = data.ref.item.link;
				data.working = nil;
				data.row = nil;
			else
				data.row = row;
				if data.working then
					return true; -- More work is required.
				else
					data.working = true;
					app:Push(function()
						if data.row then return SetRowData(data.row, data); end
					end);
					return false;
				end
			end
		end
		if data.ref.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.back then
					row.back = row:CreateTexture(nil, "BACKGROUND");
					row.back:SetAllPoints(row);
					row.back:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight");
				end
				row.back:SetAlpha(data.back or 0.2);
				row.back:Show();
			end
			if data.ref.instanceID then
				local saved = app:GetPersonalDataSubMember("Saves", data.ref.text); -- TODO: Convert this to an int-based system.... maybe?
				if saved then
					if not row.saved then
						row.saved = row:CreateTexture(nil, "ARTWORK");
						row.saved:SetPoint("TOP");
						row.saved:SetPoint("BOTTOM");
						row.saved:SetWidth(row:GetHeight());
					end
					row.saved:SetTexture("Interface\\FriendsFrame\\StatusIcon-DnD");
					row.saved:SetPoint("RIGHT", leftmost, relative, x, 0);
					row.saved.instanceID = saved;
					row.saved:Show();
				end
			elseif data.ref.unobtainable then
				if not row.saved then
					row.saved = row:CreateTexture(nil, "ARTWORK");
					row.saved:SetPoint("TOP");
					row.saved:SetPoint("BOTTOM");
					row.saved:SetWidth(row:GetHeight());
				end
				local reason = app:L("UNOBTAINABLE_ITEM_REASONS")[data.ref.unobtainable or 1];
				row.saved:SetTexture(app:L("UNOBTAINABLE_ITEM_TEXTURES")[reason[1]] or "Interface\\FriendsFrame\\StatusIcon-DnD");
				row.saved:SetPoint("RIGHT", leftmost, relative, x, 0);
				row.saved.instanceID = nil;
				row.saved:Show();
			end
			if data.ref.icon then
				if not row.icon then
					row.icon = row:CreateTexture(nil, "ARTWORK");
					row.icon:SetPoint("TOP");
					row.icon:SetPoint("BOTTOM");
					row.icon:SetWidth(row:GetHeight());
				end
				row.icon:SetPoint("LEFT", leftmost, relative, x, 0);
				row.icon:SetTexture(data.ref.icon);
				row.icon:Show();
				leftmost = row.icon;
				relative = "RIGHT";
				x = 4;
			end
			if not row.text then
				row.text = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
				row.text:SetJustifyH("CENTER");
				row.text:SetPoint("TOP");
				row.text:SetPoint("BOTTOM");
			end
			row.text:SetPoint("LEFT", leftmost, relative, x, 0);
			row.text:SetText(data.ref.text);
			row.text:Show();
			leftmost = row.text;
			relative = "RIGHT";
			row:Show();
			row.ref = data.ref;
			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.ref.itemID and not data.ref.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(row)
	return row.collected < row.total;
end
function app.FilterItemClass(item)
	if item.class then
		local filters = app:GetPersonalDataSubMember("ItemClassFilters", item.class);
		if filters and not filters[item.subclass or 0] then return false; end
		
		-- If true, we need to also filter class requirements (if any)
		if item.classes then return table.contains(item.classes, app.Class); end
	end
	return true;
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;

-- Data Harvesting (these are for reference mostly)
function app:HarvestAppearanceData()
	if not self.refreshingAppearances then
		self.refreshingAppearances = true;
		--if true then return nil; end
		app:StartCoroutine(coroutine.create(function()
			-- While the player is in combat, wait for combat to end.
			while InCombatLockdown() do coroutine.yield(); end
		
			-- Cache temporary variables...
			app.print("Refreshing Appearance Data...");
			local yieldAmount = self:GetDataMember("yield", 50);
			local counter = 0;
			
			-- Cache the Global Containers
			local data, specials, special_divs = {}, {}, {};
			
			-- Run through all of the possible categories... (even those I known don't exist)
			for categoryID = 1,5000 do
				-- Cache all of the appearances for this category
				local appearances = C_TransmogCollection.GetCategoryAppearances(categoryID);
				if appearances then
					for appearanceIter = 1,#appearances do
						-- Cache the apperance container
						local appearance = appearances[appearanceIter];
						if appearance then
							-- Cache all the sources of the appearance
							local sources = C_TransmogCollection.GetAppearanceSources(appearance.visualID)
							if sources then
								for sourceIter = 1,#sources do
									local source = sources[sourceIter];
									if source then
										local itemID = source.itemID;
										local locations = C_TransmogCollection.GetAppearanceSourceDrops(source.sourceID);
										if locations then
											--[[
											local info = C_TransmogCollection.GetSourceInfo(source.sourceID);
											for locationIter=1,#locations do
												local location = locations[locationIter];
												if location.instance == "Siege of Orgrimmar" then
													self:SetDataMember("_Appearance", appearance);
													self:SetDataMember("_Instance", location);
													self:SetDataMember("_Source", source);
													self:SetDataMember("_Sources", sources);
													self:SetDataMember("_Location", location);
													self:SetDataMember("_Locations", locations);
													self:SetDataMember("_Info", info);
												end
												if location.instance == "Siege of Orgrimmar" then
													app.print("Source ID: " .. tostring(source.sourceID));
													for key,value in pairs(source) do
														app.print(" " .. key .. ": " .. tostring(value));
													end
													app.print("Appearance ID: " .. tostring(appearance.visualID));
													for key,value in pairs(appearance) do
														app.print(" " .. key .. ": " .. tostring(value));
													end
													app.print("Location: " .. location.instance);
													for key,value in pairs(location) do
														app.print(" " .. tostring(key) .. ": " .. tostring(value));
														if type(value) == "table" then
															app.print("  " .. tostring(#value) .. " Count");
															for i,v in pairs(value) do
																app.print("   " .. tostring(i) .. ": " .. tostring(v));
															end
														end
													end
													if info then
														app.print("Info: ");
														for key,value in pairs(info) do
															app.print(" " .. tostring(key) .. ": " .. tostring(value));
															if type(value) == "table" then
																app.print("  " .. tostring(#value) .. " Count");
																for i,v in pairs(value) do
																	app.print("   " .. tostring(i) .. ": " .. tostring(v));
																end
															end
														end
													end
												end
												
												local inst = data[location.instance];
												if inst == nil then
													inst = {};
													inst.groups = {};
													data[location.instance] = inst;
												end
												if #location.difficulties == 0 then
													-- Siege of Orgrimmar likes doing this... Sigh.
													table.insert(location.difficulties, "Unspecified");
												end
												for difficultyIter=1,#location.difficulties do
													-- Cache the difficulty of the location
													local difficulty = location.difficulties[difficultyIter]
													if difficulty then
														-- Group Classification
														difficulty = difficulty;
														local group = inst.groups[difficulty];
														if group == nil then
															group = {};
															group.groups = {};
															inst.groups[difficulty] = group;
														end
														
														-- Subgroup Classification
														local subgroup = group.groups[location.encounter];
														if subgroup == nil then
															subgroup = {};
															subgroup.items = {};
															group.groups[location.encounter] = subgroup;
														end
														
														-- If the sub group doesn't have a group for this visual, create one (this helps sort items share appearances)
														local visuals = subgroup.items[appearance.visualID];
														if visuals == nil then
															visuals = {};
															subgroup.items[appearance.visualID] = visuals;
															
															-- Add to the totals
															table.insert(visuals, itemID);
														elseif not table.contains(visuals, itemID) then
															-- Add to the totals
															table.insert(visuals, itemID);
														end
													end
												end
											end
											]]--
										else
											local type = TYPES[source.sourceType] or "Other";
											--[[
											app.print("Source ID: " .. tostring(source.sourceID) .. " " .. type);
											for key,value in pairs(source) do
												app.print(key .. ": " .. tostring(value));
											end
											for key,value in pairs(appearance) do
												app.print(key .. ": " .. tostring(value));
											end
											]]--
											
											-- Add the item information to the special category
											local inst = special_divs[type];
											if inst == nil then
												inst = {};
												inst.divs = {};
												inst.groups = {};
												--inst.expanded = true;
												special_divs[type] = inst;
											end
											
											-- Determine whether or not this item is known in the system. (if cached)
											local itemInfo, div = app:Harvest(1, 0, itemID), -1;
											if itemInfo then
												-- Oh cool, we have this cached. Let's sort it as we should.
												if itemInfo.Lvl and itemInfo.Lvl > 1 then
													div = itemInfo.Lvl + ((itemInfo.iLvl or 0) / 10000);	-- By level, then by iLvl
												elseif itemInfo.iLvl and itemInfo.iLvl > 0 then
													div = itemInfo.iLvl + 10000;	-- By iLevel
												else
													div = 0;
												end
											end
											
											local difficulty = (div > 0 and ((floor((div - 1) / 10) * 10) + 1)) or div;
											local group = inst.divs[difficulty];
											if group == nil then
												group = {};
												group.groups = {};
												--group.expanded = true;
												group.div = difficulty;
												inst.divs[difficulty] = group;
												if difficulty < 0 then
													group.text = "Not Yet Cached";
												elseif difficulty >= 10000 then
													-- Item Level Sorting. (this makes it so that it goes after Level in the sort order)
													difficulty = difficulty - 10000;
													group.text = "iLvl " .. tostring(difficulty) .. " - " .. tostring(difficulty + 9);
												else
													group.text = "Level " .. tostring(difficulty) .. " - " .. tostring(difficulty + 9);
												end
											end
											
											-- If the sub group doesn't have a group for this visual, create one (this helps sort items share appearances)
											local item = { ["itemID"] = itemID, ["div"] = div };
											table.insert(group.groups, item);
										end
									end
								end
							else
								app.print("Item ID #" .. itemID .. " has no source?? WHAT? Report this to Crieve-Sargeras immediately!");
							end
						end
						
						-- Increment the Counter, we need to yield a bit for data to refresh.
						--counter = counter + 1
						--if counter % yieldAmount == 0 then coroutine.yield() end
					end
				end
			end
			
			-- Determine if we can get information from AtlasLoot. (this addon has practically everything, we just need to link stuff)
			--[[
			local AtlasLoot = _G.AtlasLoot
			if AtlasLoot and AtlasLoot.ItemDB and AtlasLoot.ItemDB.Storage then
				data = AtlasLoot.ItemDB.Storage["AtlasLoot_BurningCrusade"]["HCHellfireRamparts"];
				if data then
					self:SetDataMember("AtlasLoot", specials);
				end
			end
			]]--
			
			-- Post Process the Special's tab's Divs.
			for t, group in pairs(special_divs) do
				for div,subgroup in pairs(group.divs) do
					table.insert(group.groups, subgroup);
					table.sort(subgroup.groups, function(a, b) return a.div < b.div; end);
				end
				group.divs = nil;
				if #group.groups > 0 then
					table.sort(group.groups, function(a, b) return a.div < b.div; end);
				end
				group.text = t;
				table.insert(specials, group);
				--coroutine.yield();
			end
			table.sort(specials, function(a, b) return a.text < b.text; end);
			
			self:SetTempDataMember("Special", specials)
			
			-- Update the Scrollbar and flush garbage
			self:RefreshScrollbar();
			collectgarbage();
			self.refreshingAppearances = false;
			app.print("Done refreshing Appearance data.");
		end));
	end
end
function app:HarvestIllusionData()
	if not self.refreshingIllusions then
		self.refreshingIllusions = true;
		app:StartCoroutine(coroutine.create(function()
			-- While the player is in combat, wait for combat to end.
			while InCombatLockdown() do coroutine.yield(); end
			
			-- Cache temporary variables...
			app.print("Refreshing Illusion Data...");
			
			-- Go through all of the illusions and cache information about their sources.
			local illusionData, itemData = C_TransmogCollection.GetIllusions(), {};
			for illusionID, illusion in ipairs(illusionData) do
				-- Check to see if we have any Information about the source??
				if illusion.sourceID then
					local sourceInfo = {C_TransmogCollection.GetIllusionSourceInfo(illusion.sourceID)};
					if sourceInfo then
						local myIllusionData = {};
						itemData[illusionID] = myIllusionData;
						myIllusionData.text = sourceInfo[2];
						myIllusionData.sourceID = illusion.sourceID;
						myIllusionData.visualID = illusion.visualID;
						if illusion.sourceText then myIllusionData.sourceText = illusion.sourceText; end
					end
				end
			end
			self:SetDataMember("IllusionDB", itemData);
			
			-- Update the Scrollbar and flush garbage
			self:RefreshScrollbar();
			collectgarbage();
			self.refreshingIllusions = false;
			app.print("Done refreshing Illusion data.");
		end));
	end
end
function app:HarvestInstanceData()
	if not self.processedInstance then
		self.processedInstance = true;
		--if true then return nil; end
		app:StartCoroutine(coroutine.create(function()
			-- While the player is in combat, wait for combat to end.
			while InCombatLockdown() do coroutine.yield(); end
			
			-- Cache temporary variables...
			app.print("Harvesting Instance Data...");
			local yieldAmount = self:GetDataMember("yield", 50);
			local counter = 0;
			local data = self:GetTempDataMember("Instances", {});
			
			-- Iterate through all of the tiers
			for tierID=1,EJ_GetNumTiers() do
				-- Create a data cache for this tier.
				local tierData = {};
				tierData.groups = {};
				tierData.tierID = tierID;
				--tierData.expanded = true; -- REMOVE AFTER TESTING
				tierData.text = EJ_GetTierInfo(tierID);
				data[tierID] = tierData;
				
				-- Select the Tier
				EJ_SelectTier(tierID);
				
				-- Cache information about all of the raids in this tier.
				local showRaid, index = true, 1;
				local instanceID, name, description, _, _, _, backgroundImage, mapID, link, shouldShowDifficulties = EJ_GetInstanceByIndex(index, showRaid);
				while instanceID do
					-- Cache information about the instance.
					local groupData = {};
					groupData.groups = {};
					EJ_SelectInstance(instanceID);
					table.insert(tierData.groups, groupData);
					--groupData.expanded = true; -- REMOVE AFTER TESTING
					groupData.instanceID = instanceID;
					groupData.icon = backgroundImage;
					groupData.text = name;
					groupData.mapID = mapID;
					
					-- todo: get difficulties
					if shouldShowDifficulties then
						for i=1,#EJ_DIFFICULTIES do
							local entry = EJ_DIFFICULTIES[i];
							if EJ_IsValidInstanceDifficulty(entry.difficultyID) then
								EJ_SetDifficulty(entry.difficultyID);
								
								-- Cache information about this difficulty
								local difficultyData = {};
								difficultyData.groups = {};
								--difficultyData.expanded = true; -- REMOVE AFTER TESTING
								difficultyData.difficultyID = entry.difficultyID;
								difficultyData.text = GetDifficultyInfo(entry.difficultyID);
								
								-- Cache information about the encounters in the instance
								local bossIndex = 1;
								name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
								while bossID do
									EJ_SelectEncounter(bossID);
									local numLoot = EJ_GetNumLoot();
									if numLoot > 0 then
										local encounterData = {};
										encounterData.groups = {};
										table.insert(difficultyData.groups, encounterData);
										encounterData.encounterID = bossID;
										encounterData.expanded = true;
										encounterData.text = name;
										
										-- Cache information about the items that drop in the instance for this difficulty
										for itemIndex=1,numLoot do
											local itemData, itemID, _, _, _, _, _, itemLink = {}, EJ_GetLootInfoByIndex(itemIndex);
											app:HarvestItemLink(itemLink);
											itemData.itemID = itemID;
											table.insert(encounterData.groups, itemData);
										end
									end
									
									-- It is better to yield here because if you try above, if a different addon is processing items, it'll break the operation.
									--coroutine.yield();

									-- Move on to the next encounter
									bossIndex = bossIndex + 1;
									name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
								end
								
								-- If this boss actually had information, then add it to the list.
								if #difficultyData.groups > 0 then table.insert(groupData.groups, difficultyData); end
							end
						end
					end
					
					-- Correct an issue with any raid that does not satisfy the difficulties tier.
					local groupCount = #groupData.groups;
					if groupCount == 0 then
						-- Cache information about this difficulty
						local difficultyData = groupData;
						
						-- Cache information about the encounters in the instance
						local bossIndex = 1;
						name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
						while bossID do
							EJ_SelectEncounter(bossID);
							local numLoot = EJ_GetNumLoot();
							if numLoot > 0 then
								local encounterData = {};
								encounterData.groups = {};
								table.insert(difficultyData.groups, encounterData);
								encounterData.encounterID = bossID;
								encounterData.expanded = true;
								encounterData.text = name;
								
								-- Cache information about the items that drop in the instance for this difficulty
								for itemIndex=1,numLoot do
									local itemData, itemID, _, _, _, _, _, itemLink = {}, EJ_GetLootInfoByIndex(itemIndex);
									app:HarvestItemLink(itemLink);
									itemData.itemID = itemID;
									table.insert(encounterData.groups, itemData);
								end
							end
							
							-- It is better to yield here because if you try above, if a different addon is processing items, it'll break the operation.
							--coroutine.yield();

							-- Move on to the next encounter
							bossIndex = bossIndex + 1;
							name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
						end
					elseif groupCount == 1 then groupData.groups = groupData.groups[1].groups; end
					
					-- Move on to the next instance
					index = index + 1;
					instanceID, name, description, _, _, _, backgroundImage, mapID, link, shouldShowDifficulties = EJ_GetInstanceByIndex(index, showRaid);
				end
				
				-- Cache information about all of the dungeons in this tier.
				showRaid, index = false, 1;
				instanceID, name, description, _, _, _, backgroundImage, mapID, link, shouldShowDifficulties = EJ_GetInstanceByIndex(index, showRaid);
				while instanceID do
					-- Cache information about the instance.
					local groupData = {};
					groupData.groups = {};
					--groupData.expanded = true; -- REMOVE AFTER TESTING
					EJ_SelectInstance(instanceID);
					table.insert(tierData.groups, groupData);
					groupData.instanceID = instanceID;
					groupData.icon = backgroundImage;
					groupData.text = name;
					groupData.mapID = mapID;
					
					-- todo: get difficulties
					if shouldShowDifficulties then
						for i=1,#EJ_DIFFICULTIES do
							local entry = EJ_DIFFICULTIES[i];
							if EJ_IsValidInstanceDifficulty(entry.difficultyID) then
								EJ_SetDifficulty(entry.difficultyID);
								
								-- Cache information about this difficulty
								local difficultyData = {};
								difficultyData.groups = {};
								--difficultyData.expanded = true; -- REMOVE AFTER TESTING
								difficultyData.difficultyID = entry.difficultyID;
								difficultyData.text = GetDifficultyInfo(entry.difficultyID);
								
								-- Cache information about the encounters in the instance
								local bossIndex = 1;
								name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
								while bossID do
									EJ_SelectEncounter(bossID);
									local numLoot = EJ_GetNumLoot();
									if numLoot > 0 then
										local encounterData = {};
										encounterData.groups = {};
										table.insert(difficultyData.groups, encounterData);
										encounterData.encounterID = bossID;
										encounterData.expanded = true;
										encounterData.text = name;
										
										-- Cache information about the items that drop in the instance for this difficulty
										for itemIndex=1,numLoot do
											local itemData, itemID, _, _, _, _, _, itemLink = {}, EJ_GetLootInfoByIndex(itemIndex);
											app:HarvestItemLink(itemLink);
											itemData.itemID = itemID;
											table.insert(encounterData.groups, itemData);
										end
									end

									-- Move on to the next encounter
									bossIndex = bossIndex + 1;
									name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
								end
								
								-- If this boss actually had information, then add it to the list.
								if #difficultyData.groups > 0 then table.insert(groupData.groups, difficultyData); end
							end
						end
					end
					
					-- Correct an issue with any raid that does not satisfy the difficulties tier.
					local groupCount = #groupData.groups;
					if groupCount == 0 then
						-- Cache information about this difficulty
						local difficultyData = groupData;
						
						-- Cache information about the encounters in the instance
						local bossIndex = 1;
						name, description, bossID, rootSectionID, link = EJ_GetEncounterInfoByIndex(bossIndex);
						while bossID do
							EJ_SelectEncounter(bossID);
							local numLoot = EJ_GetNumLoot();
							if numLoot > 0 then
								local encounterData = {};
								encounterData.groups = {};
								table.insert(difficultyData.groups, encounterData);
								encounterData.encounterID = bossID;
								encounterData.expanded = true;
								encounterData.text = name;
								
								-- Cache information about the items that drop in the instance for this difficulty
								for itemIndex=1,numLoot do
									local itemData, itemID, _, _, _, _, _, itemLink = {}, EJ_GetLootInfoByIndex(itemIndex);
									app:HarvestItemLink(itemLink);
									itemData.itemID = itemID;
									table.insert(encounterData.groups, itemData);
								end
							end

							-- Move on to the next encounter
							bossIndex = bossIndex + 1;
							name, description, bossID, rootSectionID = EJ_GetEncounterInfoByIndex(bossIndex);
						end
					elseif groupCount == 1 then groupData.groups = groupData.groups[1].groups; end
					
					-- Move on to the next instance
					index = index + 1;
					instanceID, name, description, _, _, _, backgroundImage, mapID, link, shouldShowDifficulties = EJ_GetInstanceByIndex(index, showRaid);
				end
			end
			
			-- Update the Scrollbar and flush garbage
			self:RefreshScrollbar();
			collectgarbage();
			app.print("Done harvesting Instance data.");
		end));
	end
end

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.ref = {
					expanded = true,
					text = app:L("TOTAL_COMPLETION")
				};
				allData.visible = true;
				allData.collected = 0;
				allData.total = 0;
				
				-- NOTE: These are REFS, not ROW DATA OBJECTS.
				local groups = {};
				local instanceData = {};
				instanceData.expanded = true;
				instanceData.text = app:L("INSTANCES")
				instanceData.groups = self:GetTempDataMember("Instances", {});
				table.insert(groups, instanceData);
				
				local specialData = {};
				specialData.expanded = true;
				specialData.text = app:L("MISCELLANEOUS");
				specialData.groups = self:GetTempDataMember("Special", {});
				table.insert(groups, specialData);
				
				-- Source DB
				local db = {};
				db.expanded = true;
				db.text = "Dungeons & Raids";
				db.groups = app.SourceDB[1];
				table.insert(groups, db);
				
				-- Everything
				--[[
				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);
				self:UpdateGroups(allData, groups, 0, 0);
				missingData.groups = {};
				app:SetTempDataMember("CACHE", allData);
			end
			
			-- Post Process all of the Data
			local data = {};
			app:SetTempDataMember("ROW_DATA", data);
			if allData.collected < allData.total then
				self:ProcessGroup(data, allData, 0, 0);
			else
				tinsert(data, allData);
				if allData.total > 1 then
					tinsert(data, {["indent"]=2,["text"] = app:L("CONGRATULATIONS")});
				else
					tinsert(data, {["indent"]=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.ref and parent.ref.mapID and parent.ref.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 (not parent.ref or parent.ref.expanded) then
			indent = indent + 1;
			back = parent.back * 0.5;
			for i, 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
				local row = {};
				row.ref = group;
				row.visible = true;
				tinsert(parent.rows, row);
				
				-- Get additional information, if necessary.
				if group.itemID then
					local missingSource = false;
					if not row.ref.item then row.ref.item = app:Harvest(instanceID, difficultyID, group.itemID); end
					row.ref.unobtainable = app:L("UNOBTAINABLE_ITEMS")[group.itemID];
					if row.ref.item.mountID then
						row.ref.collected = self:GetDataSubMember("CollectedMounts", row.ref.item.mountID);
						if not row.ref.collected then
							row.ref.collected = app:GetMountCollected(row.ref.item.mountID);
							self:GetDataSubMember("CollectedMounts", row.ref.item.mountID, row.ref.collected);
						end
					elseif row.ref.item.illusionID then
						row.ref.collected = self:GetDataSubMember("CollectedIllusions", row.ref.item.illusionID);
						if not row.ref.collected then
							row.ref.collected = app:GetIllusionCollected(row.ref.item.illusionID);
							self:SetDataSubMember("CollectedIllusions", row.ref.item.illusionID, row.ref.collected);
						end
					elseif row.ref.item.isToy then -- Toys    or (row.ref.item.class == 4 and row.ref.item.subclass == 0)
						row.ref.collected = self:GetDataSubMember("CollectedToys", group.itemID);
						if not row.ref.collected then
							row.ref.collected = C_ToyBox.IsToyUsable(group.itemID);
							self:SetDataSubMember("CollectedToys", group.itemID, row.ref.collected);
						end
					elseif row.ref.item.class == 15 and row.ref.item.subclass == 2 then -- Companion Pets
						row.ref.collected = self:GetDataSubMember("CollectedPets", group.itemID);
						if not row.ref.collected then
							if row.ref.item.speciesID then
								local numCollected, limit = C_PetJournal.GetNumCollectedInfo(row.ref.item.speciesID);
								row.ref.collected = numCollected > 0;
								self:SetDataSubMember("CollectedPets", group.itemID, row.ref.collected);
							else
								missingSource = true;
								row.ref.item.speciesID = 0;
							end
						end
					elseif row.ref.item.sourceID then
						row.ref.collected = self:GetDataSubMember("Collected", row.ref.item.sourceID);
						if not row.ref.collected then
							local sourceInfo = C_TransmogCollection.GetSourceInfo(row.ref.item.sourceID);
							if sourceInfo then
								row.ref.collected = app.ItemSourceFilter(sourceInfo);
								self:SetDataSubMember("Collected", row.ref.item.sourceID, row.ref.collected);
							end
						end
					elseif not row.ref.groups then missingSource = true; end
					
					-- If the 'can equip' filter says true
					if app.FilterItemClass(row.ref.item) then
						if row.ref.collected then
							row.visible = app.CollectedItemVisibilityFilter(row);
						else
							row.visible = app.MissingItemVisibilityFilter(row);
						end
						
						-- Increment the parent row's totals. (but only if it is visible)
						if not group.groups then
							parent.total = (parent.total or 0) + 1;
							if row.ref.collected then parent.collected = (parent.collected or 0) + 1; end
						end
						
						-- Items without sources list
						if missingSource and row.visible then
							table.insert(self:GetTempDataMember("Missing"), row);
						end
					else
						row.visible = false;
					end
				elseif group.instanceID then
					row.instanceID = group.instanceID;
				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
					row.rows = {};
					if group.item and group.item.sourceID then
						row.total = 1;
						row.collected = (group.collected and 1) or 0;
					else
						row.total = 0;
						row.collected = 0;
					end
					self:UpdateGroups(row, group.groups, group.instanceID or instanceID, group.difficultyID or difficultyID);
					parent.collected = (parent.collected or 0) + row.collected;
					parent.total = (parent.total or 0) + row.total;
					row.visible = row.total > 0 and app.GroupVisibilityFilter(row);
				elseif group.rows then
					row.rows = group.rows;
					row.total = #row.rows;
					row.collected = 0;
					parent.collected = (parent.collected or 0) + row.collected;
					parent.total = (parent.total or 0) + row.total;
					row.visible = row.total > 0;
				end
			end
		end
	end
end


-- Refresh Button
--[[
local button = CreateFrame("BUTTON", nil, app, "UIPanelButtonTemplate");
app.RefreshButton = button;
button:SetPoint("TOPLEFT", container.rows[#container.rows], "BOTTOMLEFT", 0, 0);
button:SetPoint("BOTTOMRIGHT", scrollbar, "BOTTOMLEFT", 0, -16);
button:SetScript("OnClick", function()
	app:HarvestAppearanceData();
	app:HarvestIllusionData();
	app:HarvestInstanceData();
end);
]]--
-- Register Events required at the start
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.
	app.Class = select(2, UnitClass("player"));
	app.Level = UnitLevel("player");
	
	-- 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
	
	-- Create the Settings Menu
	app.Settings = app:CreateSettingsMenu();
	app.print(format(app:L("LOADING"), GetAddOnMetadata("AllTheThings", "Version")));
	
	-- Set the scrollbar data
	app:RefreshSaves();	-- Since this is index-based, it can be wrong when you login.
	app:RefreshScrollbar(false, true);
	scrollbar:SetValue(1);
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