-- User Settings
local CLOSE_AFTER_CLICK = true;
local GRID_LAYOUT = true;           --Multi-row, not single row
local MAX_BUTTON_PER_ROW = 4;
------------------

local _, addon = ...
local API = addon.API;
local L = addon.L;
local CallbackRegistry = addon.CallbackRegistry;
local CooldownUtil = addon.CooldownUtil;

local tinsert = table.insert;
local InCombatLockdown = InCombatLockdown;
local CreateFrame = CreateFrame;
local GetCVarBool = C_CVar.GetCVarBool;
local GetItemCount = C_Item.GetItemCount;
local PlayerHasToy = PlayerHasToy or API.Nop;
local C_PetJournal = C_PetJournal;
local GetSpellTexture = C_Spell.GetSpellTexture;
local IsSpellDataCached = C_Spell.IsSpellDataCached;
local GetItemCraftingQuality = API.GetItemCraftingQuality;
local CanPlayerPerformAction = API.CanPlayerPerformAction;
local find = string.find;
local gsub = string.gsub;
local UIParent = UIParent;

local ActionButtonPool = {};
local VisualButtonMixin = {};

local SpellFlyout = CreateFrame("Frame", nil, UIParent);
addon.SecureSpellFlyout = SpellFlyout;


local function SetupClampedFrame(frame)
	local offset = 8;
	frame:SetClampedToScreen(true);
	frame:SetClampRectInsets(-offset, offset, offset, -offset);
end

local UseSpellID = {
	-- /cast only supports spell name, it breaks if the player knows two spells of the same name
	-- We use SetAttribute("type", "spell") for these cases, the tradeoff is the flyout can no longer close after click during combat

	[440977]  = true, -- Sharpen Your Knife
	[1223388] = true,
};


local IS_CLASSIC = addon.IS_CLASSIC;
local ACTION_BUTTON_SIZE = 45;
if IS_CLASSIC then
	ACTION_BUTTON_SIZE = 36;
end

--Flyout Cursor OffsetX = 24.0
--Flyout Cursor OffsetY = 26.0


