local function CallDefaultOnFrame(frame)
	if frame.OnDefault then
		frame:OnDefault();
	end
end

local function CallRefreshOnFrame(frame)
	if frame.OnRefresh then
		frame:OnRefresh();
	end
end

local securecallfunction = securecallfunction;
local pairs = pairs;

local reservedVariables = {};

-- Matches a similar function reused in multiple places
local function EnumerateTaintedKeysTable(tableToIterate)
	local pairsIterator, enumerateTable, initialIteratorKey = securecallfunction(pairs, tableToIterate);
	local function IteratorFunction(tbl, key)
		return securecallfunction(pairsIterator, tbl, key);
	end

	return IteratorFunction, enumerateTable, initialIteratorKey;
end

local function SortTableByCommitOrder(tbl)
	-- Commit order is necessary under rare circumstances where we need to guarantee that
	-- a particular option is changed before another. This is necessary in at least two cases:
	-- 1) Applying the display mode setting prior to the resolution setting
	-- 2) Applying child graphic settings prior to the graphics or raid graphics setting.
	table.sort(tbl, function(lhs, rhs)
		return lhs:GetCommitOrder() < rhs:GetCommitOrder();
	end);
end

SettingsPanelMixin = {};

function SettingsPanelMixin:OnLoad()
	self.settings = {};
	self.categoryLayouts = {};
	self.modified = {};

	self.NineSlice.Text:SetText(SETTINGS_TITLE);

	self.tabsGroup = CreateRadioButtonGroup();

	self.tabsGroup:AddButtons({self.GameTab, self.AddOnsTab});
	self.tabsGroup:SelectAtIndex(1);
	self.tabsGroup:RegisterCallback(ButtonGroupBaseMixin.Event.Selected, self.OnTabSelected, self);

	self:SetParent(GetAppropriateTopLevelParent());

	local function closePanel()
		self:Close();
	end

	self.CloseButton.Text:SetText(SETTINGS_CLOSE);
	self.CloseButton:SetScript("OnClick", closePanel);
	self.onCloseCallback = closePanel;

	self.ApplyButton.Text:SetText(SETTINGS_APPLY);
	self.ApplyButton:SetScript("OnClick", function(button, buttonName, down)
		self:Commit();
	end);

	local settingsList = self:GetSettingsList();
	settingsList.Header.DefaultsButton.Text:SetText(SETTINGS_DEFAULTS);
	settingsList.Header.DefaultsButton:SetScript("OnClick", function(button, buttonName, down)
		StaticPopup_Show("GAME_SETTINGS_APPLY_DEFAULTS");
	end);

	self.SearchBox:HookScript("OnTextChanged", GenerateClosure(self.OnSearchTextChanged, self));

	self:GetCategoryList():RegisterCallback(SettingsCategoryListMixin.Event.OnCategorySelected, self.SelectCategory, self);

	settingsList.ScrollBox:SetScript("OnMouseWheel", function(scrollBox, delta)
		if not KeybindListener:OnForwardMouseWheel(delta) then
			ScrollControllerMixin.OnMouseWheel(scrollBox, delta);
		end
	end);

	self.Container.SettingsCanvas:SetScript("OnMouseWheel", nop);

	EventRegistry:RegisterCallback("KeybindListener.StoppedListening", self.OnKeybindStoppedListening, self);
	EventRegistry:RegisterCallback("KeybindListener.StartedListening", self.OnKeybindStartedListening, self);
	EventRegistry:RegisterCallback("KeybindListener.UnbindFailed", self.OnKeybindUnbindFailed, self);
	EventRegistry:RegisterCallback("KeybindListener.RebindFailed", self.OnKeybindRebindFailed, self);
	EventRegistry:RegisterCallback("KeybindListener.RebindSuccess", self.OnKeybindRebindSuccess, self);

	CVarCallbackRegistry:RegisterCallbackForAllCVarUpdates(self.OnCVarChanged, self);

	self:RegisterEvent("UPDATE_BINDINGS");
end

function SettingsPanelMixin:OnTabSelected(tab, tabIndex)
	self:GetCategoryList():SetCategorySet(tab.categorySet);
	PlaySound(SOUNDKIT.IG_CHARACTER_INFO_TAB);
end

