--------------------------------------------------------------------------------
--                        A L L   T H E   T H I N G S                         --
--------------------------------------------------------------------------------
--               Copyright 2017 Dylan Fortune (Crieve-Sargeras)               --
--------------------------------------------------------------------------------
AllTheThings = CreateFrame("FRAME", "AllTheThings", UIParent);
local function HandleEvents(self, e, ...) (self.events[e] or tostringall)(...); end
local app = AllTheThings;	-- Create a local (non global) reference
app.events = {};
local backdrop = {
	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.DisplayName = "AllTheThings";
app:SetScript("OnEvent", HandleEvents);
app:SetPoint("BOTTOMLEFT", UIParent, "TOPLEFT", 0, 0);
app:SetSize(1, 1);
app:Hide();

-- Game Tooltip Icon
local GameTooltipIcon = CreateFrame("FRAME", nil, GameTooltip);
GameTooltipIcon:SetBackdrop(backdrop);
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();

-- Data Lib
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 function SetDataMember(member, data)
	AllTheThingsAD[member] = data;
end
local function GetDataMember(member, default)
	if AllTheThingsAD[member] == nil then AllTheThingsAD[member] = default; end
	return AllTheThingsAD[member];
end
local function SetTempDataMember(member, data)
	AllTheThingsTempData[member] = data;
end
local function GetTempDataMember(member, default)
	if AllTheThingsTempData[member] == nil then AllTheThingsTempData[member] = default; end
	return AllTheThingsTempData[member];
end
local function SetPersonalDataMember(member, data)
	AllTheThingsPCD[member] = data;
end
local function GetPersonalDataMember(member, default)
	if AllTheThingsPCD[member] == nil then AllTheThingsPCD[member] = default; end
	return AllTheThingsPCD[member];
end
local function SetDataSubMember(member, submember, data)
	if AllTheThingsAD[member] then AllTheThingsAD[member][submember] = data; end
end
local function GetDataSubMember(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
local function SetTempDataSubMember(member, submember, data)
	if AllTheThingsTempData[member] then
		AllTheThingsTempData[member][submember] = data;
	end
end
local function GetTempDataSubMember(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
local function SetPersonalDataSubMember(member, submember, data)
	if AllTheThingsPCD[member] then AllTheThingsPCD[member][submember] = data; end
end
local function GetPersonalDataSubMember(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
local function SetDataSubSubMember(member, submember, subsubmember, data)
	if AllTheThingsAD[member] and AllTheThingsAD[member][submember] then AllTheThingsAD[member][submember][subsubmember] = data; end
end
local function GetDataSubSubMember(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
local function SetPersonalDataSubSubMember(member, submember, subsubmember, data)
	if AllTheThingsPCD[member] and AllTheThingsPCD[member][submember] then AllTheThingsPCD[member][submember][subsubmember] = data; end
end
local function GetPersonalDataSubSubMember(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
local function SetTempDataSubSubMember(member, submember, subsubmember, data)
	if AllTheThingsTempData[member] and AllTheThingsTempData[member][submember] then AllTheThingsTempData[member][submember][subsubmember] = data; end
end
local function GetTempDataSubSubMember(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

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

-- audio lib
local function PlayFanfare()
	if GetDataMember("PlayFanfare", true) then
		-- Play a random fanfare from the locale table
		local t = 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
local function PlayRemoveSound()
	if GetDataMember("PlayRemoveSound", true) then
		-- Play a random fanfare from the locale table
		local t = 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);
local 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);
local function Colorize(str, color)
	return "|c" .. color .. str .. "|r";
end
local function RGBToHex(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
local 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 RGBToHex(r * 255, g * 255, b * 255);
end
CS:Hide();

-- Source ID Harvesting Lib
local DressUpModel = CreateFrame('DressUpModel');
local inventorySlotsMap = {	-- Taken directly from CanIMogIt (Thanks!)
    ["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,
};
local function GetSourceID(itemLink)
    if IsDressableItem(itemLink) then
		local itemID, _, _, slotName = GetItemInfoInstant(itemLink);
		if slotName then
			local slots = inventorySlotsMap[slotName];
			if slots then
				DressUpModel:SetUnit('player');
				DressUpModel:Undress();
				for i, slot in pairs(slots) do
					DressUpModel:TryOn(itemLink, slot);
					local sourceID = DressUpModel:GetSlotTransmogSources(slot);
					if sourceID ~= 0 then return sourceID, true; end
				end
			end
		end
		return nil, true;
	end
	return nil, false;
end

-- Quest Name Harvesting Lib (http://www.wowinterface.com/forums/showthread.php?t=46934)
local QuestHarvester = CreateFrame("GameTooltip", "AllTheThingsQuestHarvester", UIParent, "GameTooltipTemplate");
local QuestTitleFromID = setmetatable({}, { __index = function(t, id)
	QuestHarvester:SetOwner(UIParent, "ANCHOR_NONE");
	QuestHarvester:SetHyperlink("quest:"..id);
	local title = AllTheThingsQuestHarvesterTextLeft1:GetText();
	QuestHarvester:Hide()
	if title and title ~= RETRIEVING_DATA then
		t[id] = title
		return title
	end
end })

-- Item Information Lib
local function 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();
	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
local function GetMountInfo(spellID)	-- NOTE: MountID is ACTUALLY the SpellID. Addon developers: Do not confuse this as I did, mountID can be different per spec/faction.
	local cache = GetTempDataMember("MOUNT_SPELLID_TO_MOUNTID");
	if not cache then
		-- Let's build the cache.
		local collected = GetDataMember("CollectedMounts", {});
		cache = {};
		
		local mountIDs = C_MountJournal.GetMountIDs();
		for i,mountID in ipairs(mountIDs) 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
		
		--[[
		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.
		SetTempDataMember("MOUNT_SPELLID_TO_MOUNTID", cache);
	end
	
	-- Return the mount information
	return C_MountJournal.GetMountInfoByID(cache[spellID]);
end
local function GetMountCollected(spellID)
	return select(11, GetMountInfo(spellID));
end
local function ItemRefreshAll(self)
	-- Call to GetItemInfo to harvet the data about the item.
	local name, link, quality, iLevel, reqLevel, itemClassString, itemSubClassString, 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.texture = texture;
		
		-- Harvest the Source ID. (shortened to "s")
		if not self.s or self.s == 0 then
			self.s, dressable = GetSourceID(link);
			if (not self.s or self.s == 0) 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.
		SetDataMember("ItemDB", GetTempDataMember("ItemDB"));
		return true;
	end
	return false;
end
local function ItemRefreshLink(self)
	local name, link, quality, iLevel, reqLevel, itemClassString, itemSubClassString, maxStack, equipSlot, texture, vendorPrice, itemClassID, itemSubClassID = GetItemInfo(self.itemLink);
	if link then
		-- Harvest the Source ID. (shortened to "s")
		if not self.s or self.s == 0  then
			self.s, dressable = GetSourceID(link);
			if self.s or self.s == 0  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.
				SetDataMember("ItemDB", GetTempDataMember("ItemDB"));
			elseif dressable then
				return false;
			end
		end
		
		-- You might look at this and ask yourself, "What is Crieve thinking?"
		-- The answer is, "metatables". If the base item already has this information, 
		-- there's no reason to duplicate it in memory. Also, if you give me the saved data
		-- file, I can update my database to include more accurate information. :)
		if self.texture ~= texture then self.texture = texture; end
		if self.Lvl ~= reqLevel then self.Lvl = reqLevel; end
		--if self.iLvl ~= iLevel then self.iLvl = iLevel; end
		if not self.f then
			if self.class ~= itemClassID then self.class = itemClassID; end
			if self.subclass ~= itemSubClassID then self.subclass = itemSubClassID; end
		end
		
		-- Set the link (this part is always dynamic)
		self.link = link;
		self.refresh = nil;
		self.itemLink = nil;
		return true;
	end
	return false;
end
local function 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 = GetTempDataSubSubMember("ItemDB", difficultyID, itemID);
	if not itemRef then
		-- Cache the base ItemDB table for this item / difficulty pair.
		local item = app.ItemDB[difficultyID] and app.ItemDB[difficultyID][itemID];
		if item then
			-- Create a local container to house useful information about the item.
			itemRef = item;--app.instanciate(baseItem);
			itemRef.refresh = ItemRefreshLink;
		else
			local baseItem = app.ItemDB[0][itemID];
			if baseItem then
				-- Create a local container to house useful information about the item.
				itemRef = app.instanciate(baseItem);
				itemRef.refresh = ItemRefreshLink;
			else
				-- Create a local container to house useful information about the item.
				itemRef = {};
				itemRef.refresh = ItemRefreshAll;
			end
		end
		
		-- Build the Item Link based on the instance and difficulty.
		-- NOTE: Someday, build the bonus ID value right into the instance difficulty object.
		if not itemLink then
			local bonusIDGroup = 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.s = 0; -- uncomment to force the sourceID values to rebuild themselves when you scroll the collections window
		itemRef.itemID = itemID;
		itemRef.itemLink = itemLink;
		SetTempDataSubSubMember("ItemDB", difficultyID, itemID, itemRef);
	end
	return itemRef;
end
local function 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 Harvest(0, tonumber(difficultyID or "0"), tonumber(itemID or "0"), itemLink);
	end
end

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

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

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

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

-- Encounter Lib
app.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;
	setmetatable(t, app.BaseEncounter);
	return t;
end

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

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

local function 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 = 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;
		SetDataMember("Position", math.deg(math.atan2(x, w)));
		self:Raise();
		self:update();
	end

	-- Register for Frame Events
	button:SetScript("OnDragStart", function(self)
		self:SetScript("OnUpdate", update);
	end);
	button:SetScript("OnDragStop", function(self)
		self:SetScript("OnUpdate", nil);
	end);
	button:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_LEFT");
		GameTooltip:SetText(app.DisplayName);
		GameTooltip:AddLine(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)
			ShowInterfaceOptions();
		else
			-- Left Button Toggles the App
			app:ToggleWindow("Prime");
		end
	end);
	button:update();
	button:Show();
	return button;
end
local function CreateSettingsMenu()
	local name = app:GetName();
	local frame = CreateFrame("FRAME", name .. "-Settings", UIParent);
	frame.name = L("TITLE_OPTIONS");
	InterfaceOptions_AddCategory(frame);
	
	frame.Title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge");
    frame.Title:SetPoint("TOPLEFT", 16, -16)
    frame.Title:SetText(L("TITLE_OPTIONS"));
	
	frame.Version = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
    frame.Version:SetPoint("TOPRIGHT", -16, -16)
    frame.Version:SetText(GetAddOnMetadata("AllTheThings", "Version"));
	
	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(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(GetDataMember("ShowMinimapButton"));
	frame.ShowMinimapButton:SetScript("OnClick", function(self)
		local checked = self:GetChecked();
		SetDataMember("ShowMinimapButton", checked);
		if (checked == true) then
			if not app.Minimap then app.Minimap = 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(GetDataMember("ShowCompletedGroups"));
	frame.ShowCompletedGroups:SetScript("OnClick", function(self)
		SetDataMember("ShowCompletedGroups", self:GetChecked());
		if self:GetChecked() then
			app.GroupVisibilityFilter = app.NoFilter;
		else
			app.GroupVisibilityFilter = app.FilterGroupsByCompletion;
		end
		app:RefreshData();
	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(GetDataMember("ShowCollectedItems"));
	frame.ShowCollectedItems:SetScript("OnClick", function(self)
		SetDataMember("ShowCollectedItems", self:GetChecked());
		if self:GetChecked() then
			app.CollectedItemVisibilityFilter = app.NoFilter;
		else
			app.CollectedItemVisibilityFilter = app.Filter;
		end
		app:RefreshData();
	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(GetDataMember("ShowMissingItems"));
	frame.ShowMissingItems:SetScript("OnClick", function(self)
		SetDataMember("ShowMissingItems", self:GetChecked());
		if self:GetChecked() then
			app.MissingItemVisibilityFilter = app.NoFilter;
		else
			app.MissingItemVisibilityFilter = app.Filter;
		end
		app:RefreshData();
	end);
	
	frame.UniqueAppearances = CreateFrame("CheckButton", name .. "-UniqueAppearances", frame, "InterfaceOptionsCheckButtonTemplate");
	frame.UniqueAppearances:SetPoint("TOPLEFT", frame.ShowMissingItems, "BOTTOMLEFT", 10, 2);
	frame.UniqueAppearances.Label = _G[frame.UniqueAppearances:GetName() .. "Text"];
	frame.UniqueAppearances.Label:SetText("Filter by Appearance ID rather than by Source ID");
	frame.UniqueAppearances.Label:SetWidth(200);
	frame.UniqueAppearances:SetChecked(GetDataMember("UniqueAppearances"));
	frame.UniqueAppearances:SetScript("OnClick", function(self)
		SetDataMember("UniqueAppearances", self:GetChecked());
		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:RefreshData(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(GetDataMember("PlayFanfare"));
	frame.PlayFanfare:SetScript("OnClick", function(self)
		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(GetDataMember("PlayRemoveSound"));
	frame.PlayRemoveSound:SetScript("OnClick", function(self)
		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(GetDataMember("AutoRefreshSaves"));
	frame.AutoRefreshSaves:SetScript("OnClick", function(self)
		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(GetDataMember("FilterGroupsByLevel"));
	frame.FilterGroupsByLevel:SetScript("OnClick", function(self)
		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:RefreshData();
	end);

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

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

	-- Item Filters
	local itemFilters = GetPersonalDataMember("ItemFilters");
	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(L("FILTERS"));
	local lastFilter = frame.FiltersTitle;
	
	-- "Armor Types" and "Non-Equipment Types"
	local itemFilterNames = L("FILTER_ID_TYPES");
	for itemClassID,filters in pairs({ [4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }) 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:SetScript("OnClick", function(self)
			for j, f in ipairs(self.groups) do
				local checked = not itemFilters[f.filter];
				itemFilters[f.filter] = checked;
				f:SetChecked(checked);
			end
			self:SetChecked(false);
			app:RefreshData();
		end);
		lastFilter = itemClassFilter;
		for j, filter in ipairs(filters) do
			local itemFilter = CreateFrame("CheckButton", itemClassFilter:GetName() .. "-" .. filter, frame, "InterfaceOptionsCheckButtonTemplate");
			itemFilter:SetPoint("LEFT", frame.ShowMissingItems, "LEFT", 10, 0);
			itemFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
			itemFilter.filter = filter;
			itemFilter.Label = _G[itemFilter:GetName() .. "Text"];
			itemFilter.Label:SetText(itemFilterNames[filter] .. " (" .. filter .. ")");
			itemFilter.Label:SetWidth(200);
			lastFilter = itemFilter;
			tinsert(itemClassFilter.groups, itemFilter);
			itemFilter:SetChecked(itemFilters[filter]);
			itemFilter:SetScript("OnClick", function(self)
				itemFilters[self.filter] = self:GetChecked();
				app:RefreshData();
			end);
		end
	end
	
	-- The "Non-Equipment Types Title" (not actually a title, this is an anchor reference. :) )
	frame.NonEquipmentTypesTitle = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal");
    frame.NonEquipmentTypesTitle:SetPoint("LEFT", frame.PreferencesTitle, "LEFT", 0, 0);
    frame.NonEquipmentTypesTitle:SetPoint("TOP", lastFilter, "BOTTOM", 0, -10);
	
	-- "Weapons Types"
	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:SetScript("OnClick", function(self)
		for i, f in ipairs(self.groups) do
			local checked = not itemFilters[f.filter];
			itemFilters[f.filter] = checked;
			f:SetChecked(checked);
		end
		self:SetChecked(false);
		app:RefreshData();
	end);
	
	lastFilter = weaponFilters;
	for i, filter in ipairs({ 20, 21, 22, 23, 24, 25, 26, 36, 35 }) do
		local itemFilter = CreateFrame("CheckButton", weaponFilters:GetName() .. "-" .. filter, frame, "InterfaceOptionsCheckButtonTemplate");
		itemFilter:SetPoint("LEFT", weaponFilters, "LEFT", 10, 0);
		itemFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		itemFilter.filter = filter;
		itemFilter.Label = _G[itemFilter:GetName() .. "Text"];
		itemFilter.Label:SetText(itemFilterNames[filter] .. " (" .. filter .. ")");
		itemFilter.Label:SetWidth(200);
		lastFilter = itemFilter;
		tinsert(weaponFilters.groups, itemFilter);
		itemFilter:SetChecked(itemFilters[filter]);
		itemFilter:SetScript("OnClick", function(self)
			itemFilters[self.filter] = self:GetChecked();
			app:RefreshData();
		end);
	end
	
	lastFilter = weaponFilters;
	for i, filter in ipairs({ 27, 28, 29, 30, 31, 32, 33, 34 }) do
		local itemFilter = CreateFrame("CheckButton", weaponFilters:GetName() .. filter, frame, "InterfaceOptionsCheckButtonTemplate");
		itemFilter:SetPoint("LEFT", weaponFilters, "LEFT", 210, 0);
		itemFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		itemFilter.filter = filter;
		itemFilter.Label = _G[itemFilter:GetName() .. "Text"];
		itemFilter.Label:SetText(itemFilterNames[filter] .. " (" .. filter .. ")");
		itemFilter.Label:SetWidth(200);
		lastFilter = itemFilter;
		tinsert(weaponFilters.groups, itemFilter);
		itemFilter:SetChecked(itemFilters[filter]);
		itemFilter:SetScript("OnClick", function(self)
			itemFilters[self.filter] = self:GetChecked();
			app:RefreshData();
		end);
	end
	
	-- "Non-Equipment Types" ("Title" is actually blank - this is used for positioning.)
	lastFilter = frame.NonEquipmentTypesTitle;
	for i,filter in ipairs({ 100, 101, 102, 103 }) do
		local itemFilter = CreateFrame("CheckButton", name .. "-15-" .. filter, frame, "InterfaceOptionsCheckButtonTemplate");
		itemFilter:SetPoint("LEFT", frame.ShowMissingItems, "LEFT", 0, 0);
		itemFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		itemFilter.filter = filter;
		itemFilter.Label = _G[itemFilter:GetName() .. "Text"];
		itemFilter.Label:SetText(itemFilterNames[filter] .. " (" .. filter .. ")");
		itemFilter.Label:SetWidth(200);
		lastFilter = itemFilter;
		itemFilter:SetChecked(itemFilters[filter]);
		itemFilter:SetScript("OnClick", function(self)
			itemFilters[self.filter] = self:GetChecked();
			app:RefreshData();
		end);
	end
	
	-- "Non-Equipment Types" (Part 2)
	lastFilter = frame.NonEquipmentTypesTitle;
	for i,filter in ipairs({ 104, 105, 106, 200 }) do
		local itemFilter = CreateFrame("CheckButton", name .. "-15-" .. filter, frame, "InterfaceOptionsCheckButtonTemplate");
		itemFilter:SetPoint("LEFT", frame.PlayFanfare, "LEFT", 0, 0);
		itemFilter:SetPoint("TOP", lastFilter, "BOTTOM", 0, 6);
		itemFilter.filter = filter;
		itemFilter.Label = _G[itemFilter:GetName() .. "Text"];
		itemFilter.Label:SetText(itemFilterNames[filter] .. " (" .. filter .. ")");
		itemFilter.Label:SetWidth(200);
		lastFilter = itemFilter;
		itemFilter:SetChecked(itemFilters[filter]);
		itemFilter:SetScript("OnClick", function(self)
			itemFilters[self.filter] = self:GetChecked();
			app:RefreshData();
		end);
	end
end

-- Coroutine Helper Functions
local function OnUpdate(self)
	for i=#self.__stack,1,-1 do
		if not self.__stack[i]() then
			table.remove(self.__stack, i);
			if #self.__stack < 1 then
				self:SetScript("OnUpdate", nil);
			end
		end
	end
end
local function Push(self, method)
	if not self.__stack then
		self.__stack = {};
		self:SetScript("OnUpdate", OnUpdate);
	elseif #self.__stack < 1 then 
		self:SetScript("OnUpdate", OnUpdate);
	end
	table.insert(self.__stack, method);
end
local function StartCoroutine(self, method)
	if method then
		local instance = coroutine.create(method);
		Push(self, 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

-- Row Helper Functions
local CreateRow;
local function ClearRowData(self)
	self.ref = nil;
	self.Background:Hide();
	self.Texture:Hide();
	self.Indicator:Hide();
	self.Summary:Hide();
	self.Label:Hide();
	self:Hide();
end
local function SetRowData(self, data)
	ClearRowData(self);
	if data then
		if data.item then
			if data.item.link or (data.item.refresh and data.item:refresh()) then
				data.icon = data.item.texture;
				data.text = data.item.link;
			else
				data.icon = "Interface\\Icons\\INV_Misc_QuestionMark";
				data.text = "Fetching Item #" .. tostring(data.itemID);
				self:GetParent().processingLinks = true;
			end
		end
		if data.text then
			local leftmost = self;
			local relative = "LEFT";
			local x = (((data.indent or 0) + 1) * GetDataMember("Indent", 8)) or 0;
			self.ref = data;
			if data.back then
				self.Background:SetAlpha(data.back or 0.2);
				self.Background:Show();
			end
			if data.instanceID then
				local saved = GetPersonalDataSubMember("Saves", data.text); -- TODO: Convert this to an int-based system.... maybe?
				if saved then
					self.Indicator:SetTexture("Interface\\FriendsFrame\\StatusIcon-DnD");
					self.Indicator:SetPoint("RIGHT", leftmost, relative, x, 0);
					self.Indicator.instanceID = saved;
					self.Indicator:Show();
					data.saved = saved;
				end
			elseif data.unobtainable then
				local reason = L("UNOBTAINABLE_ITEM_REASONS")[data.unobtainable or 1];
				self.Indicator:SetTexture(L("UNOBTAINABLE_ITEM_TEXTURES")[reason[1]] or "Interface\\FriendsFrame\\StatusIcon-DnD");
				self.Indicator:SetPoint("RIGHT", leftmost, relative, x, 0);
				self.Indicator.instanceID = nil;
				self.Indicator:Show();
			end
			if data.icon then
				self.Texture:SetPoint("LEFT", leftmost, relative, x, 0);
				self.Texture:SetTexture(data.icon);
				self.Texture:Show();
				leftmost = self.Texture;
				relative = "RIGHT";
				x = 4;
			end
			
			if data.total and data.total > 0 then
				local percent = data.collected / data.total;
				self.Summary:SetText("|c" .. GetProgressColor(percent) .. tostring(data.collected) .. " / " .. tostring(data.total) .. " (" .. tostring(floor(percent * 100)) .. "%)|r");
				self.Summary:Show();
			elseif data.itemID and not data.collected then
				self.Summary:SetText("|cFFFF0000X|r");
				self.Summary:Show();
			end
			self.Label:SetPoint("LEFT", leftmost, relative, x, 0);
			if self.Summary and self.Summary:IsShown() then
				self.Label:SetPoint("RIGHT", self.Summary, "LEFT", 0, 0);
			else
				self.Label:SetPoint("RIGHT");
			end
			self.Label:SetText(data.text);
			self.Label:Show();
			self:Show();
		elseif data.questID then
			data.text = QuestTitleFromID[data.questID];
			if not data.text or data.text == "" then self:GetParent().processingLinks = true; end
		elseif data.achievementID then
			local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(data.achievementID);
			if not Name then
				self:GetParent().processingLinks = true;
			else
				data.text = Name;
				data.icon = Image;
			end
		end
	end
end
local function UpdateVisibleRowData(self)
	-- If there is no raw data, then return immediately.
	if not self.rowData then return; end
	
	-- Make it so that if you scroll all the way down, you have the ability to see all of the text every time.
	local container = self.Container;
	local totalRowCount = #self.rowData + 2;
	local rowCount = math.floor(container:GetHeight() / container.rowHeight);
	local current = math.max(1, math.min(self.ScrollBar.CurrentValue, totalRowCount - rowCount));
	self.ScrollBar:SetMinMaxValues(1, math.max(1, totalRowCount - rowCount));
	
	-- Create up to the maximum number of rows required to fill the container.
	for i=#container.rows,rowCount do CreateRow(container); end
	
	-- Set the data for the first row to ALWAYS display the topmost data (essentially becoming a Title Bar)
	SetRowData(container.rows[1], self.rowData[1]);
	current = current + 1;
	
	-- Fill the remaining rows up to the (visible) row count.
	for i=2,rowCount do
		SetRowData(container.rows[i], self.rowData[current]);
		current = current + 1;
	end
	
	-- Hide the extra rows if any exist (these are created when you make the window larger and then shrink, the leftover frames are still alive)
	for i=rowCount + 1,#container.rows do
		ClearRowData(container.rows[i]);
	end
	
	-- If the rows need to be processed again, do so next update.
	if container.processingLinks then
		container.processingLinks = nil;
		Push(app, function()
			UpdateVisibleRowData(self);
		end);
	end
end
local function HideParent(self)
	self:GetParent():Toggle();
end
local function IsSelfOrChild(self, focus)
	-- This function helps validate that the focus is within the local hierarchy.
	return focus and (self == focus or IsSelfOrChild(self, focus:GetParent()));
end
local function StopMovingOrSizing(self)
	if self.isMoving then
		self:StopMovingOrSizing();
		self.isMoving = false;
	end
end
local function StartMovingOrSizing(self, fromChild)
	if self.isMoving then
		StopMovingOrSizing(self);
	else
		self.isMoving = true;
		if ((select(2, GetCursorPosition()) / self:GetEffectiveScale()) < self:GetTop() - 40) then
			self:StartSizing();
			Push(self, function()
				if self.isMoving then
					UpdateVisibleRowData(self);
					return true;
				end
			end);
		else
			self:StartMoving();
			Push(app, function()
				if self.isMoving then
					-- This fixes a bug where the window will get stuck on the mouse until you reload.
					if IsSelfOrChild(self, GetMouseFocus()) then
						return true;
					else
						StopMovingOrSizing(self);
					end
				end
			end);
		end
	end
end
local function RowOnClick(self, button)
	local reference = self.ref;
	if reference then
		if IsModifiedClick("CHATLINK") then
			app.print(reference.link or self.Label:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(reference.link or self.Label:GetText());
		elseif reference.rows then
			if button == "LeftButton" then
				if self.index > 0 then
					-- Expand Functionality
					reference.expanded = not reference.expanded;
					app:UpdateWindows(true);
				else
					-- Allow the First Frame to move the parent.
					local owner = self:GetParent():GetParent();
					self:SetScript("OnMouseUp", function(self)
						self:SetScript("OnMouseUp", nil);
						StopMovingOrSizing(owner);
					end);
					StartMovingOrSizing(owner, true);
				end
			else
				if self.index > 0 then
					-- Pop Out Functionality! :O
					local popout = app:GetWindow(self.Label:GetText());
					popout.data = self.ref;
					popout:Toggle(true);
				else
					-- Configuration Functionality!
					ShowInterfaceOptions();
				end
			end
		end
	else
		if IsModifiedClick("CHATLINK") then
			app.print(self.Label:GetText());
		elseif IsModifiedClick("DRESSUP") then
			DressUpItemLink(self.Label:GetText());
		end
	end
end
local function RowOnEnter(self)
	local reference = self.ref; -- NOTE: This is the good ref value, not the parasitic one.
	if reference and GameTooltip then
		GameTooltipIcon:Hide();
		GameTooltip:ClearLines();
		GameTooltip:SetOwner(self, "ANCHOR_LEFT");
		
		-- NOTE: Order matters, we "fall-through" certain values in order to pass this information to the item ID section.
		if reference.item then
			-- This is an item reference. :)
			if reference.item.link then
				GameTooltip:SetHyperlink(reference.item.link);
			else
				GameTooltip:SetText(self.Label:GetText());
			end
			if reference.itemID then GameTooltip:AddDoubleLine(L("ITEM_ID"), tostring(reference.itemID)); end
			if reference.item.f then
				GameTooltip:AddDoubleLine(L("FILTER_ID"), tostring(L("FILTER_ID_TYPES")[reference.item.f]));
			else
				-- Class ID and Sub Class ID are deprecated and only shown if the filter is invalid.
				local classID, subClassID = reference.item.class, reference.item.subclass;
				if classID and subClassID then
					GameTooltip:AddDoubleLine(tostring(GetItemClassInfo(classID)) .. " (".. tostring(classID) .. ")", 
						tostring(GetItemSubClassInfo(classID, subClassID)) .. " (" .. tostring(subClassID) .. ")");
				end
			end
			if reference.item.visualID then GameTooltip:AddDoubleLine(L("VISUAL_ID"), tostring(reference.item.visualID)); end
			if reference.item.slot and reference.item.slot ~= "" then GameTooltip:AddDoubleLine(L("ITEM_SLOT"), _G[reference.item.slot]); end
			if reference.item.isToy then
				GameTooltip:AddDoubleLine(L("TOY_ID"), tostring(reference.itemID));
				GameTooltip:AddDoubleLine(" ", L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
			elseif reference.item.illusionID then
				GameTooltip:AddDoubleLine(L("ILLUSION_ID"), tostring(reference.item.illusionID));
				GameTooltip:AddDoubleLine(" ", L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
			elseif reference.item.mountID then
				GameTooltip:AddDoubleLine(L("MOUNT_ID"), tostring(reference.item.mountID)); -- NOTE: This is a spellID base type.
				GameTooltip:AddDoubleLine(" ", L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
				local creatureName, spellID, icon, active, isUsable, sourceType, isFavorite, isFactionSpecific, faction, hideOnChar, isCollected, mountID = GetMountInfo(reference.item.mountID);
				if creatureName then
					local creatureDisplayID, descriptionText, sourceText, isSelfMount, mountType = C_MountJournal.GetMountInfoExtraByID(mountID);
					GameTooltip:AddLine(" ");
					GameTooltip:AddLine(tostring(creatureName));
					GameTooltip:AddLine(tostring(sourceText));
					GameTooltip:AddLine(tostring(descriptionText), 1, 1, 1, 1);
				end
			elseif reference.item.speciesID then
				GameTooltip:AddDoubleLine(L("SPECIES_ID"), tostring(reference.item.speciesID));
				if reference.item.speciesID > 0 then
					GameTooltip:AddDoubleLine(" ", L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
					local speciesName, speciesIcon, petType, companionID, tooltipSource, tooltipDescription  = C_PetJournal.GetPetInfoBySpeciesID(reference.item.speciesID);
					if speciesName then
						GameTooltip:AddLine(" ");
						GameTooltip:AddLine(tostring(speciesName));
						GameTooltip:AddLine(tostring(tooltipDescription), 1, 1, 1, 1);
					end
				else
					GameTooltip:AddLine(L("SPECIES_ID_MISSING"), 1, 0, 0, 0.5);
				end
			elseif reference.item.s then
				GameTooltip:AddDoubleLine(L("SOURCE_ID"), tostring(reference.item.s));
				GameTooltip:AddDoubleLine(" ", L(reference.collected and "COLLECTED" or "NOT_COLLECTED"));
			elseif not reference.rows and not reference.collected then
				GameTooltip:AddLine(L("SOURCE_ID_MISSING"), 1, 0, 0, 0.5);
			end
			if reference.unobtainable then GameTooltip:AddLine(L("UNOBTAINABLE_ITEM_REASONS")[reference.unobtainable or 1][2], 1, 0, 0, 0.5); end
		elseif reference.instanceID then
			-- Contains information about an Instance (Raid or Dungeon)
			-- Is the player's character saved to this instance?
			if self.ref.saved and self.ref.saved then
				GameTooltip:SetInstanceLockEncountersComplete(self.ref.saved);
				GameTooltip:AddLine(" ");
			end
			
			-- Show information about the instance
			GameTooltip:AddDoubleLine(reference.text or "???", L("INSTANCE"));
			GameTooltip:AddDoubleLine(L("INSTANCE_ID"), tostring(reference.instanceID));
		end
		
		-- Standalone Fields
		if reference.tierID then
			-- Contains information about the Expansion. (called "Tier" in the WoW API)
			GameTooltip:AddDoubleLine(EJ_GetTierInfo(reference.tierID), L("EXPANSION"));
			GameTooltip:AddDoubleLine(L("EXPANSION_ID"), tostring(reference.tierID));
			local tier = app.TierDB[reference.tierID];
			if tier then
				if tier.texture then
					GameTooltipIcon:SetSize(128,64);
					GameTooltipIcon.icon:SetTexture(tier.texture);
					GameTooltipIcon:Show();
				elseif tier.atlas then
					GameTooltipIcon:SetSize(128,64);
					GameTooltipIcon.icon:SetAtlas(tier.atlas);
					GameTooltipIcon:Show();
				end
			end
		end
		if reference.difficultyID then
			-- Contains information about a Difficulty Setting (only useful information is the name really)
			GameTooltip:AddDoubleLine(GetDifficultyInfo(reference.difficultyID), L("DIFFICULTY"));
			GameTooltip:AddDoubleLine(L("DIFFICULTY_ID"), tostring(reference.difficultyID));
		end
		if reference.encounterID then
			-- Contains information about an Encounter. (TODO: Show the models for the bosses?)
			--local name, description, _, _, link = EJ_GetEncounterInfo(reference.encounterID);
			--self.link = link;	-- TODO: Clicking on this opens Adventure Guide?
			GameTooltip:AddDoubleLine(reference.text or "???", L("ENCOUNTER"));
			GameTooltip:AddDoubleLine(L("ENCOUNTER_ID"), tostring(reference.encounterID));
		end
		if reference.questID then
			-- Contains information about a Quest. (TODO: Show the information for the quest?)
			GameTooltip:SetHyperlink("quest:" .. reference.questID);
			GameTooltip:AddDoubleLine(L("QUEST_ID"), tostring(reference.questID));
		end
		if reference.achievementID then
			-- Contains information about an Achievement. (TODO: Show the information for the achievement?)
			GameTooltip:SetHyperlink("achievement:" .. reference.achievementID);
			GameTooltip:AddDoubleLine(L("ACHIEVEMENT_ID"), tostring(reference.achievementID));
		end
		
		-- Miscellaneous fields
		if reference.description then GameTooltip:AddLine(reference.description, 1, 1, 1, 1); end
		if reference.icon then
			if reference.itemID then
				GameTooltipIcon:SetSize(64,64);
			else
				GameTooltipIcon:SetSize(96,96);
			end
			GameTooltipIcon.icon:SetTexture(reference.icon);
			GameTooltipIcon:Show();
		end
		if reference.mapID then
			GameTooltip:AddDoubleLine(L("MAP_ID"), tostring(reference.mapID));
			if reference.mapID == 0 then
				-- Looks like Blizzard goofed and didn't include the map ID. Way to go!
				GameTooltip:AddLine(L("MAP_ID_INVALID_WARNING"), 1, 0, 0, 0.5);
				local mapID = GetPersonalDataMember("MapID");
				if mapID then
					local mapName = GetMapNameByID(mapID);
					if reference.text == mapName then
						app.print("Updated Map ID to #" .. tostring(mapID) .. " for " .. reference.text);
						reference.mapID = mapID;
						app:UpdateWindows();
					else
						GameTooltip:AddDoubleLine("Map ID (Current)", tostring(mapID));
						GameTooltip:AddLine(mapName);
					end
				end
			end
		end
		if not reference.item and reference.Lvl and reference.Lvl > 0 then
			GameTooltip:AddDoubleLine(L("REQUIRES_LEVEL"), tostring(reference.Lvl));
		end
		-- No reference data, so let's display the label's text.
		if GameTooltip:NumLines() < 1 then GameTooltip:SetText(self.Label:GetText()); end
		if reference.rows then
			GameTooltip:AddLine(L((self.index > 0 and "LEFT_CLICK_TO_EXPAND") or "LEFT_CLICK_TO_MOVE"), 1, 1, 1);
			GameTooltip:AddLine(L((self.index > 0 and "RIGHT_CLICK_TO_POPOUT") or "RIGHT_CLICK_TO_CONFIGURE"), 1, 1, 1);
		end
		GameTooltip:Show();
	end
end
local function RowOnLeave(self)
	if self.ref and GameTooltip then
		GameTooltip:Hide();
		GameTooltipIcon:Hide();
	end
end
CreateRow = function(self)
	local row = CreateFrame("Button", nil, self);
	row:SetHeight(self.rowHeight);
	row.index = #self.rows;
	if row.index == 0 then
		-- This means relative to the parent.
		row:SetPoint("TOPLEFT");
		row:SetPoint("TOPRIGHT");
	else
		-- This means relative to the row above this one.
		row:SetPoint("TOPLEFT", self.rows[row.index], "BOTTOMLEFT");
		row:SetPoint("TOPRIGHT", self.rows[row.index], "BOTTOMRIGHT");
	end
	table.insert(self.rows, row);
	
	-- Setup highlighting and event handling
	row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD");
	row:RegisterForClicks("LeftButtonDown","RightButtonDown");
	row:SetScript("OnClick", RowOnClick);
	row:SetScript("OnEnter", RowOnEnter);
	row:SetScript("OnLeave", RowOnLeave);
	row:EnableMouse(true);
	
	-- Background is used by the Map Highlight functionality.
	row.Background = row:CreateTexture(nil, "BACKGROUND");
	row.Background:SetPoint("LEFT", 4, 0);
	row.Background:SetPoint("BOTTOM");
	row.Background:SetPoint("RIGHT");
	row.Background:SetPoint("TOP");
	row.Background:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight");
	
	-- Indicator is used by the Instance Saves functionality.
	row.Indicator = row:CreateTexture(nil, "ARTWORK");
	row.Indicator:SetPoint("BOTTOM");
	row.Indicator:SetPoint("TOP");
	row.Indicator:SetWidth(row:GetHeight());
	
	-- Texture is the icon.
	row.Texture = row:CreateTexture(nil, "ARTWORK");
	row.Texture:SetPoint("BOTTOM");
	row.Texture:SetPoint("TOP");
	row.Texture:SetWidth(row:GetHeight());
	
	-- Label is the text information you read.
	row.Label = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
	row.Label:SetJustifyH("LEFT");
	row.Label:SetPoint("BOTTOM");
	row.Label:SetPoint("TOP");
	
	-- Summary is the completion summary information. (percentage text)
	row.Summary = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
	row.Summary:SetJustifyH("CENTER");
	row.Summary:SetPoint("BOTTOM");
	row.Summary:SetPoint("RIGHT");
	row.Summary:SetPoint("TOP");
	
	-- Clear the Row Data Initially
	ClearRowData(row);
end

-- Refresh Functions (Coroutines)
local function ProcessGroup(data, parent, indent, back)
	if parent.visible then
		if parent.mapID and parent.mapID == GetPersonalDataMember("MapID") then
			parent.back = 1;
		else 
			parent.back = back or parent.back;
		end
		parent.indent = indent;
		tinsert(data, parent);
		if parent.rows and parent.expanded then
			indent = indent + 1;
			back = parent.back * 0.5;
			for j, group in ipairs(parent.rows) do
				ProcessGroup(data, group, indent, back);
			end
		end
	end
end
local function UpdateGroups(parent, groups, instanceID, difficultyID)
	if groups then
		-- Iterate through the groups
		for key, group in ipairs(groups) do
			-- Determine if this user can enter the instance or acquire the item.
			local meetsRequirements = app.GroupRequirementsFilter(group);
			if meetsRequirements then
				-- Create a Row Data Object
				group.visible = true;
				tinsert(parent.rows, group);
				
				-- Get additional information, if necessary.
				if group.itemID then
					local missingSource = false;
					if not group.item then group.item = Harvest(instanceID, difficultyID, group.itemID); end
					group.unobtainable = L("UNOBTAINABLE_ITEMS")[group.itemID];
					if group.item.mountID then
						group.collected = GetDataSubMember("CollectedMounts", group.item.mountID);
						if not group.collected then
							group.collected = GetMountCollected(group.item.mountID);
							GetDataSubMember("CollectedMounts", group.item.mountID, group.collected);
						end
					elseif group.item.illusionID then
						group.collected = GetDataSubMember("CollectedIllusions", group.item.illusionID);
						if not group.collected then
							group.collected = GetIllusionCollected(group.item.illusionID);
							SetDataSubMember("CollectedIllusions", group.item.illusionID, group.collected);
						end
					elseif group.item.isToy then -- Toys
						group.collected = GetDataSubMember("CollectedToys", group.itemID);
						if not group.collected then
							group.collected = PlayerHasToy(group.itemID);
							SetDataSubMember("CollectedToys", group.itemID, group.collected);
						end
					elseif group.item.speciesID and group.item.speciesID > 0 then -- Companion Pets
						group.collected = GetDataSubMember("CollectedPets", group.itemID);
						local numCollected, limit = C_PetJournal.GetNumCollectedInfo(group.item.speciesID);
						group.collected = (numCollected or 0) > 0;
						SetDataSubMember("CollectedPets", group.itemID, group.collected);
					elseif group.item.s and group.item.s > 0 then
						group.collected = GetDataSubMember("Collected", group.item.s);
						if not group.collected then
							local sourceInfo = C_TransmogCollection.GetSourceInfo(group.item.s);
							if sourceInfo then
								group.collected = app.ItemSourceFilter(sourceInfo);
								SetDataSubMember("Collected", group.item.s, group.collected);
							end
						end
					elseif not group.groups then missingSource = true; end
					
					-- If the 'can equip' filter says true
					if app.FilterItemClass(group.item) then
						if group.collected then
							group.visible = app.CollectedItemVisibilityFilter(group);
						else
							group.visible = app.MissingItemVisibilityFilter(group);
							
							-- Items without sources list (for debugging)
							if missingSource then
								table.insert(GetTempDataMember("Missing"), group);
								if not group.item.link then table.insert(GetTempDataMember("MissingLinks"), group); end
							end
						end
						
						-- Does this item have subgroups? (doing this here makes it so that the parent item can be collected and still show the subgroup if they weren't collected yet)
						if group.groups then
							if not group.rows then
								group.rows = {};
							else
								wipe(group.rows);
							end
							if group.item and group.item.s then
								group.total = 1;
								group.collected = (group.collected and 1) or 0;
							else
								group.total = 0;
								group.collected = 0;
							end
							UpdateGroups(group, group.groups, group.instanceID or instanceID, group.difficultyID or difficultyID);
							parent.collected = (parent.collected or 0) + group.collected;
							parent.total = (parent.total or 0) + group.total;
							group.visible = group.total > 0 and app.GroupVisibilityFilter(group);
						else
							-- Increment the parent group's totals. (but only if it is visible)
							parent.total = (parent.total or 0) + 1;
							if group.collected then parent.collected = (parent.collected or 0) + 1; end
						end
					else
						group.visible = false;
					end
				else
					-- If the 'can equip' filter says true
					if app.FilterItemClass(group) then
						if group.npcID then
							-- TODO: Move this to an NPC constructor.
							group.text = L("NPC_ID_NAMES")[group.npcID] or ("Unknown NPC ID #" .. group.npcID);
						end
						if group.questID then
							group.icon = "Interface\\Icons\\Achievement_Quests_Completed_08";
						end
						
						-- Does this group have subgroups?
						if group.groups then
							if not group.rows then
								group.rows = {};
							else
								wipe(group.rows);
							end
							if group.item and group.item.s then
								group.total = 1;
								group.collected = (group.collected and 1) or 0;
							else
								group.total = 0;
								group.collected = 0;
							end
							UpdateGroups(group, group.groups, group.instanceID or instanceID, group.difficultyID or difficultyID);
							parent.collected = (parent.collected or 0) + group.collected;
							parent.total = (parent.total or 0) + group.total;
							group.visible = group.total > 0 and app.GroupVisibilityFilter(group);
						elseif group.rows and group.visible then
							group.rows = group.rows;
							group.total = #group.rows;
							group.collected = 0;
							parent.collected = (parent.collected or 0) + group.collected;
							parent.total = (parent.total or 0) + group.total;
							group.visible = group.total > 0;
						end
					else
						group.visible = false;
					end
				end
			end
		end
	end
end
local function RefreshLocation()
	if not app.refreshingLocation then
		app.refreshingLocation = true;
		StartCoroutine(app, 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.
			SetPersonalDataMember("MapID", mapID);
			app:UpdateWindows();
			app.refreshingLocation = false;
		end);
	end
end
local function RefreshSaves()
	if not app.refreshingSaves then
		app.refreshingSaves = true;
		StartCoroutine(app, 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, GetPersonalDataMember("Saves", {});
			local converter = 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)
			app.refreshingSaves = false;
			if newSave then
				SetPersonalDataMember("Saves", saves);
				RefreshLocation();
			end
		end);
	end
end

-- Collection Window Creation
app.Windows = {};
local function OnScrollBarMouseWheel(self, delta)
	self.ScrollBar:SetValue(self.ScrollBar.CurrentValue - delta);
end
local function OnScrollBarValueChanged(self, value)
	local un = math.floor(value);
	local up = un + 1;
	self.CurrentValue = (up - value) > (-(un - value)) and un or up;
	UpdateVisibleRowData(self:GetParent());
end
local function ToggleWindow(self, show)
	if self:IsVisible() and not show then
		self:Hide();
		for i, self in pairs(app.Windows) do
			if self:IsVisible() then
				app:Show();
				return;
			end
		end
		app:Hide();
	else
		self:Show();
		self:Update();
		app:Show();
	end
end
function app:ToggleWindow(suffix)
	local window = app.Windows[suffix];
	if window then ToggleWindow(window); end
end
local function UpdateWindow(self)
	-- If this window doesn't have data, do nothing.
	if not self.data then return; end
	if not self.rowData then
		self.rowData = {};
	else
		wipe(self.rowData);
	end
	if self.data and self:IsVisible() then
		self.data.expanded = true;
		ProcessGroup(self.rowData, self.data, 0, 0);
	end
	UpdateVisibleRowData(self);
end
function app:UpdateWindows()
	for i, window in pairs(app.Windows) do
		window:Update();
	end
end
function app:RefreshData(lazy, safely)
	app.refreshDataForce = app.refreshDataForce or not lazy;
	if not app.refreshingData then
		app.refreshingData = true;
		StartCoroutine(app, 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 = GetTempDataMember("CACHE");
			if not allData or app.refreshDataForce then
				app.refreshDataForce = nil;
				allData = {};
				allData.rows = {};
				allData.expanded = true;
				allData.icon = "Interface\\Icons\\INV_Axe_106.blp";
				allData.text = app.DisplayName;
				allData.visible = true;
				allData.collected = 0;
				allData.total = 0;
				local groups = {};
				
				-- Dungeons & Raids
				local db = {};
				db.expanded = true;
				db.text = L("DUNGEONS&RAIDS");
				db.groups = app.Instances;
				table.insert(groups, db);
				
				-- Everything (for debugging)
				--[[
				local everything = {};
				everything.expanded = false;
				everything.text = "EVERRRRYTTHING";
				everything.groups = {};
				table.insert(groups, everything);
				for difficultyID,items in pairs(app.ItemDB) do
					local g = {};
					g.expanded = true;
					g.text = tostring(difficultyID);
					g.difficultyID = difficultyID;
					local h = {};
					g.groups = h;
					table.insert(everything.groups, g);
					for itemID, item in pairs(items) do
						table.insert(h, { ["itemID"] = itemID });
					end
				end
				]]--
				
				local missingData = {};
				missingData.visible = true;
				missingData.expanded = false;
				missingData.text = "Items without Sources";
				missingData.rows = GetTempDataMember("Missing", {});
				table.wipe(missingData.rows);
				table.insert(groups, missingData);
				
				local missingLinkData = {};
				missingLinkData.visible = true;
				missingLinkData.expanded = false;
				missingLinkData.text = "Items without Links";
				missingLinkData.rows = GetTempDataMember("MissingLinks", {});
				table.wipe(missingLinkData.rows);
				table.insert(groups, missingLinkData);
				UpdateGroups(allData, groups, 0, 0);
				missingLinkData.groups = {};
				missingData.groups = {};
				SetTempDataMember("CACHE", allData);
			end
			app:GetWindow("Prime").data = allData;
			
			-- Send an Update to the Windows to Rebuild their Row Data
			app:UpdateWindows();
			app.refreshDataForce = false;
			app.refreshingData = false;
		end);
	end
end
function app:GetWindow(suffix)
	local window = app.Windows[suffix];
	if not window then
		-- Create the window instance.
		window = CreateFrame("FRAME", app:GetName() .. "-Window-" .. suffix, UIParent);
		app.Windows[suffix] = window;
		window.Toggle = ToggleWindow;
		window.Update = UpdateWindow;
		window:SetScript("OnMouseWheel", OnScrollBarMouseWheel);
		window:SetScript("OnMouseDown", StartMovingOrSizing);
		window:SetScript("OnMouseUp", StopMovingOrSizing);
		window:SetScript("OnHide", StopMovingOrSizing);
		window:SetBackdrop(backdrop);
		window:SetBackdropColor(0, 0, 0, 255);
		window:SetClampedToScreen(true);
		window:SetToplevel(true);
		window:EnableMouse(true);
		window:SetMovable(true);
		window:SetResizable(true);
		window:SetPoint("CENTER");
		window:SetSize(300, 300);
		window:SetUserPlaced(true);
		window.data = {};
		window:Hide();
		
		-- The Close Button. It's assigned as a local variable so you can change how it behaves.
		window.CloseButton = CreateFrame("Button", nil, window, "UIPanelCloseButton");
		window.CloseButton:SetPoint("TOPRIGHT", window, "TOPRIGHT", 4, 3);
		window.CloseButton:SetScript("OnClick", HideParent);
		
		-- The Scroll Bar.
		local scrollbar = CreateFrame("Slider", nil, window, "UIPanelScrollBarTemplate");
		scrollbar:SetPoint("TOP", window.CloseButton, "BOTTOM", 0, -10);
		scrollbar:SetPoint("BOTTOMRIGHT", window, "BOTTOMRIGHT", -4, 36);
		scrollbar:SetScript("OnValueChanged", OnScrollBarValueChanged);
		scrollbar.back = scrollbar:CreateTexture(nil, "BACKGROUND");
		scrollbar.back:SetColorTexture(0,0,0,0.4)
		scrollbar.back:SetAllPoints(scrollbar);
		scrollbar:SetMinMaxValues(1, 1);
		scrollbar:SetValueStep(1);
		scrollbar.CurrentValue = 1;
		scrollbar:SetWidth(16);
		scrollbar:EnableMouseWheel(true);
		window:EnableMouseWheel(true);
		window.ScrollBar = scrollbar;
		scrollbar:SetValue(1);
		
		-- The Corner Grip. (this isn't actually used, but it helps indicate to players that they can do something)
		local grip = window:CreateTexture(nil, "ARTWORK");
		grip:SetTexture("Interface\\AddOns\\AllTheThings\\assets\\grip");
		grip:SetSize(16, 16);
		grip:SetTexCoord(0,1,0,1);
		grip:SetPoint("BOTTOMRIGHT", -5, 5);
		
		-- The Row Container. This contains all of the row frames.
		local container = CreateFrame("FRAME", nil, window);
		container:SetPoint("TOPLEFT", window, "TOPLEFT", 0, -6);
		container:SetPoint("RIGHT", scrollbar, "LEFT", 0, 0);
		container:SetPoint("BOTTOM", window, "BOTTOM", 0, 6);
		container.rowHeight = select(2, GameFontNormal:GetFont()) + 4;
		window.Container = container;
		container.rows = {};
		container:Show();
	end
	return window;
end

-- Create the Primary Collection Window
app:GetWindow("Prime");

-- 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()
	app:ToggleWindow("Prime");
end

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

-- Define Event Behaviours
app.events.VARIABLES_LOADED = function()
	-- Setup the localization and interpret the Display Information.
	SetLocale(GetLocale());
	app.DisplayName = Colorize(L("TITLE"), RGBToHex(180, 180, 255));
	
	-- Cache information about the player.
	local _, class, classIndex = UnitClass("player");
	local raceName, race = UnitRace("player");
	app.Class = class;
	app.ClassIndex = classIndex;
	app.Level = UnitLevel("player");
	local raceIndex = app.RaceDB[race];
	if type(raceIndex) == "table" then
		local factionGroup = UnitFactionGroup("player");
		raceIndex = raceIndex[factionGroup];
	end
	app.Race = race;
	app.RaceIndex = raceIndex;
	app.InstanceDB = nil;
	
	-- Load in the Presets if they exist for this character.
	-- Default values should fallback to their presets.
	local filters = GetPersonalDataMember("ItemFilters");
	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 filter, state in pairs(presets) do
				data[filter] = state;
			end
		end
		SetPersonalDataMember("ItemFilters", data);
	end
	
	-- Register for Dynamic Events and Assign Filters
	if GetDataMember("AutoRefreshSaves", true) then app:RegisterEvent("BOSS_KILL"); end
	if GetDataMember("FilterGroupsByLevel", false) then
		app:RegisterEvent("PLAYER_LEVEL_UP");
		app.GroupRequirementsFilter = app.FilterGroupsByLevel;
	else
		app.GroupRequirementsFilter = app.NoFilter;
	end
	if GetDataMember("ShowCompletedGroups", false) then
		app.GroupVisibilityFilter = app.NoFilter;
	else
		app.GroupVisibilityFilter = app.FilterGroupsByCompletion;
	end
	if GetDataMember("ShowCollectedItems", false) then
		app.CollectedItemVisibilityFilter = app.NoFilter;
	else
		app.CollectedItemVisibilityFilter = app.Filter;
	end
	if GetDataMember("ShowMissingItems", true) then
		app.MissingItemVisibilityFilter = app.NoFilter;
	else
		app.MissingItemVisibilityFilter = app.Filter;
	end
	if GetDataMember("ShowMinimapButton", true) then
		app.Minimap = CreateMinimapButton(); -- NOTE: Create this if they turn it on.
		app.Minimap:Show();
	end
	if GetDataMember("UniqueAppearances", false) then
		app.ItemSourceFilter = app.FilterItemSourceUnique;
	else
		app.ItemSourceFilter = app.FilterItemSource;
	end
	if GetDataMember("FilterItemsByClass", true) then
		app.ClassRequirementFilter = app.FilterItemClass_RequireClasses;
	else
		app.ClassRequirementFilter = app.NoFilter;
	end
	if GetDataMember("FilterItemsByRace", true) then
		app.RaceRequirementFilter = app.FilterItemClass_RequireRaces;
	else
		app.RaceRequirementFilter = app.NoFilter;
	end
	
	-- Create the Settings Menu and show version information
	app.print(format(L("LOADING"), GetAddOnMetadata("AllTheThings", "Version")));
	CreateSettingsMenu();
	RefreshSaves();
	app:RefreshData(false, true);
end
app.events.PLAYER_LOGIN = function()
	LibStub:GetLibrary("LibDataBroker-1.1"):NewDataObject(app.DisplayName, {
		type = "launcher",
		icon = "Interface\\Icons\\INV_Axe_106",
		OnTooltipShow = function(self)
			GameTooltip:SetText(app.DisplayName);
			GameTooltip:AddLine(L("MINIMAP_MOUSEOVER_TEXT"), 1, 1, 1);
			GameTooltip:Show();
		end,
		OnClick = function(self, button)
			if button == "RightButton" then
				-- Right Button opens the Options menu.
				ShowInterfaceOptions();
			else
				-- Left Button Toggles the App
				app:ToggleWindow("Prime");
			end
		end,
	})
end
app.events.ZONE_CHANGED_NEW_AREA = RefreshLocation;
app.events.PLAYER_LEVEL_UP = function(newLevel)
	app.Level = newLevel;
	app:UpdateWindows(false, true);
end
app.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
app.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
app.events.UPDATE_INSTANCE_INFO = function()
	-- We got new information, not refresh the saves. :D
	app:UnregisterEvent("UPDATE_INSTANCE_INFO");
	RefreshSaves();
end
app.events.TOYS_UPDATED = function(itemID, new)
	if itemID then
		local name, link = GetItemInfo(itemID);
		if link then
			print(format(L("ITEM_ID_ADDED"), link, itemID));
			PlayFanfare();
			app:RefreshData(false, true);
		end
	end
end
app.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 = HarvestItemLink(itemLink);
		if data and not GetDataSubMember("Collected", sourceID) then
			print(format(L("ITEM_ID_ADDED"), itemLink, data.itemID));
			SetDataSubMember("Collected", sourceID, true);
			PlayFanfare();
			app:RefreshData(false, true);
		end
	end
end
app.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 = HarvestItemLink(itemLink);
		if data and GetDataSubMember("Collected", sourceID) then
			print(format(L("ITEM_ID_REMOVED"), itemLink, data.itemID));
			SetDataSubMember("Collected", sourceID, false);
			PlayRemoveSound();
			app:RefreshData(false, true);
		end
	end
end