do --SpellFlyout Main
	SpellFlyout:Hide();
	SpellFlyout:SetFrameStrata("FULLSCREEN_DIALOG");

	function SpellFlyout:Clear()
		self.actions = nil;
		self:Hide();
		self:ClearAllPoints();
	end

	function SpellFlyout:UpdateSpellCooldowns()
		CooldownUtil.UpdateSpellButtonCooldowns(self.SpellButtons);
	end

	function SpellFlyout:UpdateItemCooldowns()
		CooldownUtil.UpdateItemButtonCooldowns(self.ItemButtons);
	end

	function SpellFlyout:SetArrowDirection(direction)
		self.FlyoutArrow:ClearAllPoints();
		if direction == "down" then
			self.FlyoutArrow:SetPoint("CENTER", self, "BOTTOMLEFT", 2 + 45/2, -1);
			self.FlyoutArrow:SetTexCoord(48/512, 96/512, 0, 48/512);
		elseif direction == "up" then
			self.FlyoutArrow:SetPoint("CENTER", self, "TOPLEFT", 2 + 45/2, 1);
			self.FlyoutArrow:SetTexCoord(0/512, 48/512, 0, 48/512);
		elseif direction == "right" then
			self.FlyoutArrow:SetPoint("CENTER", self, "RIGHT", 1, 0);
			self.FlyoutArrow:SetTexCoord(144/512, 192/512, 0, 48/512);
		elseif direction == "left" then
			self.FlyoutArrow:SetPoint("CENTER", self, "LEFT", -1, 0);
			self.FlyoutArrow:SetTexCoord(96/512, 144/512, 0, 48/512);
		end
	end

	function SpellFlyout:IsFocused()
		if self:IsVisible() then
			if self:IsMouseOver() or (self.owner and self.owner:IsMouseMotionFocus()) then
				return true
			end
		end
		return false
	end

	function SpellFlyout:OnEvent(event, ...)
		if event == "SPELL_UPDATE_COOLDOWN" then
			self:UpdateSpellCooldowns();
		elseif event == "BAG_UPDATE_COOLDOWN" then
			self:UpdateItemCooldowns();
		elseif event == "BAG_UPDATE_DELAYED" then
			self:UpdateItemCount();
		end
	end
	SpellFlyout:SetScript("OnEvent", SpellFlyout.OnEvent);

	function SpellFlyout:OnShow()
		self:RegisterEvent("SPELL_UPDATE_COOLDOWN");
		self:RegisterEvent("BAG_UPDATE_COOLDOWN");
		self:RegisterEvent("BAG_UPDATE_DELAYED");
	end

	function SpellFlyout:OnHide()
		self:Clear();
		self:UnregisterEvent("SPELL_UPDATE_COOLDOWN");
		self:UnregisterEvent("BAG_UPDATE_COOLDOWN");
		self:UnregisterEvent("BAG_UPDATE_DELAYED");
	end

	function SpellFlyout:OnMouseDown(button)

	end

	function SpellFlyout:Init()
		self.Init = nil;

		--Create Background. TO-DO convert into a template?
		--This texture has a large shadow
		local bg = addon.CreateNineSliceFrame(self, "NineSlice_GenericBox_Black_Shadowed");
		local buttonScale = ACTION_BUTTON_SIZE / 45;
		bg:SetUsingParentLevel(true);
		bg:CoverParent(16 * buttonScale);
		bg.pieces[1]:SetTexCoord(0/128, 48/128, 0/128, 48/128);
		bg.pieces[2]:SetTexCoord(48/128, 80/128, 0/128, 48/128);
		bg.pieces[3]:SetTexCoord(80/128, 128/128, 0/128, 48/128);
		bg.pieces[4]:SetTexCoord(0/128, 48/128, 48/128, 80/128);
		bg.pieces[5]:SetTexCoord(48/128, 80/128, 48/128, 80/128);
		bg.pieces[6]:SetTexCoord(80/128, 128/128, 48/128, 80/128);
		bg.pieces[7]:SetTexCoord(0/128, 48/128, 80/128, 128/128);
		bg.pieces[8]:SetTexCoord(48/128, 80/128, 80/128, 128/128);
		bg.pieces[9]:SetTexCoord(80/128, 128/128, 80/128, 128/128);
		if IS_CLASSIC then
			bg:SetCornerSize(48 * buttonScale);
		else
			bg:SetCornerSize(48);
		end

		self:EnableMouse(true);
		self:EnableMouseMotion(true);

		local file = "Interface/AddOns/Plumber/Art/Frame/SpellFlyout";
		self.FlyoutArrow = self:CreateTexture(nil, "OVERLAY");
		self.FlyoutArrow:SetTexture(file)
		self.FlyoutArrow:SetTexCoord(0, 48/512, 0, 48/512);
		self.FlyoutArrow:SetSize(24, 24);
		self.FlyoutArrow:Hide();

		self.Note = self:CreateFontString(nil, "OVERLAY", "GameFontRed");
		self.Note:SetPoint("CENTER", self, "CENTER", 0, 0);
		self.Note:SetJustifyH("CENTER");
		self.Note:SetJustifyH("CENTER");
		self.Note:Hide();

		self:SetScript("OnShow", self.OnShow);
		self:SetScript("OnHide", self.OnHide);
		self:SetScript("OnEvent", self.OnEvent);
		self:SetScript("OnMouseDown", self.OnMouseDown);

		SetupClampedFrame(self);
	end

	function SpellFlyout:ShowActions(actions)
		self.actions = actions;
		self.SpellButtons = {};
		self.ItemButtons = {};

		if self.Init then
			self:Init();
		end

		local buttonSize = ACTION_BUTTON_SIZE;
		local gap = 2;
		local h, v = 0, 0;

		if actions and #actions > 0 then
			self.Note:Hide();
			local level = 9;
			local n = 0;
			local action;
			local anySpell, anyItem;

			for index, button in ipairs(ActionButtonPool.visualButtons) do
				button:ClearAction();
				action = actions[index];
				if action then
					n = n + 1;
					h = h + 1;

					if GRID_LAYOUT then
						if h > MAX_BUTTON_PER_ROW then
							h = 1;
							v = v + 1;
						end
					end

					button:SetAction(action);
					button:SetButtonState("NORMAL");
					button:SetPoint("TOPLEFT", self, "TOPLEFT", gap + (h - 1) * (buttonSize + gap), -gap - v * (buttonSize + gap));
					button:SetFrameLevel(level);

					if action.actionType == "spell" then
						anySpell = true;
						tinsert(self.SpellButtons, button);
					elseif action.actionType == "item" then
						anyItem = true;
						tinsert(self.ItemButtons, button);
					end
				end
			end

			if v > 0 then
				h = MAX_BUTTON_PER_ROW;
			end

			if anySpell then
				self:UpdateSpellCooldowns();
			end
			if anyItem then
				self:UpdateItemCooldowns();
				self:UpdateItemCount();
			end

			self:SetSize(h * (buttonSize + gap) + gap, (v + 1) * (buttonSize + gap) + gap);
		else
			self.Note:SetText(L["PlumberMacro Error NoAction"]);
			self.Note:Show();
			h = 4;
			v = 0;

			self.Note:SetWidth(3 * (buttonSize + gap) - gap);
			local textWidth = API.Round(self.Note:GetWrappedWidth());
			local textHeight = API.Round(self.Note:GetHeight());
			self:SetSize(textWidth + 32, textHeight + 24);
			--self:SetSize(h * (buttonSize + gap) + gap, (v + 1) * (buttonSize + gap) + gap);
		end

		local x, y = API.GetScaledCursorPositionForFrame(self);
		self:ClearAllPoints();
		self:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", x - 24.0, y + 26.0);
		self:SetFrameStrata("FULLSCREEN_DIALOG");
		self:SetFrameLevel(7);
		self:EnableMouse(true);
		self:Show();
	end

	function SpellFlyout:SetOwner(owner)
		--owner is ActionBarButton
		self.owner = owner;
	end

	function SpellFlyout:UpdateItemCount()
		if self.ItemButtons then
			for i, button in ipairs(self.ItemButtons) do
				button:UpdateCount();
			end
		end
	end