function SettingsPanelMixin:OnCVarChanged(cvar, cvarValue)
	-- This handler is intended only for cvar updates originating from the console or through the cvar script api.
	-- If the cvar was changed through the settings api, as recommended, this value will already be updated.
	local setting = self:GetSetting(cvar);
	if not setting then
		return;
	end

	-- There are cases where a cvar value cannot always be retrieved. For example, output audio device will not be
	-- retrievable if the sound system is in the process of reinitializing. Skip handling if this occurs.
	if setting:GetValue() == nil then
		return;
	end

	-- The value is forced because the setting will always evaluate the current state of this
	-- cvar as unchanged due to the source of the value being the cvar that was just modified.
	-- This won't produce a recursive overflow because when setting the value legimately through
	-- the settings api, or through the controls, the setting will temporarily reject any changes
	-- to it's value.

	local force = true;
	-- Value is converted from the cvar string representation into a format the setting expects. Note
	-- this is done manually because we normally are strict about the value being the correct type.
	local value = securecallfunction(setting.ConvertValueInternal, setting, cvarValue);
	securecallfunction(setting.SetValue, setting, value, force);
end

function SettingsPanelMixin:OnEvent(event, ...)
	if event == "UPDATE_BINDINGS" then
		self:RenewKeybinds();
	end
end

function SettingsPanelMixin:OnShow()
	-- Ensure that the current category is reinitialized because any
	-- initializers with show or enable predicates need to be reevaluated.
	local force = true;
	self:SelectFirstCategory(force);

	-- Checks for if there are any categories to show on the AddOnsTab
	local categories = self:GetCategoryList();
	local showTabs = false;
	for _, category in ipairs(categories.allCategories) do
		if category.categorySet == Settings.CategorySet.AddOns then
			showTabs = true;
			break;
		end
	end

	if not showTabs then
		self.GameTab:Hide();
		self.AddOnsTab:Hide();
	end

	-- WOW10-16900
	if C_Glue.IsOnGlueScreen() then
		self:SetFrameStrata("DIALOG");
		GlueParent_AddModalFrame(self);

		self.NineSlice.Text:SetText(SYSTEMOPTIONS_MENU);
	else
		self.NineSlice.Text:SetText(SETTINGS_TITLE);
		UpdateMicroButtons();
	end

	self:CheckApplyButton();

	self:CallRefreshOnCanvases();
	self:CheckTutorials();

	categories:RefreshNewFeatures();
end

function SettingsPanelMixin:CheckTutorials()
end

function SettingsPanelMixin:Flush()
	KeybindListener:StopListening();

	self:ClearOutputText();
	self:ClearSearchBox();
	self:WipeModifiedTable();
end

function SettingsPanelMixin:OnHide()
	self:Flush();

	self:ClearActiveCategoryTutorial();

	if C_Glue.IsOnGlueScreen() then
		GlueParent_RemoveModalFrame(self);
		GlueParent_CloseSecondaryScreen();
		return;
	else
		UpdateMicroButtons();
	end

	local checked = Settings.GetValue("PROXY_CHARACTER_SPECIFIC_BINDINGS");
	local bindingSet = checked and Enum.BindingSet.Character or Enum.BindingSet.Account;
	SaveBindings(bindingSet);

	EventRegistry:TriggerEvent("SettingsPanel.OnHide");
end

function SettingsPanelMixin:Commit(unrevertable)
	self:CommitCanvases();
	self:CommitSettings(unrevertable);
	self:CommitBindings();
	self:WipeModifiedTable();
end

function SettingsPanelMixin:Close(skipTransitionBackToOpeningPanel)
	if self:HasUnappliedSettings() then
		StaticPopup_Show("GAME_SETTINGS_CONFIRM_DISCARD");
	else
		self:ExitWithCommit(skipTransitionBackToOpeningPanel);
	end
end

function SettingsPanelMixin:ExitWithoutCommit()
	local settings = {};

	for setting, record in pairs(self.modified) do
		-- The settings under affect of IgnoreApply flag shouldn't be in the self.modified table after having been applied
		-- Needs bug investigation
		if (securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.Revertable) or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.Apply)) and not securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.IgnoreApply) then
			--store the setting we want to revert and do it outside of this loop so we can avoid any invalid key errors
			table.insert(settings, setting);
		end
	end

	for i, setting in ipairs(settings) do
		securecallfunction(setting.Revert, setting);
	end

	self:Flush();
	self:TransitionBackOpeningPanel();
end

