ActionBarMixin = {}

function ActionBarMixin:ActionBar_OnLoad()
    self.numButtonsShowable = self.numButtons;
    self.minButtonPadding = 2;
    self.buttonPadding = self.minButtonPadding;
    self.actionButtons = {};
    self.shownButtonContainers = {};

    local actionBarName = self:GetName();

    -- Create action buttons
    for i=1, self.numButtons do
        local buttonContainer = CreateFrame("Frame", actionBarName.."ButtonContainer"..i, self, "ActionBarButtonContainerTemplate", i);

        -- Different naming for these bars is to avoid errors with legacy code
        -- Ideally this wouldn't be needed
        local buttonName;
        if self == MainMenuBar then
            buttonName = "ActionButton"..i;
        elseif self == StanceBar then
            buttonName = "StanceButton"..i;
        elseif self == PetActionBar then
            buttonName = "PetActionButton"..i;
        elseif self == PossessActionBar then
            buttonName = "PossessButton"..i;
        else
            buttonName = actionBarName.."Button"..i;
        end

		local actionButton = CreateFrame("CheckButton", buttonName, buttonContainer, self.buttonTemplate, i);
        actionButton:SetPoint("CENTER");
        actionButton.bar = self;
        actionButton.container = buttonContainer;
        actionButton.index = i;

        if self.commandNamePrefix then
            actionButton.commandName = self.commandNamePrefix.."BUTTON"..i;
        end

        table.insert(self.actionButtons, actionButton);

        buttonContainer:SetSize(actionButton:GetWidth(), actionButton:GetHeight());
    end

    self:UpdateShownButtons();
    self:UpdateGridLayout();

    if self.showGridEventName then
        self:RegisterEvent(self.showGridEventName);
    end
    if self.hideGridEventName then
        self:RegisterEvent(self.hideGridEventName);
    end
end

function ActionBarMixin:ActionBar_OnEvent(event, ...)
    if event == self.showGridEventName then
        self:SetShowGrid(true, ACTION_BUTTON_SHOW_GRID_REASON_EVENT);
	elseif event == self.hideGridEventName then
		self:SetShowGrid(false, ACTION_BUTTON_SHOW_GRID_REASON_EVENT);
    end
end

function ActionBarMixin:CacheGridSettings()
    self.oldGridSettings = {
        numLaidOutObjects = #self.shownButtonContainers,
        numRows = self.numRows,
        isHorizontal = self.isHorizontal,
        addButtonsToRight = self.addButtonsToRight,
        addButtonsToTop = self.addButtonsToTop,
        buttonPadding = self.buttonPadding,
    };
end

function ActionBarMixin:ShouldUpdateGrid()
    if self.oldGridSettings == nil then
        return true;
    end

    if self.oldGridSettings.numLaidOutObjects ~= #self.shownButtonContainers
    or self.oldGridSettings.numRows ~= self.numRows
    or self.oldGridSettings.isHorizontal ~= self.isHorizontal
    or self.oldGridSettings.addButtonsToRight ~= self.addButtonsToRight
    or self.oldGridSettings.addButtonsToTop ~= self.addButtonsToTop
    or self.oldGridSettings.buttonPadding ~= self.buttonPadding then
        return true;
    end

    return false;
end