end


do  --VisualButtonMixin
	function VisualButtonMixin:OnLoad()
		local file = "Interface/AddOns/Plumber/Art/Frame/SpellFlyout";

		self.NormalTexture:SetTexture(file);
		self.NormalTexture:SetTexCoord(0/512, 96/512, 48/512, 144/512);
		self.PushedTexture:SetTexture(file);
		self.PushedTexture:SetTexCoord(96/512, 192/512, 48/512, 144/512);
		self.HighlightTexture:SetTexture(file);
		self.HighlightTexture:SetTexCoord(192/512, 288/512, 48/512, 144/512);
		self.IconOverlay:SetTexture(file);
		self.IconOverlay:SetTexCoord(0/512, 48/512, 144/512, 192/512);

		self.NormalTexture:SetDrawLayer("OVERLAY");
		self.PushedTexture:SetDrawLayer("OVERLAY");

		self:SetScript("OnEnter", self.OnEnter);
		self:SetScript("OnLeave", self.OnLeave);

		if IS_CLASSIC then
			self:SetEffectiveSize(ACTION_BUTTON_SIZE);
		end
	end

	function VisualButtonMixin:SetAction(action)
		self.id = action.id;
		self.Icon:SetTexture(action.icon);
		self.actionType = action.actionType;
		self.macroText = action.macroText;
		self.rawMacroText = action.rawMacroText;
		self.tooltipLineText = action.tooltipLineText;

		if self.actionType == "spell" then
			self.tooltipMethod = "SetSpellByID";
			if not CanPlayerPerformAction("spell", self.id) then
				self:SetUsableVisual(false);
			end

		elseif self.actionType == "item" then
			if API.IsToyItem(self.id) then
				self.isToy = true;
				self.tooltipMethod = "SetToyByItemID";
			else
				self.tooltipMethod = "SetItemByID";
			end
			local craftingQuality = GetItemCraftingQuality(self.id);
			self:SetCraftingQuality(craftingQuality);
		elseif self.actionType == "profession" then
			self:SetPrimaryProfession(action.id);
		elseif self.actionType == "mount" then
			--Only RandomFavoriteMount use this method
			--Regular mounts have been converted to spells
			self:SetMount(self.id);
		elseif self.actionType == "teleportHome" then
			self:SetTeleportHome(self.id);
		elseif self[action.actionType] then
			self[action.actionType](self, action.id);
		end
	end

	function VisualButtonMixin:UpdateCount()
		if self.actionType == "item" then
			local count = GetItemCount(self.id);
			if self.isToy then
				count = PlayerHasToy(self.id) and 1 or 0;
			end
			if count > 1 then
				self.Count:SetText(count);
			else
				self.Count:SetText(nil);
			end
			self:SetUsableVisual(count > 0);
		end
	end

	function VisualButtonMixin:SetUsableVisual(state)
		if state then
			self.Icon:SetVertexColor(1, 1, 1);
			self.Icon:SetDesaturated(false);
			self.IconOverlay:SetVertexColor(1, 1, 1);
		else
			self.Icon:SetVertexColor(0.5, 0.5, 0.5);
			self.Icon:SetDesaturated(true);
			self.IconOverlay:SetVertexColor(0.8, 0.8, 0.8);
		end
	end

	function VisualButtonMixin:SetCraftingQuality(quality)
		if quality then
			self.IconOverlay:Show();
			if quality == 1 then
				self.IconOverlay:SetTexCoord(0/512, 48/512, 144/512, 192/512);
			elseif quality == 2 then
				self.IconOverlay:SetTexCoord(48/512, 96/512, 144/512, 192/512);
			elseif quality == 3 then
				self.IconOverlay:SetTexCoord(96/512, 144/512, 144/512, 192/512);
			elseif quality == 4 then
				self.IconOverlay:SetTexCoord(144/512, 192/512, 144/512, 192/512);
			elseif quality == 5 then
				self.IconOverlay:SetTexCoord(192/512, 240/512, 144/512, 192/512);
			else
				self.IconOverlay:Hide();
				return
			end
		else
			self.IconOverlay:Hide();
		end
	end

	function VisualButtonMixin:OnEnter()
		self:ShowTooltip();
	end

	function VisualButtonMixin:OnLeave()
		GameTooltip:Hide();
		self:SetButtonState("NORMAL");
	end

	function VisualButtonMixin:ShowTooltip()
		if self.tooltipMethod then
			local tooltip = GameTooltip;
			tooltip:SetOwner(self, "ANCHOR_RIGHT");
			tooltip[self.tooltipMethod](tooltip, self.id);
			tooltip:Show();
			self.UpdateTooltip = self.ShowTooltip;
			return true
		elseif self.tooltipText then
			local tooltip = GameTooltip;
			tooltip:SetOwner(self, "ANCHOR_RIGHT");
			tooltip:SetText(self.tooltipText, 1, 1, 1, 1, true);
			tooltip:Show();
			self.UpdateTooltip = nil;
			return true
		elseif self.tooltipFunc then
			local tooltip = GameTooltip;
			tooltip:SetOwner(self, "ANCHOR_RIGHT");
			self.tooltipFunc(tooltip, self.id);
			tooltip:Show();
			self.UpdateTooltip = self.ShowTooltip;
			return true
		else
			self.UpdateTooltip = nil;
		end
	end

	function VisualButtonMixin:ClearAction()
		if self.actionType then
			self.Icon:SetTexture(nil);
			self.id = nil;
			self.actionType = nil;
			self.tooltipMethod = nil;
			self.tooltipFunc = nil;
			self.macroText = nil;
			self.rawMacroText = nil;
			self.tooltipLineText = nil;
			self.tooltipText = nil;
			self.isToy = nil;
			if self.Cooldown then
				self.Cooldown:Clear();
				self.Cooldown:Hide();
			end
			if self.Count then
				self.Count:SetText(nil);
			end
			self:SetUsableVisual(true);
			self:SetCraftingQuality(nil);
		end
	end

	function VisualButtonMixin:SetEffectiveSize(size)
		local s = size / 45;
		self:SetSize(45 * s, 45 * s);
		self.Icon:SetSize(40 * s, 40 * s);
		self.NormalTexture:SetSize(48 * s, 48 * s);
		self.PushedTexture:SetSize(48 * s, 48 * s);
		self.HighlightTexture:SetSize(48 * s, 48 * s);
		self.IconOverlay:SetSize(24 * s, 24 * s);
	end

	function VisualButtonMixin:IsDataCached()
		if self.actionType == "spell" then
			return IsSpellDataCached(self.id);
		elseif self.actionType == "item" then
			return C_Item.IsItemDataCachedByID(self.id);
		end
		return true
	end

	--Default Actions
	function VisualButtonMixin:SetRandomFavoritePet()
		self.tooltipMethod = "SetSpellByID";
		self.id = 243819;
		self.Icon:SetTexture(GetSpellTexture(self.id));
	end

	function VisualButtonMixin:SetRandomPet()
		self.tooltipText = SUMMON_RANDOM_PET;
		self.Icon:SetTexture(613074);
	end

	function VisualButtonMixin:SetDismissPet()
		self.tooltipText = L["Dismiss Battle Pet"];
		self.Icon:SetTexture(653220);
	end

	function VisualButtonMixin:SetSummonPet(petNameOrGUID)
		local speciesID, petGUID = C_PetJournal.FindPetIDByName(petNameOrGUID);
		if petGUID then
			self.tooltipMethod = "SetCompanionPet";
			self.id = petGUID;
			local icon = select(9, C_PetJournal.GetPetInfoByPetID(petGUID));
			self.Icon:SetTexture(icon);
		elseif speciesID then   --Summoning not owned pet causes errors
			local speciesName, icon = C_PetJournal.GetPetInfoBySpeciesID(speciesID);
			self.tooltipText = speciesName;
			self.Icon:SetTexture(icon);
			self:SetUsableVisual(false);
			self.macroText = nil;
		end
	end

	function VisualButtonMixin:SetCustomEmote(customEmote)
		self.tooltipText = (EMOTE or "Emote")..": "..customEmote;
	end

	function VisualButtonMixin:SetPrimaryProfession(spellID)
		if spellID and spellID > 0 then
			self.tooltipMethod = "SetSpellByID";
		else
			self.tooltipText = self.tooltipLineText;
		end
	end

	function VisualButtonMixin:SetMount(spellID)
		self.tooltipMethod = "SetMountBySpellID";
		self.Icon:SetTexture(GetSpellTexture(spellID));
	end

	function VisualButtonMixin:SetTeleportHome(id)
		self.tooltipMethod = nil;
		self.tooltipText = nil;
		self.id = id;
		local h = addon.Housing;
		if h then
			self.tooltipFunc = h.SetupTeleportTooltip;
			h.SetupActionButtonCooldown(self, self.id);
		else
			self.tooltipFunc = nil;
		end
	end

	function SpellFlyout:PopulateButtonMixin(externalMixin)
		for k, v in pairs(VisualButtonMixin) do
			if not externalMixin[k] then
				externalMixin[k] = v;
			end
		end
		return externalMixin
	end