function SettingsPanelMixin:ExitWithCommit(skipTransitionBackToOpeningPanel)
	local unrevertable = true;
	self:Commit(unrevertable);

	if skipTransitionBackToOpeningPanel then
		HideUIPanel(self);
	else
		self:TransitionBackOpeningPanel();
	end
end

function SettingsPanelMixin:TransitionBackOpeningPanel()
	HideUIPanel(self);

	if not C_Glue.IsOnGlueScreen() then
		if not EditModeManagerFrame:ShowIfActive() then
			ToggleGameMenu();
		end
	end
end

function SettingsPanelMixin:Open()
	self:WipeModifiedTable();
	ShowUIPanel(SettingsPanel);
	if not SettingsAddonLoaded then
		C_AddOns.LoadAddOn("Blizzard_Settings"); -- Loaded for tracking
	end
end

function SettingsPanelMixin:OpenToCategory(categoryID, scrollToElementName)
	self:Open();

	local categoryTbl = self:GetCategoryList():GetCategory(categoryID);
	if categoryTbl then
		categoryTbl:SetExpanded(true);

		self:SelectCategory(categoryTbl);

		if scrollToElementName then
			self:GetSettingsList():ScrollToElementByName(scrollToElementName);
		end
	end
	return categoryTbl ~= nil;
end

function SettingsPanelMixin:SetKeybindingsCategory(category)
	self.keybindingsCategory = category;
end

function SettingsPanelMixin:CommitBindings()
	if not C_Glue.IsOnGlueScreen() then
		SaveBindings(GetCurrentBindingSet());

		local shouldSave = true;
		securecallfunction(SaveAllCustomBindings, shouldSave);
	end
end

function SettingsPanelMixin:IsCommitInProgress()
	return self.isCommitInProgress;
end

function SettingsPanelMixin:CommitSettings(unrevertable)
	self.revertableSettings = {};

	local saveBindings = false;
	local gxRestart = false;
	local windowUpdate = false;

	local settings = {};
	for setting, record in pairs(self.modified) do
		table.insert(settings, setting);
		setting:LockPendingValue();
	end
	SortTableByCommitOrder(settings);

	if #settings > 0 then
		self.isCommitInProgress = true;

		for index, setting in ipairs(settings) do
			saveBindings = saveBindings or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.SaveBindings);
			gxRestart = gxRestart or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.GxRestart);
			windowUpdate = windowUpdate or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.UpdateWindow);

			if not unrevertable then
				if securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.Revertable) then
					local originalValue = securecallfunction(setting.GetValueDerived, setting);
					table.insert(self.revertableSettings, {setting = setting, originalValue = originalValue});
				end
			end

			securecallfunction(setting.Commit, setting);
		end

		self.isCommitInProgress = nil;
	end

	self:FinalizeCommit(saveBindings, gxRestart, windowUpdate);

	if #self.revertableSettings > 0 then
		local duration = 8.0;
		StaticPopup_Show("GAME_SETTINGS_TIMED_CONFIRMATION", nil, nil, duration);
		local function Timer()
			self:RevertSettings();
			StaticPopup_Hide("GAME_SETTINGS_TIMED_CONFIRMATION");
		end
		self.Timer = C_Timer.NewTimer(duration, Timer);
	end

end

function SettingsPanelMixin:FinalizeCommit(saveBindings, gxRestart, windowUpdate)
	if saveBindings then
		SaveBindings(GetCurrentBindingSet());
	end

	if gxRestart then
		RestartGx();
	end

	if windowUpdate then
		UpdateWindow();
	end
end

function SettingsPanelMixin:DiscardRevertableSettings()
	self.revertableSettings = nil;
	self:WipeModifiedTable();
	self:CancelPendingRevertTimer();
end

function SettingsPanelMixin:RevertSettings()
	local saveBindings = false;
	local gxRestart = false;
	local windowUpdate = false;

	for index, data in ipairs(self.revertableSettings) do
		local setting = data.setting;
		saveBindings = saveBindings or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.SaveBindings);
		gxRestart = gxRestart or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.GxRestart);
		windowUpdate = windowUpdate or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.UpdateWindow);

		local immediate = true;
		local originalValue = data.originalValue;
		securecallfunction(setting.SetValue, setting, originalValue, immediate);
	end

	self:WipeModifiedTable();
	self:CancelPendingRevertTimer();
	self:FinalizeCommit(saveBindings, gxRestart, windowUpdate);