function ActionBarMixin:UpdateGridLayout()
    if not self:ShouldUpdateGrid() then
        return;
    end

    -- Stride is the number of buttons per row (or column if we are vertical)
    -- Set stride so that if we can have the same number of icons per row we do
    local stride = math.ceil(#self.shownButtonContainers / self.numRows);

    -- Set button padding. User can set padding through edit mode
    local buttonPadding = math.max(self.minButtonPadding, self.buttonPadding);

    -- Multipliers determine the direction the bar grows for grid layouts 
    -- Positive means right/up
    -- Negative means left/down
    local xMultiplier = self.addButtonsToRight and 1 or -1;
    local yMultiplier = self.addButtonsToTop and 1 or -1;

    -- Create the grid layout according to whether we are horizontal or vertical
    local layout;
    if self.isHorizontal then
        layout = GridLayoutUtil.CreateStandardGridLayout(stride, buttonPadding, buttonPadding, xMultiplier, yMultiplier);
    else
        layout = GridLayoutUtil.CreateVerticalGridLayout(stride, buttonPadding, buttonPadding, xMultiplier, yMultiplier);
    end

    -- Need to change where the buttons anchor based on how the bar grows
    local anchorPoint;
	if self.addButtonsToLeft then 
		  anchorPoint = "LEFT";
    elseif self.addButtonsToTop then
        if self.addButtonsToRight then
            anchorPoint = "BOTTOMLEFT";
        else
            anchorPoint = "BOTTOMRIGHT";
        end
    else
        if self.addButtonsToRight then
            anchorPoint = "TOPLEFT";
        else
            anchorPoint = "TOPRIGHT";
        end
    end

    -- Apply the layout and then update our size
	GridLayoutUtil.ApplyGridLayout(self.shownButtonContainers, AnchorUtil.CreateAnchor(anchorPoint, self, anchorPoint), layout);
    self:Layout();
    self:UpdateSpellFlyoutDirection();
    self:CacheGridSettings();
end

function ActionBarMixin:SetShowGrid(showGrid, reason)
    if not showGrid and KeybindFrames_InQuickKeybindMode() then
        return; -- Don't hide grid if we are in QuickKeybindMode
    end

    -- SetShowGrid overrides "Always Show Buttons" being false, showAllButtons overrides "Bar Visible" setting
    -- When dragging spells or going through spell collection, we want to override both
    if reason == ACTION_BUTTON_SHOW_GRID_REASON_EVENT or reason == ACTION_BUTTON_SHOW_GRID_REASON_SPELLCOLLECTION then
        -- Use bit flags to ensure we don't re-hide the bar for one reason while another reason is still active
        -- IE dropping a dragged spell while the Spellbook is still open
        if showGrid then
            self.showAllButtons = bit.bor(self.showAllButtons or 0, reason);
        else
            self.showAllButtons = bit.band(self.showAllButtons or 0, bit.bnot(reason));
        end
    end

    for i, actionButton in pairs(self.actionButtons) do
        actionButton:SetShowGrid(showGrid, reason);
    end

	self:UpdateFrameStrata(showGrid, reason);
    self:UpdateShownButtons();

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

function ActionBarMixin:GetShowAllButtons()
	return self.showAllButtons and self.showAllButtons > 0 or false;
end

-- If you are dragging a spell then raise the bar's frame strata
-- Do this so bars can appear above talents UI since it's pretty big and likely covers your bars
-- Don't do for other cursor types (specifically items since we don't want bars to cover bag slots when dragging items)
-- The current intent is to raise this when dragging from the spellbook/talents
local cursorTypesThatRaiseActionBar = tInvert({ "spell", "petaction", "flyout"});

function ActionBarMixin:ShouldRaise(showGrid, reason)
	if showGrid and (reason == ACTION_BUTTON_SHOW_GRID_REASON_EVENT) then
    	local cursorType = GetCursorInfo();
		if cursorType then
			return cursorTypesThatRaiseActionBar[cursorType] ~= nil;
		end
	end

	return false;
end

function ActionBarMixin:UpdateFrameStrata(showGrid, reason)
	self:SetFrameStrata(self:ShouldRaise(showGrid, reason) and "TOOLTIP" or "MEDIUM");
end

function ActionBarMixin:UpdateShownButtons()
    table.wipe(self.shownButtonContainers);

    for i, actionButton in pairs(self.actionButtons) do
        local showButton = actionButton.index <= self.numButtonsShowable  -- Show button if it is within the num buttons which are showable
            and not actionButton:GetAttribute("statehidden") -- and it isn't being hidden by an attribute
            and (actionButton:GetShowGrid() or actionButton:HasAction()); -- And either the grid is being shown or the button has an action

        actionButton:SetShown(showButton);

        local showButtonContainer = showButton or (not self.noSpacers and i <= self.numButtonsShowable);
        actionButton.container:SetShown(showButtonContainer);
        if showButtonContainer then
            table.insert(self.shownButtonContainers, actionButton.container);
        end
    end

    -- If visible buttons changed then we may need to update the dividers we're showing between buttons
    if self.UpdateDividers then
        self:UpdateDividers();
    end
end

function ActionBarMixin:UpdateSpellFlyoutDirection()
    local direction = self.isHorizontal and "UP" or "LEFT";

	local actionBarCenterX, actionBarCenterY = self:GetCenter();
	if actionBarCenterX and actionBarCenterY then
		if self.isHorizontal then
			local halfScreen = GetScreenHeight() / 2;
			direction = actionBarCenterY < halfScreen and "UP" or "DOWN";
		else
			local halfScreen = GetScreenWidth() / 2;
			direction = actionBarCenterX > halfScreen and "LEFT" or "RIGHT";
		end
	end

    if self.flyoutDirection ~= direction then
        self.flyoutDirection = direction;

        for i, actionButton in pairs(self.actionButtons) do
            if actionButton.UpdateFlyout then
                actionButton:UpdateFlyout();
            end
        end
    end