end



---- Secure Objects ----
local SecureRootContainer = CreateFrame("Frame", "PlumberSecureFlyoutContainer", UIParent, "SecureHandlerMouseUpDownTemplate, SecureHandlerShowHideTemplate", "SecureHandlerClickTemplate", "PlumberPropagateMouseTemplate");    --Name is need for OverrideBindingClick
SecureRootContainer:Hide();
SecureRootContainer:SetAllPoints(true);
SecureRootContainer:SetFrameStrata("BACKGROUND");
--SecureRootContainer:SetPropagateMouseMotion(true);
--SecureRootContainer:SetPropagateMouseClicks(true);

SecureRootContainer:SetAttribute("_onmousedown", [=[
	self:Hide();
]=]);

SecureRootContainer:SetAttribute("_onmouseup", [=[

]=]);

SecureRootContainer:SetAttribute("_onclick", [=[
	self:Hide();
]=]);

SecureRootContainer:SetAttribute("_onshow", [=[
	self:SetBindingClick(true, "ESCAPE", self)
]=]);

SecureRootContainer:SetAttribute("_onhide", [=[
	self:ClearBindings();
]=]);

SpellFlyout:SetParent(SecureRootContainer);


local ActionButtonContainer = CreateFrame("Frame", nil, SecureRootContainer, "SecureHandlerStateTemplate");
ActionButtonContainer:SetFrameStrata("FULLSCREEN_DIALOG");
ActionButtonContainer:SetFixedFrameStrata(true);
SetupClampedFrame(ActionButtonContainer);