end

function SettingsPanelMixin:CancelPendingRevertTimer()
	if self.Timer then
		self.Timer:Cancel();
		self.Timer = nil;
	end
end

function SettingsPanelMixin:SetAllSettingsToDefaults()
	local keys = {GetCVarDefault("VoicePushToTalkKeybind")}
	C_VoiceChat.SetPushToTalkBinding(keys);

	local saveBindings = false;
	local gxRestart = false;
	local windowUpdate = false;

	local settings = {};
	for setting, category in pairs(self.settings) do
		table.insert(settings, setting);
	end
	SortTableByCommitOrder(settings);

	for index, setting in ipairs(settings) do
		if securecallfunction(setting.SetValueToDefault, setting) then
			saveBindings = saveBindings or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.SaveBindings);
			gxRestart = gxRestart or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.GxRestart);
			windowUpdate = windowUpdate or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.UpdateWindow);
		end
	end

	self:CallDefaultOnCanvases();
	self:WipeModifiedTable();
	self:CheckApplyButton();
	self:FinalizeCommit(saveBindings, gxRestart, windowUpdate);

	Settings.SafeLoadBindings(Enum.BindingSet.Default);

	EventRegistry:TriggerEvent("Settings.Defaulted");
end

function SettingsPanelMixin:SetCurrentCategorySettingsToDefaults()
	local saveBindings = false;
	local gxRestart = false;
	local windowUpdate = false;

	local settings = {};
	local currentCategory = self:GetCurrentCategory();
	for setting, category in pairs(self.settings) do
		if category == currentCategory then
			table.insert(settings, setting);
		end
	end
	SortTableByCommitOrder(settings);

	for index, setting in ipairs(settings) do
		if securecallfunction(setting.SetValueToDefault, setting) then
			saveBindings = saveBindings or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.SaveBindings);
			gxRestart = gxRestart or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.GxRestart);
			windowUpdate = windowUpdate or securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.UpdateWindow);
		end
		self.modified[setting] = nil;
	end

	for _, category in ipairs(self:GetAllCategories()) do
		if category == currentCategory then
			local layout = self:GetLayout(category);
			local layoutType = layout:GetLayoutType();
			if layoutType == SettingsLayoutMixin.LayoutType.Canvas then
				local frame = layout:GetFrame();
				securecallfunction(CallDefaultOnFrame, frame);
			end
		end
	end

	self:FinalizeCommit(saveBindings, gxRestart, windowUpdate);

	self:CheckApplyButton();

	if currentCategory == self.keybindingsCategory then
		Settings.SafeLoadBindings(Enum.BindingSet.Default);
	end

	EventRegistry:TriggerEvent("Settings.CategoryDefaulted", currentCategory);
end

function SettingsPanelMixin:HasUnappliedSettings()
	for setting in pairs(self.modified) do
		if securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.Apply) and not securecallfunction(setting.HasCommitFlag, setting, Settings.CommitFlag.IgnoreApply) then
			return true;
		end
	end
	return false;
end

function SettingsPanelMixin:CheckApplyButton()
	self:SetApplyButtonEnabled(self:HasUnappliedSettings());
end

function SettingsPanelMixin:ForEachCanvas(func)
	local function CallOnCanvases(index, category, func)
		local layout = self:GetLayout(category);
		if layout:GetLayoutType() == SettingsLayoutMixin.LayoutType.Canvas then
			xpcall(func, CallErrorHandler, layout:GetFrame());
		end

		local subcategories = securecallfunction(category.GetSubcategories, category);
		secureexecuterange(subcategories, CallOnCanvases, func);
	end

	local allCategories = securecallfunction(self.GetAllCategories, self);
	secureexecuterange(allCategories, CallOnCanvases, func);
end

function SettingsPanelMixin:SetApplyButtonEnabled(enabled)
	self.ApplyButton:SetEnabled(enabled);
	self.ApplyButton:SetShown(enabled);
end

function SettingsPanelMixin:WipeModifiedTable()
	wipe(self.modified);
	self:CheckApplyButton();
end

function SettingsPanelMixin:CommitCanvases()
	self:ForEachCanvas(function(frame)
		if frame.OnCommit then
			frame:OnCommit();
		end
	end);
end

function SettingsPanelMixin:CallDefaultOnCanvases()
	self:ForEachCanvas(function(frame)
		CallDefaultOnFrame(frame);
	end);