end

function ActionBarMixin:GetSpellFlyoutDirection()
    if not self.flyoutDirection then
        self:UpdateSpellFlyoutDirection();
    end

	return self.flyoutDirection;
end

EditModeActionBarMixin = {}

function EditModeActionBarMixin:EditModeActionBar_OnLoad()
	self:ActionBar_OnLoad();
	self:OnSystemLoad();

    self.isShownExternal = self:IsShown();

    self:RegisterEvent("PLAYER_REGEN_ENABLED");
	self:RegisterEvent("PLAYER_REGEN_DISABLED");
end

function EditModeActionBarMixin:SetupVisibilityFunctionOverrides()
	-- This is called from EditModeSystemMixin.
    -- Need to override all the show/hide methods so that we can manage our visibility based on settings
    self.IsShownBase = self.IsShown;
    self.IsShown = self.IsShownOverride;
    self.SetShownBase = self.SetShown;
    self.SetShown = self.SetShownOverride;
    self.ShowBase = self.Show;
    self.Show = self.ShowOverride;
    self.HideBase = self.Hide;
    self.Hide = self.HideOverride;
end

function EditModeActionBarMixin:EditModeActionBar_OnEvent(event, ...)
    if event == "PLAYER_REGEN_ENABLED" or event == "PLAYER_REGEN_DISABLED" then
        -- Update shown state when combat state changes to account for bar visibility setting
        -- Apparently regen being enabled/disabled is a good way to tell whether the player's combat state has changed
		self:UpdateVisibility();
    end
end

function EditModeActionBarMixin:ShouldUpdateGrid()
	if self:IsInitialized() and not self.gridInitialized then
        self.gridInitialized = true;
		return true;
	end

	return ActionBarMixin.ShouldUpdateGrid(self);
end

function EditModeActionBarMixin:IsShownOverride()
    -- This is needed since the bar may technically be hidden due to visibility settings but we don't actually want things to
    -- interpret it as truly hidden since a lot of things will use this info to know whether they need to show/hide the bar
    -- and that literal shown state doesn't accurately represent what the bar's state is
    if self.visibility and self.visibility ~= "Always" then
        return self.isShownExternal;
    end

    -- If the bar isn't using visibility settings then just return the base IsShown
    return self:IsShownBase();
end

function EditModeActionBarMixin:SetShownOverride(shown)
    if shown then
        self:ShowOverride();
    else
        self:HideOverride();
    end
end

function EditModeActionBarMixin:ShowOverride()
    self.isShownExternal = true;

    -- Action bars may have fancy visibility rules which we have to follow rather than just directly showing/hiding
    self:UpdateVisibility();
end

function EditModeActionBarMixin:HideOverride()
    self.isShownExternal = false;

	if self:ShouldBreakSnappedFramesOnHide() then
		self:BreakSnappedFrames();
	end

    -- Action bars may have fancy visibility rules which we have to follow rather than just directly showing/hiding
    self:UpdateVisibility();
end

function EditModeActionBarMixin:UpdateVisibility()
    if not self.visibility then
        -- If we don't have visiblity settings, then just follow whatever we are told to do externally
        self:SetShownBase(self.isShownExternal or self.editModeForceShow);
    elseif not self.isShownExternal then
        -- If we are being hidden externally then don't change that
        self:HideBase();
    elseif self:GetShowAllButtons() then
        -- If we are showing all buttons likely due to an icon being dragged or viewing spell collection UI then show the bar
        self:ShowBase();
    elseif EditModeManagerFrame:IsEditModeActive() then
        -- If edit mode manager is showing then show
        self:ShowBase();
    elseif self.visibility == "InCombat" or self.visibility == "OutOfCombat" then
        -- If we care about combat visibility, update visibility based on whether you're in combat
        local isInCombat = UnitAffectingCombat("player");

        if self.visibility == "InCombat" then
            self:SetShownBase(isInCombat);
        else -- Out of combat
            self:SetShownBase(not isInCombat);
        end
    elseif self.visibility == "Hidden" then
        -- If we are set to be hidden then hide
        self:HideBase();
    else
        -- If no other rules, show the bar
        self:ShowBase();
    end

    EditModeManagerFrame:UpdateActionBarLayout(self);
end