local SecureControllerPool = {};
do  --SecureControllerPool
	local VIRTUAL_BUTTON_NAME = "PLMR";

	SecureControllerPool.clickHandlers = {};
	SecureControllerPool.numHandlers = 0;
	SecureControllerPool.usedHandlers = 0;

	function SecureControllerPool:ReleaseClickHandlers()
		if InCombatLockdown() then
			return
		end

		self.usedHandlers = 0;
		self.emptyActionHandlerName = nil;

		for _, handler in ipairs(self.clickHandlers) do
			handler:SetAttribute("_onclick", nil);
		end
	end

	function SecureControllerPool:CreateClickHandler(id)
		local name = VIRTUAL_BUTTON_NAME..id;
		local handler = CreateFrame("Button", name, UIParent, "SecureHandlerClickTemplate");
		handler:Hide();
		handler:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0);
		handler:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", 0, 0);
		self:SetupHandler(handler);
		handler:SetID(id);
		return handler
	end

	function SecureControllerPool:AcquireClickHandler()
		local handler;
		self.usedHandlers = self.usedHandlers + 1;
		if self.usedHandlers > self.numHandlers then
			self.numHandlers = self.numHandlers + 1;
			local id = self.numHandlers;
			handler = self:CreateClickHandler(id);
			self.clickHandlers[id] = handler;
		else
			handler = self.clickHandlers[self.usedHandlers];
		end
		return handler
	end

	function SecureControllerPool:SetupHandler(handler)
		handler:RegisterForClicks("AnyUp");
		handler:SetFrameRef("MainFrame", SecureRootContainer);
		handler:SetFrameRef("UIParent", UIParent);
		handler:SetFrameRef("ActionButtonContainer", ActionButtonContainer);
		ActionButtonPool:AddFrameRef(handler);  --SetFrameRef("SecureButton")
	end

	local HANDLER_ONCLICK_FORMAT = [=[
		local frame = self:GetFrameRef("MainFrame");
		local handlerID = self:GetID();
		local show = (not frame:IsShown()) or (handlerID ~= frame:GetID());
		if show then
			frame:SetID(handlerID);
			local UIParent = self:GetFrameRef("UIParent");
			local uiWidth = UIParent:GetWidth();
			local uiHeight = UIParent:GetHeight();
			local xRatio, yRatio = self:GetMousePosition();
			if xRatio and yRatio then
				local numActions = self:GetAttribute("numActions");
				local ActionButtonContainer = self:GetFrameRef("ActionButtonContainer");
				local index = numActions + 1;

				local button = self:GetFrameRef("SecureButton"..index);
				while button do
					button:Hide();
					index = index + 1;
					button = self:GetFrameRef("SecureButton"..index);
				end

				local numButtons = 0;
				local buttonSize = %s;
				local useGridLayout = %s;
				local maxButtonPerRow = %s;
				local gap = 2;
				local h = 0;
				local v = 0;

				for i = 1, numActions do
					button = self:GetFrameRef("SecureButton"..i);
					if button then
						numButtons = numButtons + 1;

						local processed = false;
						if self:GetAttribute("shouldUseSpellID"..i) then
							local spellID = self:GetAttribute("actionID"..i);
							if spellID and spellID > 0 then
								processed = true;
								button:SetAttribute("type", "spell");
								button:SetAttribute("spell", spellID);
							end
						end

						if not processed then
							local macroText = self:GetAttribute("customMacroText"..i);
							button:SetAttribute("type", "macro");
							button:SetAttribute("macrotext", macroText);
						end

						button:SetFrameLevel(10);
						button:ClearAllPoints();
						h = h + 1;
						if useGridLayout then
							if h > maxButtonPerRow then
								h = 1;
								v = v + 1;
							end
						end
						button:SetPoint("TOPLEFT", ActionButtonContainer, "TOPLEFT", gap + (h - 1) * (buttonSize + gap), -gap - v * (buttonSize + gap));
						button:Show();
					end
				end

				if v > 0 then
					h = maxButtonPerRow;
				end

				local x = uiWidth * xRatio;
				local y = uiHeight * yRatio;

				if numActions == 0 then
					h = 4;
				end

				ActionButtonContainer:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", x - 24.0, y + 26.0);
				ActionButtonContainer:SetWidth(h * (buttonSize + gap) + gap);
				ActionButtonContainer:SetHeight((v + 1) * (buttonSize + gap) + gap);
				--ActionButtonContainer:RegisterAutoHide(2);
				frame:Show();
				ActionButtonContainer:Show();
			end
		else
			frame:Hide();
		end
	]=];

	local HANDLER_ONCLICK;

	function SecureControllerPool:UpdateLayout()
		HANDLER_ONCLICK = HANDLER_ONCLICK_FORMAT:format(ACTION_BUTTON_SIZE, tostring(GRID_LAYOUT), MAX_BUTTON_PER_ROW);
	end
	SecureControllerPool:UpdateLayout()

	function SecureControllerPool:AddActions(actions)
		local numActions = actions and #actions or 0;

		if numActions == 0 and self.emptyActionHandlerName then
			return self.emptyActionHandlerName;
		end

		local handler = self:AcquireClickHandler();
		local handlerName = handler:GetName();

		if numActions == 0 then
			self.emptyActionHandlerName = handlerName;
		end

		handler:SetAttribute("numActions", numActions);

		if numActions > 0 then
			local appendedMacro;
			if CLOSE_AFTER_CLICK then
				local closeFlyoutMacro = self:GetCloseFlyoutMacro();
				appendedMacro = "\n"..closeFlyoutMacro;
			end

			for i, action in ipairs(actions) do
				if appendedMacro then
					handler:SetAttribute("customMacroText"..i, (action.macroText or "")..appendedMacro);
				else
					handler:SetAttribute("customMacroText"..i, action.macroText);
				end

				if action.actionType == "spell" and action.id and UseSpellID[action.id] then
					handler:SetAttribute("shouldUseSpellID"..i, true);
				else
					handler:SetAttribute("shouldUseSpellID"..i, nil);
				end
				handler:SetAttribute("actionID"..i, action.id);
			end
		end

		handler:SetScript("PreClick", function()
			if not(SpellFlyout:IsShown() and SpellFlyout.actions == actions) then
				SpellFlyout:ShowActions(actions);
			end
			if not InCombatLockdown() then
				ActionButtonPool:UpdateClicks();
			end
			--Also triggered when clicking the handler to close the menu
		end);

		handler:SetAttribute("_onclick", HANDLER_ONCLICK);

		return handlerName
	end

	function SecureControllerPool:GetCloseFlyoutMacro()
		--We create a ClickHandler that closes the SecureRootContainer
		--This handler will not be reused/released by other drawer macro
		if not self.CloseFrameClickHandler then
			local id = 0;
			local handler = self:CreateClickHandler(id);
			self.CloseFrameClickHandler = handler;
			handler:SetAttribute("_onclick", [==[
				local frame = self:GetFrameRef("MainFrame");
				frame:Hide();
			]==]);
			self.clickToCloseMacro = "/click "..handler:GetName();
		end
		return self.clickToCloseMacro
	end


	function SpellFlyout:ReleaseClickHandlers()
		SecureControllerPool:ReleaseClickHandlers();
	end

	function SpellFlyout:AddActionsAndGetHandler(actions)
		return SecureControllerPool:AddActions(actions)
	end

	function SpellFlyout:RemoveClickHandlerFromMacro(body)
		local pattern = "/click%s+"..VIRTUAL_BUTTON_NAME.."%d+";
		local anyChange;
		if find(body, pattern) then
			body = gsub(body, pattern, "");
			while find(body, "\n\n") do
				body = gsub(body, "\n\n", "\n");
			end
			body = gsub(body, "%s+$", "");
			anyChange = true;
		else
			anyChange = false;
		end
		return body, anyChange
	end

	function SpellFlyout:Close()
		if not InCombatLockdown() then
			if SecureRootContainer:IsShown() then
				SecureRootContainer:Hide();
			end
		end
	end