end

function SettingsPanelMixin:CallRefreshOnCanvases()
	self:ForEachCanvas(function(frame)
		CallRefreshOnFrame(frame);
	end);
end

function SettingsPanelMixin:FindInitializersMatchingSearchText(searchText)
	searchText = searchText:upper();

	local words = { searchText };
	for word in string.gmatch(searchText, "([^, ]+)") do
		table.insert(words, word);
	end

	-- Score is the number of matching characters.
	local scoredInitializers = {};
	local function ParseCategory(category, parentCategory)
		local layout = self:GetLayout(category);
		local redirectCategory = category.redirectCategory or category;
		if layout:IsVerticalLayout() then
			for _, initializer in layout:EnumerateInitializers() do
				if not initializer:IsSearchIgnoredInLayout(layout) and initializer:ShouldShow() then
					local score = initializer:MatchesSearchTags(words);
					if score then
						if not scoredInitializers[score] then
							scoredInitializers[score] = {};
						end

						table.insert(scoredInitializers[score], { initializer = initializer, category = redirectCategory });
					end
				end
			end
		end
	end

	for _, category in ipairs(self:GetAllCategories()) do
		ParseCategory(category);

		for _, subcategory in EnumerateTaintedKeysTable(category:GetSubcategories()) do
			ParseCategory(subcategory, category);
		end
	end

	local matchScores = GetKeysArray(scoredInitializers);
	table.sort(matchScores, function(a, b) return a > b end);

	-- Shuffle all of the initializers that are now ordered by match score into their respective category.
	local scoredInitializersList = {};
	local initializerLists = {};
	local found = {};
	for _, score in ipairs(matchScores) do
		for _, match in ipairs(scoredInitializers[score]) do
			local category = match.category;

			local list = scoredInitializersList[category];
			if not list then
				list = {};
				scoredInitializersList[category] = list;
				table.insert(initializerLists, list);
			end

			if not found[category] then
				found[category] = true;
				table.insert(list, CreateSettingsListSearchCategoryInitializer(category));
			end

			table.insert(list, match.initializer);
		end
	end

	-- Flatten the category lists back into a single array.
	local initializers = {};
	for index, categoryList in ipairs(initializerLists) do
		tAppendAll(initializers, categoryList);
	end

	return initializers;
end


function SettingsPanelMixin:OnSearchTextChanged()
	local initializing = (self.searchText == nil);

	local text = self.SearchBox:GetText();
	if text == self.searchText then
		return;
	end

	self.searchText = text;

	if initializing then
		return;
	end

	local hasText = self.SearchBox:HasText();
	if not hasText then
		self:DisplayCategory(self:GetCurrentCategory());
		local settingsList = self:GetSettingsList();
		settingsList.Header.DefaultsButton:Show();
	elseif text and string.len(text) > 0 then
		local initializers = self:FindInitializersMatchingSearchText(text);
		local layout = CreateVerticalLayout();

		local added = {};

		for _, initializer in ipairs(initializers) do
			local parentInitializer = initializer.parentInitializer;
			if parentInitializer and not added[parentInitializer] then
				added[parentInitializer] = true;
				layout:AddInitializer(parentInitializer);
			end

			if not added[initializer] then
				added[initializer] = true;
				layout:AddInitializer(initializer);
			end
		end

		local settingsList = self:GetSettingsList();
		local searchSuccess = not layout:IsEmpty();
		if not searchSuccess then
			layout:AddInitializer(CreateSettingsListSectionHeaderInitializer(SETTINGS_SEARCH_NOTHING_FOUND));
		end

		settingsList.Header.Title:SetText(SETTINGS_SEARCH_RESULTS);

		self:DisplayLayout(layout);

		settingsList.Header.DefaultsButton:SetShown(not hasText);
		settingsList.Header.TutorialButton:SetShown(not hasText);
		self:ClearActiveCategoryTutorial();
	end
end

function SettingsPanelMixin:ClearSearchBox()
	self.SearchBox:SetText("");
end

local function ReserveVariable(setting)
	local variable = setting:GetVariable();
	if reservedVariables[variable] then
		return false, variable;
	end

	reservedVariables[variable] = true;
	return true, variable;
end

function SettingsPanelMixin:RegisterSetting(category, setting)
	local success, variable = securecallfunction(ReserveVariable, setting);
	if not success then
		error(string.format("Setting variable '%s' was previously registered.", variable));
	end

	self.settings[setting] = category;

	SettingsInbound.RegisterOnSettingValueChanged(variable);
end

function SettingsPanelMixin:RegisterInitializer(category, initializer)
	local layout = self.categoryLayouts[category];
	layout:AddInitializer(initializer);
end

function SettingsPanelMixin:AssignLayoutToCategory(category, layout)
	self.categoryLayouts[category] = layout;
end

function SettingsPanelMixin:GetLayout(category)
	return self.categoryLayouts[category];
end

function SettingsPanelMixin:GetSetting(variable)
	for setting, category in pairs(self.settings) do
		local settingVariable = securecallfunction(setting.GetVariable, setting);
		if settingVariable == variable then
			return setting;
		end
	end
	return nil;
end

function SettingsPanelMixin:OnSettingValueChanged(setting, value)
	assert(value ~= nil);
	if not self:IsShown() then
		return;
	end

	local isModified = securecallfunction(setting.IsModified, setting);
	self.modified[setting] = isModified and setting or nil;
	securecallfunction(setting.UpdateIgnoreApplyFlag, setting);
	self:CheckApplyButton();

	SettingsInbound.RepairDisplay();
end

function SettingsPanelMixin:RepairDisplay()
	local layout = self.currentLayout;
	if layout then
		local layoutType = layout:GetLayoutType();
		if layoutType == SettingsLayoutMixin.LayoutType.Vertical then
			local settingsList = self:GetSettingsList();
			securecallfunction(settingsList.RepairDisplay, settingsList, layout);
		end
	end
end

function SettingsPanelMixin:GetAllCategories()
	return self:GetCategoryList():GetAllCategories();
end

function SettingsPanelMixin:RegisterCategory(category, group, addon)
	self:GetCategoryList():AddCategory(category, group, addon);
end

function SettingsPanelMixin:GetCategory(categoryName)
	return self:GetCategoryList():GetCategory(categoryName);
end

function SettingsPanelMixin:GetOrCreateGroup(group, order)
	return self:GetCategoryList():GetOrCreateGroup(group, order);
end

function SettingsPanelMixin:SelectFirstCategory(force)
	local categories = self:GetAllCategories();
	if #categories > 0 then
		self:SelectCategory(categories[1], force);
	end
end

function SettingsPanelMixin:SelectCategory(category, force)
	if force or (self:GetCurrentCategory() ~= category) then
		self:ClearActiveCategoryTutorial();
		self:ClearSearchBox();
		self:ClearOutputText();
		self:ClearCurrentCategoryCanvas();
		self:SetCurrentCategory(category);
		self:DisplayCategory(category);

		EventRegistry:TriggerEvent("Settings.CategoryChanged", category);
	end
end

function SettingsPanelMixin:SetCurrentCategory(category)
	local newMode = self:GetCategoryList():SetCurrentCategory(category);
	self.tabsGroup:SelectAtIndex(newMode);

	self:CheckApplyButton();
end

function SettingsPanelMixin:GetCategoryList()
	return self.CategoryList;
end

function SettingsPanelMixin:GetSettingsList()
	return self.Container.SettingsList;
end

function SettingsPanelMixin:GetSettingsCanvas()
	return self.Container.SettingsCanvas;
end

function SettingsPanelMixin:DisplayCategory(category)
	if not category then
		return;
	end

	local settingsList = self:GetSettingsList();
	settingsList.Header.Title:SetText(category:GetName());

	-- Help Tip
	local categoryTutorial = category:GetCategoryTutorialInfo();
	settingsList.Header.TutorialButton:SetShown(categoryTutorial);

	if categoryTutorial then
		settingsList.Header.TutorialButton.Ring:Hide();

		settingsList.Header.TutorialButton:SetScript("OnEnter", function()
			GameTooltip:SetOwner(settingsList.Header.TutorialButton, "ANCHOR_RIGHT",-22,-22);
			GameTooltip:SetText(categoryTutorial.tooltip);
			GameTooltip:Show();
		end);

		settingsList.Header.TutorialButton:SetScript("OnLeave", function()
			GameTooltip_Hide();
		end);

		settingsList.Header.TutorialButton:SetScript("OnClick", categoryTutorial.callback);
	else
		settingsList.Header.TutorialButton:SetScript("OnEnter", nil);
		settingsList.Header.TutorialButton:SetScript("OnLeave", nil);
		settingsList.Header.TutorialButton:SetScript("OnClick", nil);
	end

	local layout = self:GetLayout(category);
	self:DisplayLayout(layout);