end

do  --SecureHandler
	ActionButtonPool.actionButtons = {};
	ActionButtonPool.visualButtons = {};

	local function ActionButton_PostClick()
		if CLOSE_AFTER_CLICK and not InCombatLockdown() then
			SecureRootContainer:Hide();
		end
	end

	function ActionButtonPool:InitButtons(numButtons)
		local n = #self.actionButtons;
		if numButtons > n then
			local diff = numButtons - n;
			local index, button;
			for i = 1, diff do
				index = n + i;
				button = CreateFrame("Button", nil, ActionButtonContainer, "PlumberSecureActionBarButtonTemplate");
				self.actionButtons[index] = button;
				button:SetSize(ACTION_BUTTON_SIZE, ACTION_BUTTON_SIZE);
				button:SetScript("PostClick", ActionButton_PostClick);

				local vb = CreateFrame("Button", nil, UIParent, "PlumberActionBarButtonTemplate");
				vb:Hide();
				self.visualButtons[index] = vb;
				button.VisualButton = vb;
				API.Mixin(vb, VisualButtonMixin);
				vb:SetFrameStrata("FULLSCREEN_DIALOG");
				vb:OnLoad();
			end
		end
		self:UpdateClicks();
	end

	function ActionButtonPool:UpdateClicks()
		local useKeyDown = GetCVarBool("ActionButtonUseKeyDown");
		if useKeyDown ~= self.useKeyDown then
			local btn1, btn2;
			if useKeyDown then
				btn1, btn2 = "LeftButtonDown", "RightButtonDown";
			else
				btn1, btn2 = "LeftButtonUp", "RightButtonUp";
			end
			for _, button in ipairs(self.actionButtons) do
				button:RegisterForClicks(btn1, btn2);
			end
		end
	end

	function ActionButtonPool:AddFrameRef(handler)
		for index, button in ipairs(self.actionButtons) do
			handler:SetFrameRef("SecureButton"..index, button);
		end
	end

	ActionButtonPool:InitButtons(16);   --255/16
end

do  --Settings Registry
	CallbackRegistry:RegisterSettingCallback("SpellFlyout_CloseAfterClick", function(state, userInput)
		CLOSE_AFTER_CLICK = state;
		if userInput and not InCombatLockdown() then
			SecureRootContainer:Hide();
		end
	end);

	CallbackRegistry:RegisterSettingCallback("SpellFlyout_SingleRow", function(state, userInput)
		GRID_LAYOUT = not state;
		SecureControllerPool:UpdateLayout();
		if userInput and not InCombatLockdown() then
			SecureRootContainer:Hide();
		end
	end);

	CallbackRegistry:RegisterSettingCallback("SpellFlyout_HideUnusable", function(state, userInput)
		if userInput and not InCombatLockdown() then
			SecureRootContainer:Hide();
		end
	end);
end