end

function SettingsPanelMixin:SetCurrentLayout(layout)
	self.currentLayout = layout;
end

function SettingsPanelMixin:GetCurrentLayout()
	return self.currentLayout;
end

function SettingsPanelMixin:ClearCurrentCategoryCanvas()
	local currentCategory = self:GetCurrentCategory();
	if not currentCategory then
		return;
	end

	local layout = self:GetLayout(currentCategory);
	if layout:GetLayoutType() == SettingsLayoutMixin.LayoutType.Canvas then
		local frame = layout:GetFrame();
		frame:SetParent(nil);
		frame:ClearAllPoints();
		frame:Hide();
	end
end

function SettingsPanelMixin:DisplayLayout(layout)
	if not layout then
		return;
	end

	SettingsInbound.SetCurrentLayout(layout);

	local settingsList = self:GetSettingsList();
	local settingsCanvas = self:GetSettingsCanvas();
	local layoutType = layout:GetLayoutType();
	if layoutType == SettingsLayoutMixin.LayoutType.Vertical then
		local initializers = layout:GetInitializers();
		settingsList:Display(initializers);
		settingsList:Show();
		settingsCanvas:Hide();
	elseif layoutType == SettingsLayoutMixin.LayoutType.Canvas then
		local frame = layout:GetFrame();
		frame:SetParent(settingsCanvas);
		frame:ClearAllPoints();

		local anchors = layout:GetAnchorPoints();
		if #anchors > 0 then
			for index, tbl in ipairs(anchors) do
				frame:SetPoint(tbl.p, tbl.x, tbl.y);
			end
		else
			frame:SetAllPoints(settingsCanvas);
		end

		frame:Show();

		securecallfunction(CallRefreshOnFrame, frame);

		settingsCanvas:Show();
		settingsList:Hide();
	end
end

function SettingsPanelMixin:GetCurrentCategory()
	return self:GetCategoryList():GetCurrentCategory();
end

function SettingsPanelMixin:RenewKeybinds()
	EventRegistry:TriggerEvent("Settings.UpdateKeybinds");
end

function SettingsPanelMixin:SetOutputText(text)
	self.OutputText:SetText(text);
end

function SettingsPanelMixin:ClearOutputText()
	self.OutputText:SetText(nil);
end

function SettingsPanelMixin:OnKeybindStoppedListening(action, slotIndex)
	self:RenewKeybinds();

	self.SearchBox:Enable();

	self.InputBlocker:Hide();
	self:GetSettingsList():SetInputBlockerShown(false);

	self:SetOutputText(nil);

	EventRegistry:TriggerEvent("Settings.UnparentBindingsToInputBlocker");
end

function SettingsPanelMixin:OnKeybindStartedListening(action, slotIndex)
	self.SearchBox:Disable();

	self.InputBlocker:Show();
	self.InputBlocker:SetFrameStrata("DIALOG");

	local settingsList = self:GetSettingsList();
	settingsList:SetInputBlockerShown(true);

	self:SetOutputText(SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL:format(GetBindingName(action), GetBindingText("ESCAPE")));

	EventRegistry:TriggerEvent("Settings.ReparentBindingsToInputBlocker", settingsList:GetInputBlocker());
end

function SettingsPanelMixin:OnKeybindUnbindFailed(action, unbindAction, unbindSlotIndex)
	local errorFormat = unbindSlotIndex == 1 and PRIMARY_KEY_UNBOUND_ERROR or KEY_UNBOUND_ERROR;
	self:SetOutputText(errorFormat:format(GetBindingName(unbindAction)));
end

function SettingsPanelMixin:OnKeybindRebindFailed(action)
	self:SetOutputText(KEYBINDINGFRAME_MOUSEWHEEL_ERROR);
end

function SettingsPanelMixin:OnKeybindRebindSuccess(action)
	self:SetOutputText(KEY_BOUND);
end

function SettingsPanelMixin:ClearActiveCategoryTutorial()
	if self.activeCategoryTutorial then
		local settingsList = self:GetSettingsList();
		settingsList.Header.TutorialButton:Click();
	end
end
