ZO_KeybindStrip = ZO_InitializingObject:Subclass()

KEYBIND_STRIP_ALIGN_LEFT = 1
KEYBIND_STRIP_ALIGN_CENTER = 2
KEYBIND_STRIP_ALIGN_RIGHT = 3

KEYBIND_STRIP_DISABLED_ALERT = "alert"
KEYBIND_STRIP_DISABLED_DIALOG = "dialog"

local DOWN = false
local UP = true
function ZO_KeybindStrip:Initialize(control, keybindButtonTemplate, styleInfo)
    self.control = control
    self.centerParent = control:GetNamedChild("CenterParent") or control
    self.keybinds = {}
    self.keybindsByGamepadPreferredKeybind = {}
    self.keybindGroups = {}
    self.cooldownKeybinds = {}
    self.keybindStateStack = {}
    self.keybindButtons = {}

    self:SetStyle(styleInfo)

    local function OnButtonClicked(button)
        local keybindButtonDescriptor = button.keybindButtonDescriptor
        if self:FilterSceneHiding(keybindButtonDescriptor) then
            if keybindButtonDescriptor.callback then
                keybindButtonDescriptor.callback(DOWN)
            end
            if keybindButtonDescriptor.handlesKeyUp or keybindButtonDescriptor.keybindButtonGroupDescriptor and keybindButtonDescriptor.keybindButtonGroupDescriptor.handlesKeyUp then
                if keybindButtonDescriptor.callback then
                    keybindButtonDescriptor.callback(UP)
                end
            end
        end
    end

    local function CreateButton(objectPool)
        -- NOTE: Whatever template is used here is expected to have the interface defined by
        -- ZO_KeybindButtonTemplate_OnInitialized.  This should set up a few control references (for the name and key labels)
        -- as well as defining some state management functions...see that object for more details.
        local button = ZO_ObjectPool_CreateControl(keybindButtonTemplate, objectPool, control)
        button:SetCallback(OnButtonClicked)
        return button
    end

    local function Reset(control)
        control:SetHidden(true)
        control:ClearAnchors()
        if control.customKeybindControl then
            control.customKeybindControl:SetParent(nil)
            control.customKeybindControl:SetHidden(true)
            control.customKeybindControl = nil
        end
    end
    
    self.keybindButtonPool = ZO_ObjectPool:New(CreateButton, Reset)

    local function UpdateBindingLabels()
        for keybind, buttonOrEtherealDescriptor in pairs(self.keybinds) do
            if type(buttonOrEtherealDescriptor) == "userdata" then
                self:SetUpButton(buttonOrEtherealDescriptor)
            end
        end

        self:UpdateAnchors()
    end

    control:RegisterForEvent(EVENT_KEYBINDING_SET, UpdateBindingLabels)
    control:RegisterForEvent(EVENT_KEYBINDING_CLEARED, UpdateBindingLabels)
    control:RegisterForEvent(EVENT_KEYBINDINGS_LOADED, UpdateBindingLabels)
    control:RegisterForEvent(EVENT_GAMEPAD_PREFERRED_MODE_CHANGED, UpdateBindingLabels)
    control:RegisterForEvent(EVENT_INPUT_TYPE_CHANGED, UpdateBindingLabels)

    local function OnUpdate(control, time)
        self:UpdateCooldowns(time)
    end
    control:SetHandler("OnUpdate", OnUpdate)
end

-- Keybind State Stack Management

function ZO_KeybindStrip:PushKeybindGroupState()
    local background
    if not ZO_KeybindStripMungeBackground:IsHidden() then
        background = ZO_KeybindStripMungeBackground
    else
        background = ZO_KeybindStripGamepadBackground
    end
    
    local state = 
    {
        keybindGroups = {},
        individualButtons = {},
        drawTier = self.control:GetDrawTier(),
        drawLayer = self.control:GetDrawLayer(),
        drawLevel = self.control:GetDrawLevel(),
        backgroundTier = background:GetDrawTier(),
        backgroundLayer = background:GetDrawLayer(),
        backgroundLevel = background:GetDrawLevel(),
        allowDefaultExit = self.allowDefaultExit
    }

    --copy all the keybind groups into the state and remove them from the strip
    for key, value in pairs(self.keybindGroups) do
        state.keybindGroups[key] = value
    end
    local topStateIndex = self:GetTopKeybindStateIndex()
    self:RemoveAllKeyButtonGroups(topStateIndex)

    --now copy/overwrite any left over buttons to the state
    for key, value in pairs(self.keybinds) do
        state.individualButtons[key] = value.keybindButtonDescriptor or value
        self:RemoveKeybindButton(value.keybindButtonDescriptor or value, topStateIndex)
    end

    table.insert(self.keybindStateStack, state)

    self:UpdateAnchors()

    return self:GetTopKeybindStateIndex()
end

function ZO_KeybindStrip:PopKeybindGroupState()
    local numStates = #self.keybindStateStack
    if(numStates > 0) then
        local topStateIndex = self:GetTopKeybindStateIndex()
        self:RemoveAllKeyButtonGroups(topStateIndex)
        for key, value in pairs(self.keybinds) do
            self:RemoveKeybindButton(value.keybindButtonDescriptor or value, topStateIndex)
        end

        local state = table.remove(self.keybindStateStack, numStates)
        topStateIndex = self:GetTopKeybindStateIndex()

        self:SetDrawOrder(state.drawTier, state.drawLayer, state.drawLevel)
        self:SetBackgroundDrawOrder(state.backgroundTier, state.backgroundLayer, state.backgroundLevel)
        self.allowDefaultExit = state.allowDefaultExit

        for i, descriptorGroup in pairs(state.keybindGroups) do
            self:AddKeybindButtonGroup(descriptorGroup, topStateIndex)
        end

        for i, keybind in pairs(state.individualButtons) do
            if(self:HasKeybindButton(keybind, topStateIndex)) then
                self:UpdateKeybindButton(keybind, topStateIndex)
            else
                self:AddKeybindButton(keybind, topStateIndex)
            end
        end
        
        self:UpdateAnchors()
    end
end

function ZO_KeybindStrip:RemoveAllKeyButtonGroups(stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return ZO_KeybindStrip.RemoveAllKeyButtonGroupsStack(state)
    end

    if next(self.keybindGroups) ~= nil then
        local prevKeybindGroups = ZO_ShallowTableCopy(self.keybindGroups)
        for _, groupDescriptor in pairs(prevKeybindGroups) do
            self:RemoveKeybindButtonGroup(groupDescriptor, stateIndex)
        end
    end
end

function ZO_KeybindStrip:ClearKeybindGroupStateStack()
    --remove all keybind buttons and loose buttons as well as any states
    local topStateIndex = self:GetTopKeybindStateIndex()
    self:RemoveAllKeyButtonGroups(topStateIndex)
    for key, value in pairs(self.keybinds) do
        self:RemoveKeybindButton(value.keybindButtonDescriptor or value, topStateIndex)
    end
    ZO_ClearTable(self.keybindStateStack)
end

function ZO_KeybindStrip:SetDrawOrder(tier, layer, level)
    if tier then
         self.control:SetDrawTier(tier)
    end
    if layer then
         self.control:SetDrawLayer(layer)
    end
    if level then
         self.control:SetDrawLevel(level)
    end
end

function ZO_KeybindStrip:SetBackgroundDrawOrder(tier, layer, level)
    if ZO_KeybindStripMungeBackground then
        if tier then 
            ZO_KeybindStripMungeBackground:SetDrawTier(tier)
        end
        if layer then 
            ZO_KeybindStripMungeBackground:SetDrawLayer(layer)
        end
        if level then 
            ZO_KeybindStripMungeBackground:SetDrawLevel(level)
        end
    end
    if ZO_KeybindStripGamepadBackground then
        if tier then 
            ZO_KeybindStripGamepadBackground:SetDrawTier(tier)
        end
        if layer then 
            ZO_KeybindStripGamepadBackground:SetDrawLayer(layer)
        end
        if level then 
            ZO_KeybindStripGamepadBackground:SetDrawLevel(level)
        end
    end
end

--Returns nil if there are no keybind group states (push was not called).
--Returns the bottom of the stack when there is no stateIndex if there are keybind group states (push was called at least once).
function ZO_KeybindStrip:GetKeybindState(stateIndex)
    stateIndex = stateIndex or 1
    assert(stateIndex > 0)
    if stateIndex <= #self.keybindStateStack then
        return self.keybindStateStack[stateIndex]
    end
    return nil
end

function ZO_KeybindStrip:GetTopKeybindStateIndex()
    return #self.keybindStateStack + 1
end

--- PRIVATE FUNCTIONS ---

--We store the descriptor for ethereals and the button control for keybinds that actually appear on the strip
local function GetDescriptorFromButton(buttonOrEtherealDescriptor)
    if type(buttonOrEtherealDescriptor) == "userdata" then
        return buttonOrEtherealDescriptor.keybindButtonDescriptor
    end
    return buttonOrEtherealDescriptor
end

local function GetValueFromRawOrFunction(keybindButtonDescriptor, key)
    local value = keybindButtonDescriptor[key]
    if value == nil and keybindButtonDescriptor.keybindButtonGroupDescriptor then
        value = keybindButtonDescriptor.keybindButtonGroupDescriptor[key]
    end

    if type(value) == "function" then
        return value(keybindButtonDescriptor, keybindButtonDescriptor.keybindButtonGroupDescriptor)
    end

    return value
end

function ZO_KeybindStrip.RemoveKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    if state.keybindGroups[keybindButtonGroupDescriptor] then
        state.keybindGroups[keybindButtonGroupDescriptor] = nil
        return true
    end

    return false
end

local function RemoveKeybindButtonStack(keybindButtonDescriptor, state)
    state.individualButtons[keybindButtonDescriptor.keybind] = nil
end

local function UpdateKeybindButtonGroupStack(keybindButtonDescriptor, state)
    if state.keybindGroups[keybindButtonDescriptor] then
        return true
    end

    return false
end

local function UpdateKeybindButtonStack(keybindButtonDescriptor, state)
    -- do nothing
end

local function UpdateCurrentKeybindButtonGroupsStack(stateIndex)
    -- do nothing
end

function ZO_KeybindStrip.RemoveAllKeyButtonGroupsStack(state)
    if next(state.keybindGroups) ~= nil then
        local prevKeybindGroups = ZO_ShallowTableCopy(state.keybindGroups)
        for _, groupDescriptor in pairs(prevKeybindGroups) do
            ZO_KeybindStrip.RemoveKeybindButtonGroupStack(groupDescriptor, state)
        end
    end
end


--- END OF PRIVATE FUNCTIONS ---

--[[

An example keybind button descriptor

local descriptor = {
    alignment = KEYBIND_STRIP_ALIGN_CENTER, -- defaults to KEYBIND_STRIP_ALIGN_RIGHT
    order = 100 -- or a function that returns the relative ordering within the alignment, where a lower number is anchored first (right to left for left alignment, left to right for center and right), default is 0 - buttons with the same order are ordered by insertion
    name = "Exit", -- or function that returns a name
    keybind = "UI_SHORTCUT_PRIMARY",
    gamepadPreferredKeybind = "UI_SHORTCUT_NEGATIVE", -- optional, only use in the case that you want to reuse a button between keyboard and gamepad, but want them to have different bindings.
    customKeybindControl = nil, -- control or a function that returns a control to display instead of the normal keybind label
    callback = function(up) DoSomething() end, -- First and only parameter is whether its a key up or down, ups will only be sent if handlesKeyUp flag is set
    visible = function(descriptor) return IsUsePossible(descriptor.something) end, -- An optional predicate, if present returning true indicates that this descriptor is visible, otherwise it is not
    icon = "IconPath/Icon.dds", -- or a function that returns an icon path, an optional icon to display to the right of the name
    handlesKeyUp = true, -- indicates that the callback would like to be informed of key ups in addition to downs, by default only downs are sent
    disabledDuringSceneHiding = false, -- indicates that the callback is disabled during Scene Hiding, by default it is false
    ethereal = false, -- if true, indicates that the button has no physical presence and will only be considered when a keybind is used, which means that name, alignment, etc properties are ignored. This cannot be a function that returns a value.
}

]]--

do
    local function GetKeybindDescriptorDebugIdentifier(keybindButtonDescriptor)
        return GetValueFromRawOrFunction(keybindButtonDescriptor, "name") or ""
    end

    function ZO_KeybindStrip:HandleDuplicateAddKeybind(existingButtonOrEtherealDescriptor, keybindButtonDescriptor, state, stateIndex, currentSceneName)
        local existingDescriptor = GetDescriptorFromButton(existingButtonOrEtherealDescriptor)
        if existingDescriptor then
            --We tried to re-add the same exact button, just return
            if existingDescriptor == keybindButtonDescriptor then
                return
            end

            local existingSceneName = existingDescriptor.addedForSceneName or ""
            local existingDescriptorIdentifier = GetKeybindDescriptorDebugIdentifier(existingDescriptor)
            local newDescriptorIdentifier = GetKeybindDescriptorDebugIdentifier(keybindButtonDescriptor)
            local keybindIdentifier
            if keybindButtonDescriptor.gamepadPreferredKeybind then
                keybindIdentifier = string.format("%s or %s", keybindButtonDescriptor.keybind, keybindButtonDescriptor.gamepadPreferredKeybind)
            else
                keybindIdentifier = keybindButtonDescriptor.keybind
            end
            local assertMessage = string.format("Duplicate Keybind: %s. Before: %s (%s). After: %s (%s).", keybindIdentifier, existingSceneName, existingDescriptorIdentifier, currentSceneName, newDescriptorIdentifier)

            -- Asserting here usually means that a key is already bound (typically because someone forgot to remove a keybinding).
            internalassert(false, assertMessage)
            self:RemoveKeybindButton(existingDescriptor, stateIndex)
        end
    end

    function ZO_KeybindStrip:AddKeybindButtonStack(keybindButtonDescriptor, state, stateIndex, currentSceneName)
        local existingButtonOrEtherealDescriptor = state.individualButtons[keybindButtonDescriptor.keybind]
        if existingButtonOrEtherealDescriptor then
            self:HandleDuplicateAddKeybind(existingButtonOrEtherealDescriptor, keybindButtonDescriptor, state, stateIndex, currentSceneName)
        end
        state.individualButtons[keybindButtonDescriptor.keybind] = keybindButtonDescriptor
        keybindButtonDescriptor.addedForSceneName = currentSceneName
    end

    function ZO_KeybindStrip:RegisterKeybindButtonOrEtherealDescriptorInternal(buttonOrEtherealDescriptor)
        local descriptor = GetDescriptorFromButton(buttonOrEtherealDescriptor)
        self.keybinds[descriptor.keybind] = buttonOrEtherealDescriptor
        if descriptor.gamepadPreferredKeybind then
            self.keybindsByGamepadPreferredKeybind[descriptor.gamepadPreferredKeybind] = buttonOrEtherealDescriptor
        end
    end

    function ZO_KeybindStrip:AddKeybindButton(keybindButtonDescriptor, stateIndex)
        local currentSceneName = ""
        if SCENE_MANAGER then
            local currentScene = SCENE_MANAGER:GetCurrentScene()
            if currentScene then
                currentSceneName = currentScene:GetName()
            end
        end

        local state = self:GetKeybindState(stateIndex)
        if state then
            return self:AddKeybindButtonStack(keybindButtonDescriptor, state, stateIndex, currentSceneName)
        end

        local existingButtonOrEtherealDescriptor = self.keybinds[keybindButtonDescriptor.keybind]
        if existingButtonOrEtherealDescriptor then
            self:HandleDuplicateAddKeybind(existingButtonOrEtherealDescriptor, keybindButtonDescriptor, state, stateIndex, currentSceneName)
        end

        if keybindButtonDescriptor.gamepadPreferredKeybind then
            local existingButtonOrEtherealDescriptor = self.keybindsByGamepadPreferredKeybind[keybindButtonDescriptor.gamepadPreferredKeybind]
            if existingButtonOrEtherealDescriptor then
                self:HandleDuplicateAddKeybind(existingButtonOrEtherealDescriptor, keybindButtonDescriptor, state, stateIndex, currentSceneName)
            end
            -- edge case: if a keybind registers only using the normal pathway, but
            -- we shadow it here, there is no way to press that keybind. so let's
            -- prevent that using the normal duplicate keybind handling
            local existingButtonOrEtherealDescriptor = self.keybinds[keybindButtonDescriptor.gamepadPreferredKeybind]
            if existingButtonOrEtherealDescriptor and GetDescriptorFromButton(existingButtonOrEtherealDescriptor).gamepadPreferredKeybind == nil then
                self:HandleDuplicateAddKeybind(existingButtonOrEtherealDescriptor, keybindButtonDescriptor, state, stateIndex, currentSceneName)
            end
        end

        keybindButtonDescriptor.addedForSceneName = currentSceneName

        if GetValueFromRawOrFunction(keybindButtonDescriptor, "ethereal") then
            self:RegisterKeybindButtonOrEtherealDescriptorInternal(keybindButtonDescriptor)
        else
            local button, key = self.keybindButtonPool:AcquireObject()
            button.keybindButtonDescriptor = keybindButtonDescriptor
            button.key = key

            self.insertionId = (self.insertionId or 0) + 1
            button.insertionOrder = self.insertionId

            self:RegisterKeybindButtonOrEtherealDescriptorInternal(button)

            if not self.batchUpdating then
                -- clear this out in case it was previously in a group
                keybindButtonDescriptor.keybindButtonGroupDescriptor = nil
            end

            table.insert(self.keybindButtons, button)

            if not self.batchUpdating then
                self:SetUpButton(button)
                self:UpdateAnchors()
            end
            return button
        end
    end
end

local function CompareKeybindButtonDescriptor(first, second)
    return  (first == nil) or (second == nil) or
            ((first.keybind == second.keybind) and
            (first.name == second.name) and
            (first.callback == second.callback))
end

function ZO_KeybindStrip:RemoveKeybindButton(keybindButtonDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return RemoveKeybindButtonStack(keybindButtonDescriptor, state)
    end

    local buttonOrEtherealDescriptor = self.keybinds[keybindButtonDescriptor.keybind]     
    if buttonOrEtherealDescriptor and CompareKeybindButtonDescriptor(buttonOrEtherealDescriptor.keybindButtonDescriptor, keybindButtonDescriptor) then
        self.keybinds[keybindButtonDescriptor.keybind] = nil
        if keybindButtonDescriptor.gamepadPreferredKeybind then
            self.keybindsByGamepadPreferredKeybind[keybindButtonDescriptor.gamepadPreferredKeybind] = nil
        end

        keybindButtonDescriptor.addedForSceneName = nil

        for i, cooldownKeybindDescriptor in ipairs(self.cooldownKeybinds) do
            if cooldownKeybindDescriptor == keybindButtonDescriptor and not cooldownKeybindDescriptor.shouldCooldownPersist then
                table.remove(self.cooldownKeybinds, i)
                cooldownKeybindDescriptor.cooldown = nil
                cooldownKeybindDescriptor.cooldownStart = nil
                break
            end
        end

        if type(buttonOrEtherealDescriptor) == "userdata" then
            local button = buttonOrEtherealDescriptor
            self.keybindButtonPool:ReleaseObject(button.key)
            local buttonIndex = ZO_IndexOfElementInNumericallyIndexedTable(self.keybindButtons, button)
            table.remove(self.keybindButtons, buttonIndex)

            if not self.batchUpdating then
                self:UpdateAnchors()
            end
        end
    end
end

function ZO_KeybindStrip:UpdateKeybindButton(keybindButtonDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return UpdateKeybindButtonStack(keybindButtonDescriptor, state)
    end

    local buttonOrEtherealDescriptor = self.keybinds[keybindButtonDescriptor.keybind]
    if type(buttonOrEtherealDescriptor) == "userdata" then
        local existingButton = buttonOrEtherealDescriptor
        if existingButton.keybindButtonDescriptor == keybindButtonDescriptor and not GetValueFromRawOrFunction(keybindButtonDescriptor, "ethereal") then
            local UPDATE_ONLY = true
            self:SetUpButton(existingButton, UPDATE_ONLY)
            if not self.batchUpdating then
                self:UpdateAnchors()
            end
        else
            self:RemoveKeybindButton(existingButton.keybindButtonDescriptor)
            self:AddKeybindButton(keybindButtonDescriptor)
        end
    elseif buttonOrEtherealDescriptor then
        local existingButtonDescriptor = buttonOrEtherealDescriptor
        if keybindButtonDescriptor ~= existingButtonDescriptor or not GetValueFromRawOrFunction(keybindButtonDescriptor, "ethereal") then
            self:RemoveKeybindButton(existingButtonDescriptor)
            self:AddKeybindButton(keybindButtonDescriptor)
        end
    end
end

local function HasKeybindButtonStack(keybindButtonDescriptor, state)
    return state.individualButtons[keybindButtonDescriptor.keybind] ~= nil
end

function ZO_KeybindStrip:HasKeybindButton(keybindButtonDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return HasKeybindButtonStack(keybindButtonDescriptor, state)
    end
    return self.keybinds[keybindButtonDescriptor.keybind] ~= nil
end

local function HasKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    return state.keybindGroups[keybindButtonGroupDescriptor] ~= nil
end

function ZO_KeybindStrip:HasKeybindButtonGroup(keybindButtonGroupDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return HasKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    end
    return self.keybindGroups[keybindButtonGroupDescriptor] ~= nil
end

--[[

An example keybind button group descriptor

local keybindButtonGroup = {
    -- Anything added here will be inherited by individual descriptors, for example adding an alignment of left here will cause individial descriptors to all be aligned left
    -- unless they specify their own alignment
    alignment = KEYBIND_STRIP_ALIGN_LEFT,
    {
        name = "Exit", -- or function that returns a name
        keybind = "UI_SHORTCUT_PRIMARY",
        callback = function(up) DoSomething() end, -- First and only parameter is whether its a key up or down, ups will only be sent if handlesKeyUp flag is set
        handlesKeyUp = true, -- indicates that the callback would like to be informed of key ups in addition to downs, by default only downs are sent
    },

    {
        alignment = KEYBIND_STRIP_ALIGN_CENTER, -- override the alignment set un the group
        name = "Use", -- or function that returns a name
        keybind = "UI_SHORTCUT_SECONDARY",
        callback = function(up) UseSomething() end, -- First and only parameter is whether its a key up or down, ups will only be sent if handlesKeyUp flag is set
        visible = function(descriptor) return IsUsePossible(descriptor.something) end,
        sound = SOUNDS.POSITIVE_CLICK, -- An audio hook that plays when the button is pressed
    },
}

]]--

local function AddKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    if not state.keybindGroups[keybindButtonGroupDescriptor] then
        state.keybindGroups[keybindButtonGroupDescriptor] = keybindButtonGroupDescriptor
        return true
    end

    return false
end

function ZO_KeybindStrip:AddKeybindButtonGroup(keybindButtonGroupDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return AddKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    end

    if not self.keybindGroups[keybindButtonGroupDescriptor] then
        self.batchUpdating = true

        self.keybindGroups[keybindButtonGroupDescriptor] = keybindButtonGroupDescriptor

        for i, keybindButtonDescriptor in ipairs(keybindButtonGroupDescriptor) do
            keybindButtonDescriptor.keybindButtonGroupDescriptor = keybindButtonGroupDescriptor
            local button = self:AddKeybindButton(keybindButtonDescriptor, stateIndex)
            if button then
                self:SetUpButton(button)
            end
        end

        self:UpdateAnchors()

        self.batchUpdating = false
        return true
    end
    return false
end

function ZO_KeybindStrip:RemoveKeybindButtonGroup(keybindButtonGroupDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return ZO_KeybindStrip.RemoveKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    end

    if self.keybindGroups[keybindButtonGroupDescriptor] then
        self.batchUpdating = true

        for i, keybindButtonDescriptor in ipairs(keybindButtonGroupDescriptor) do
            self:RemoveKeybindButton(keybindButtonDescriptor, stateIndex)
            keybindButtonDescriptor.keybindButtonGroupDescriptor = nil
        end

        self.keybindGroups[keybindButtonGroupDescriptor] = nil

        self:UpdateAnchors()

        self.batchUpdating = false
        return true
    end
    return false
end

function ZO_KeybindStrip:UpdateCurrentKeybindButtonGroups(stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return UpdateCurrentKeybindButtonGroupsStack(state)
    end

    for keybindButtonDescriptor, group in pairs(self.keybindGroups) do
        self:UpdateKeybindButtonGroup(keybindButtonDescriptor, stateIndex)
    end
end

function ZO_KeybindStrip:UpdateKeybindButtonGroup(keybindButtonGroupDescriptor, stateIndex)
    local state = self:GetKeybindState(stateIndex)
    if state then
        return UpdateKeybindButtonGroupStack(keybindButtonGroupDescriptor, state)
    end

    if self.keybindGroups[keybindButtonGroupDescriptor] then
        self.batchUpdating = true

        for i, keybindButtonDescriptor in ipairs(keybindButtonGroupDescriptor) do
            self:UpdateKeybindButton(keybindButtonDescriptor, stateIndex)
        end

        self:UpdateAnchors()

        self.batchUpdating = false
        return true
    end
    return false
end

function ZO_KeybindStrip:FilterSceneHiding(keybindButtonDescriptor)
    if keybindButtonDescriptor.disabledDuringSceneHiding then
        local currentScene = SCENE_MANAGER:GetCurrentScene()
        if currentScene and currentScene:GetState() == SCENE_HIDING then
            return false
        end
    end

    return true
end

function ZO_KeybindStrip:GetButtonOrEtherealDescriptorForKeybind(keybind)
    if IsInGamepadPreferredMode() then
        -- in the case of duplicates, prefer the gamepad key
        return self.keybindsByGamepadPreferredKeybind[keybind] or self.keybinds[keybind]
    else
        -- in the case of duplicates, prefer the keyboard key
        return self.keybinds[keybind] or self.keybindsByGamepadPreferredKeybind[keybind]
    end
end

function ZO_KeybindStrip:TryHandlingKeybindDown(keybind)
    if not self.control:IsHidden() then
        local buttonOrEtherealDescriptor = self:GetButtonOrEtherealDescriptorForKeybind(keybind)
        if buttonOrEtherealDescriptor and (not buttonOrEtherealDescriptor.IsControlHidden or not buttonOrEtherealDescriptor:IsControlHidden()) then
            local keybindButtonDescriptor = GetDescriptorFromButton(buttonOrEtherealDescriptor)
            local enabled, disabledAlertText, disabledAlertType = GetValueFromRawOrFunction(keybindButtonDescriptor, "enabled")
            local cooldown = GetValueFromRawOrFunction(keybindButtonDescriptor, "cooldown")
            if cooldown then
                enabled = false
            end
            if enabled ~= false then
                local keybindHandled = nil
                if self:FilterSceneHiding(keybindButtonDescriptor) then
                    if keybindButtonDescriptor.callback then
                        keybindHandled = keybindButtonDescriptor.callback(DOWN)
                        keybindButtonDescriptor.handledDown = true

                        -- ClearMenu must come after the keybind callback because it clears the
                        -- MouseOverRow for SortFilterLists on which the callback may be relying
                        ClearMenu()

                        local sound = GetValueFromRawOrFunction(keybindButtonDescriptor, "sound")
                        if sound then
                            PlaySound(sound)
                        end
                    end
                end
                return keybindHandled or keybindHandled == nil --nil is considered true in this case to ensure backwards compatability
            elseif disabledAlertText then
                local alertText = disabledAlertText
                if type(disabledAlertText) == "function" then
                    alertText = disabledAlertText()
                end

                if disabledAlertType == KEYBIND_STRIP_DISABLED_DIALOG then
                    ZO_Dialogs_ShowPlatformDialog("KEYBIND_STRIP_DISABLED_DIALOG", nil, { mainTextParams = { alertText }})
                else
                    if ZO_REMOTE_SCENE_CHANGE_ORIGIN == SCENE_MANAGER_MESSAGE_ORIGIN_INTERNAL then
                        RequestAlert(UI_ALERT_CATEGORY_ALERT, SOUNDS.GENERAL_ALERT_ERROR, alertText)
                    elseif ZO_REMOTE_SCENE_CHANGE_ORIGIN == SCENE_MANAGER_MESSAGE_ORIGIN_INGAME then
                        ZO_Alert(UI_ALERT_CATEGORY_ALERT, SOUNDS.GENERAL_ALERT_ERROR, alertText)
                    end
                end
            end
        end
    end
    return false
end

function ZO_KeybindStrip:TryHandlingKeybindUp(keybind)
    if not self.control:IsHidden() then
        local buttonOrEtherealDescriptor = self:GetButtonOrEtherealDescriptorForKeybind(keybind)
        if buttonOrEtherealDescriptor and (not buttonOrEtherealDescriptor.IsControlHidden or not buttonOrEtherealDescriptor:IsControlHidden()) then
            local keybindButtonDescriptor = GetDescriptorFromButton(buttonOrEtherealDescriptor)
            local enabled = GetValueFromRawOrFunction(buttonOrEtherealDescriptor, "enabled")
            local cooldown = GetValueFromRawOrFunction(buttonOrEtherealDescriptor, "cooldown")
            if cooldown then
                enabled = false
            end
            local handledPreviousDown = keybindButtonDescriptor.handledDown
            keybindButtonDescriptor.handledDown = nil
            if enabled ~= false or handledPreviousDown then
                if keybindButtonDescriptor.handlesKeyUp or keybindButtonDescriptor.keybindButtonGroupDescriptor and keybindButtonDescriptor.keybindButtonGroupDescriptor.handlesKeyUp then
                    local keybindHandled = nil
                    if self:FilterSceneHiding(keybindButtonDescriptor) then
                        if keybindButtonDescriptor.callback then
                            ClearMenu()
                            keybindHandled = keybindButtonDescriptor.callback(UP)
                        end
                    end
                    return keybindHandled or keybindHandled == nil --nil is considered true in this case to ensure backwards compatability
                end
            end
        end
    end
    return false
end

function ZO_KeybindStrip:TriggerCooldown(keybindButtonDescriptor, duration, stateIndex, shouldCooldownPersist)
    if type(duration) == "function" then
        keybindButtonDescriptor.cooldown = duration() / 1000
        keybindButtonDescriptor.remainingFunction = duration
    else
        keybindButtonDescriptor.cooldown = duration / 1000
        keybindButtonDescriptor.cooldownStart = GetFrameTimeMilliseconds() / 1000
        keybindButtonDescriptor.shouldCooldownPersist = shouldCooldownPersist
    end

    if self.keybinds[keybindButtonDescriptor.keybind] and self.keybinds[keybindButtonDescriptor.keybind].keybindButtonDescriptor == keybindButtonDescriptor then
        self:UpdateKeybindButton(keybindButtonDescriptor, stateIndex)
    end

    --Check for duplicates
    for i = 1, #self.cooldownKeybinds do
        local descriptor = self.cooldownKeybinds[i]
        if descriptor == keybindButtonDescriptor then
            return
        end
    end

    table.insert(self.cooldownKeybinds, keybindButtonDescriptor)
end

function ZO_KeybindStrip:UpdateCooldowns(time)
    for i = #self.cooldownKeybinds, 1, -1 do
        local descriptor = self.cooldownKeybinds[i]
        local newCooldown
        if descriptor.remainingFunction then
            newCooldown = descriptor.remainingFunction()
        else
            local cooldownStart = descriptor.cooldownStart
            local difference = time - cooldownStart
            newCooldown = descriptor.cooldown - difference
        end

        --Only update for changes in the seconds count
        local oldCeiling = zo_ceil(descriptor.cooldown)
        local newCeiling = zo_ceil(newCooldown)

        if oldCeiling ~= newCeiling then
            descriptor.cooldownStart = time
            descriptor.cooldown = newCooldown
            if descriptor.cooldown <= 0 then
                descriptor.cooldown = nil
                descriptor.cooldownStart = nil
                descriptor.shouldCooldownPersist = nil
                table.remove(self.cooldownKeybinds, i)
            end

            local activeDescriptor = self.keybinds[descriptor.keybind]
            if activeDescriptor then
                if not GetValueFromRawOrFunction(activeDescriptor, "ethereal") then
                    activeDescriptor = activeDescriptor.keybindButtonDescriptor
                end
                if descriptor == activeDescriptor then
                    self:UpdateKeybindButton(descriptor, self:GetTopKeybindStateIndex())
                end
            end
        elseif descriptor.cooldown <= 0 then
            descriptor.cooldown = nil
            descriptor.cooldownStart = nil
            descriptor.shouldCooldownPersist = nil
            table.remove(self.cooldownKeybinds, i)
        end
    end
end

function ZO_KeybindStrip:SetHidden(hidden)
    self.control:SetHidden(hidden)
end

--[[
    A style table provides font and other information to describe the look of the keybind buttons

    An example descriptor:

    {
        nameFont = "ZoFontKeybindStripDescription",
        nameFontColor = ZO_NORMAL_TEXT,
        keyFont = "ZoFontKeybindStripKey",
        modifyTextType = MODIFY_TEXT_TYPE_UPPERCASE,
        alwaysPreferGamepadMode = false,
        resizeToFitPadding = 40,
        yAnchorOffset = 0,
        leftAnchorRelativeToControl = GuiRoot,
        leftAnchorRelativePoint = LEFT,
        leftAnchorOffset = 0,
        centerAnchorOffset = 0,
        rightAnchorOffset = 0,
        rightAnchorRelativeToControl = GuiRoot,
        rightAnchorRelativePoint = RIGHT,
        drawTier = DT_HIGH,
        drawLevel = ZO_HIGH_TIER_GAMEPAD_KEYBIND_STRIP,
        backgroundDrawTier = DT_HIGH,
        backgroundDrawLevel = ZO_HIGH_TIER_GAMEPAD_KEYBIND_STRIP_BG,
    }
]]

function ZO_KeybindStrip:SetStyle(styleInfo)
    if self.styleInfo ~= styleInfo then
        self.styleInfo = styleInfo

        for keybind, buttonOrEtherealDescriptor in pairs(self.keybinds) do
            if type(buttonOrEtherealDescriptor) == "userdata" then
                self:SetUpButton(buttonOrEtherealDescriptor)
            end
        end
        self:UpdateAnchors()

        self:SetDrawOrder(styleInfo.drawTier, styleInfo.drawLayer, styleInfo.drawLevel)
        self:SetBackgroundStyle(styleInfo)

        if self.onStyleChanged then
            self.onStyleChanged(self, styleInfo)
        end
    end
end

function ZO_KeybindStrip:SetBackgroundStyle(styleInfo)
    -- even though we are only refreshing the background style, we are still changing the style overall
    -- store the potential style change so that future calls to SetStyle can refresh everything appropriately
    if self.styleInfo ~= styleInfo then
        self.styleInfo = styleInfo
    end

    self:SetBackgroundDrawOrder(styleInfo.backgroundDrawTier, styleInfo.backgroundDrawLayer, styleInfo.backgroundDrawLevel)
end

function ZO_KeybindStrip:GetStyle()
    return self.styleInfo
end 

function ZO_KeybindStrip:SetOnStyleChangedCallback(onStyleChanged)
    self.onStyleChanged = onStyleChanged
end

-- implementation functions below
function ZO_KeybindStrip:SetupButtonStyle(button, styleInfo)
    if styleInfo then
        if styleInfo.nameFont then
            button:SetNameFont(styleInfo.nameFont)
        end

        if styleInfo.nameFontColor then
            button:SetNormalTextColor(styleInfo.nameFontColor)
        end

        if styleInfo.keyFont then
            button:SetKeyFont(styleInfo.keyFont)
        end

        if styleInfo.resizeToFitPadding then
            button:SetResizeToFitPadding(styleInfo.resizeToFitPadding)
        end

        if styleInfo.modifyTextType then
            button.nameLabel:SetModifyTextType(styleInfo.modifyTextType)
        end

        return styleInfo.alwaysPreferGamepadMode
    end
    return nil
end

do
    local function IsVisible(keybindButtonDescriptor)
        local visibilityFunction = keybindButtonDescriptor.visible or keybindButtonDescriptor.keybindButtonGroupDescriptor and keybindButtonDescriptor.keybindButtonGroupDescriptor.visible
        return not visibilityFunction or visibilityFunction(keybindButtonDescriptor, keybindButtonDescriptor.keybindButtonGroupDescriptor)
    end

    function ZO_KeybindStrip:SetUpButton(button, updateOnly)
        local keybindButtonDescriptor = button.keybindButtonDescriptor

        local isVisible = IsVisible(button.keybindButtonDescriptor)
        local wasVisible = not button:IsHidden()
        local customKeybindControl = GetValueFromRawOrFunction(keybindButtonDescriptor, "customKeybindControl")
        if isVisible then
            if customKeybindControl then
                customKeybindControl:SetHidden(false)
                customKeybindControl:SetParent(button)
                customKeybindControl:ClearAnchors()
                customKeybindControl:SetAnchor(RIGHT, button.keyLabel, RIGHT)
                button.keyLabel:SetHidden(true)
                button.customKeybindControl = customKeybindControl
                button:SetResizeToFitPadding(0) -- This should be handled on the custom control
            else
                button.keyLabel:SetHidden(false)
            end

            local keybind = keybindButtonDescriptor.keybind
            local gamepadPreferredKeybind = keybindButtonDescriptor.gamepadPreferredKeybind
            local keybindButtonGroupDescriptor = keybindButtonDescriptor.keybindButtonGroupDescriptor
            
            if keybindButtonGroupDescriptor then
                if not keybind then
                    keybind = keybindButtonGroupDescriptor.keybind
                end

                if not gamepadPreferredKeybind then
                    gamepadPreferredKeybind = keybindButtonGroupDescriptor.gamepadPreferredKeybind
                end
            end

            local buttonKeybindChanging = button:GetKeybind() ~= keybind
            local suppressUpdate = (updateOnly and isVisible == wasVisible)
            -- updateOnly is used only when we are trying to update keybinds from UpdateKeybindButton so we know that this setup is coming from an update

            local alwaysPreferGamepadMode = self:SetupButtonStyle(button, self.styleInfo)

            if not suppressUpdate or buttonKeybindChanging then
                local DEFAULT_SHOW_UNBOUND = nil
                button:SetKeybind(keybind, DEFAULT_SHOW_UNBOUND, gamepadPreferredKeybind, alwaysPreferGamepadMode)
            end

            local enabled = GetValueFromRawOrFunction(keybindButtonDescriptor, "enabled")
            if enabled == nil then
                enabled = true
            end
            local cooldown = GetValueFromRawOrFunction(keybindButtonDescriptor, "cooldown")
            if cooldown and cooldown > 0 then
                enabled = false
            end
            button:SetEnabled(enabled)
            -- if we have a custom keybind control then attempt to disable it as well to match
            if customKeybindControl and customKeybindControl.SetEnabled then
                customKeybindControl:SetEnabled(enabled)
            end

            local name = GetValueFromRawOrFunction(keybindButtonDescriptor, "name")
            if cooldown and cooldown > 0 then
                local seconds = zo_ceil(cooldown)
                name = zo_strformat(SI_BINDING_NAME_COOLDOWN_FORMAT, name, seconds)
            end
            button.nameLabel:SetText(name)
            local iconPath = GetValueFromRawOrFunction(keybindButtonDescriptor, "icon")
            if iconPath and iconPath ~= "" then
                if not button.icon then
                    button.icon = CreateControl("$(parent)Icon", button, CT_TEXTURE)
                    button.icon:SetDimensions(20, 20)
                    button.icon:SetAnchor(LEFT, button.nameLabel, RIGHT, 0, -5)
                end
                button.icon:SetTexture(iconPath)
                button.icon:SetHidden(false)
            else
                if button.icon then
                    button.icon:SetHidden(true)
                end
            end
        else
            if customKeybindControl then
                customKeybindControl:SetHidden(true)
            end
        end

        button:SetHidden(not isVisible)
    end

    local function KeyboardSort(buttonLeft, buttonRight)
        local leftOrder = GetValueFromRawOrFunction(buttonLeft.keybindButtonDescriptor, "order") or 0
        local rightOrder = GetValueFromRawOrFunction(buttonRight.keybindButtonDescriptor, "order") or 0

        if leftOrder == rightOrder then
            return buttonLeft.insertionOrder < buttonRight.insertionOrder
        end
        return leftOrder < rightOrder
    end

    local GAMEPAD_BUTTON_ORDER =
    {
        UI_SHORTCUT_EXIT = 0,
        UI_SHORTCUT_PRIMARY = 1,
        UI_SHORTCUT_NEGATIVE = 2,
        UI_SHORTCUT_SECONDARY = 3,
        UI_SHORTCUT_TERTIARY = 4,
        UI_SHORTCUT_QUATERNARY = 5,
        UI_SHORTCUT_QUINARY = 6,
        UI_SHORTCUT_LEFT_STICK = 7,
        UI_SHORTCUT_RIGHT_STICK = 8,
        UI_SHORTCUT_LEFT_TRIGGER = 9,
        UI_SHORTCUT_RIGHT_TRIGGER = 10,

        DIALOG_PRIMARY = 1,
        DIALOG_NEGATIVE = 2,
        DIALOG_SECONDARY = 3,
        DIALOG_TERTIARY = 4,
        DIALOG_RESET = 5,
    }

    local function GetGamepadOrderFromKeybindDescriptor(keybindDescriptor)
        local order = GetValueFromRawOrFunction(keybindDescriptor, "gamepadOrder")
        if order then
            return order
        end
        order = GAMEPAD_BUTTON_ORDER[keybindDescriptor.gamepadPreferredKeybind or keybindDescriptor.keybind]
        if order then
            return order
        end
        order = GetValueFromRawOrFunction(keybindDescriptor, "order")
        if order then
            return order
        end
        return 0
    end

    local function GamepadSort(buttonLeft, buttonRight)
        local leftKeybindDescriptor = buttonLeft.keybindButtonDescriptor
        local rightKeybindDescriptor = buttonRight.keybindButtonDescriptor

        local leftOrder = GetGamepadOrderFromKeybindDescriptor(leftKeybindDescriptor)
        local rightOrder = GetGamepadOrderFromKeybindDescriptor(rightKeybindDescriptor)

        if leftOrder == rightOrder then
            return buttonLeft.insertionOrder < buttonRight.insertionOrder
        end
        return leftOrder < rightOrder
    end

    local function EtherealSort(leftKeybindDescriptor, rightKeybindDescriptor)
        local leftOrder = leftKeybindDescriptor.etherealNarrationOrder or 0
        local rightOrder = rightKeybindDescriptor.etherealNarrationOrder or 0
        if leftOrder == rightOrder then
            return leftKeybindDescriptor.keybind < rightKeybindDescriptor.keybind
        else
            return leftOrder < rightOrder
        end
    end

    local function GetKeybindButtonDescriptorNarrationInfo(keybindButtonDescriptor)
        local enabled = GetValueFromRawOrFunction(keybindButtonDescriptor, "enabled")
        if enabled == nil then
            enabled = true
        end

        --Any additional info we end up needing for narration should go here
        local data = 
        {
            name = GetValueFromRawOrFunction(keybindButtonDescriptor, "narrationOverrideName") or GetValueFromRawOrFunction(keybindButtonDescriptor, "name"),
            keybindName = ZO_Keybindings_GetHighestPriorityNarrationStringFromAction(keybindButtonDescriptor.keybind) or GetString(SI_ACTION_IS_NOT_BOUND),
            enabled = enabled,
        }

        return data
    end

    function ZO_KeybindStrip:GetOrderedNarratableKeybindButtonInfo()
        local keybinds = {}
        --Do not include anything if the keybind strip is currently hidden
        if not self.control:IsHidden() then
            local buttonsByAlignment =
            {
                [KEYBIND_STRIP_ALIGN_LEFT] = {},
                [KEYBIND_STRIP_ALIGN_CENTER] = {},
                [KEYBIND_STRIP_ALIGN_RIGHT] = {},
            }

            for _, button in ipairs(self.keybindButtons) do
                local isVisible = IsVisible(button.keybindButtonDescriptor)
                if isVisible then
                    local alignment = GetValueFromRawOrFunction(button.keybindButtonDescriptor, "alignment") or KEYBIND_STRIP_ALIGN_RIGHT
                    table.insert(buttonsByAlignment[alignment], button)
                end
            end

            for alignment, buttons in pairs(buttonsByAlignment) do
                if IsInGamepadPreferredMode() then
                    table.sort(buttons, GamepadSort)
                else
                    table.sort(buttons, KeyboardSort)
                end
            end

            for _, button in ipairs(buttonsByAlignment[KEYBIND_STRIP_ALIGN_LEFT]) do
                table.insert(keybinds, GetKeybindButtonDescriptorNarrationInfo(button.keybindButtonDescriptor))
            end

            for _, button in ipairs(buttonsByAlignment[KEYBIND_STRIP_ALIGN_CENTER]) do
                table.insert(keybinds, GetKeybindButtonDescriptorNarrationInfo(button.keybindButtonDescriptor))
            end

            --Iterate backwards through the right aligned keybinds so they are ordered left to right visually
            for _, button in ZO_NumericallyIndexedTableReverseIterator(buttonsByAlignment[KEYBIND_STRIP_ALIGN_RIGHT]) do
                table.insert(keybinds, GetKeybindButtonDescriptorNarrationInfo(button.keybindButtonDescriptor))
            end

            local etherealDescriptors = {}
            --Iterate through ethereal keybinds to see if we need to narrate any
            for keybind, buttonOrEtherealDescriptor in pairs(self.keybinds) do
                if type(buttonOrEtherealDescriptor) ~= "userdata" then
                    --If any ethereal descriptor are marked as something we want to narrate, include them here
                    if GetValueFromRawOrFunction(buttonOrEtherealDescriptor, "ethereal") then
                        local narrateEthereal = buttonOrEtherealDescriptor.narrateEthereal
                        if type(narrateEthereal) == "function" then
                            narrateEthereal = narrateEthereal()
                        end

                        if narrateEthereal then
                            table.insert(etherealDescriptors, buttonOrEtherealDescriptor)
                        end
                    end
                end
            end

            table.sort(etherealDescriptors, EtherealSort)

            for _, descriptor in ipairs(etherealDescriptors) do
                table.insert(keybinds, GetKeybindButtonDescriptorNarrationInfo(descriptor))
            end
        end

        return keybinds
    end

    function ZO_KeybindStrip:UpdateAnchorsInternal(anchorTable, parent, initialConstrainXAnchor, initialConstrainYAnchor, subsequentAnchor)
        if IsInGamepadPreferredMode() then
            table.sort(anchorTable, GamepadSort)
        else
            table.sort(anchorTable, KeyboardSort)
        end
        for i, button in ipairs(anchorTable) do
            button:ClearAnchors()
        end

        local prevButton
        for i, button in ipairs(anchorTable) do
            local isVisible = IsVisible(button.keybindButtonDescriptor)
            local wasVisible = not button:IsHidden()
            if isVisible and not wasVisible then
                local UPDATE_ONLY = true
                self:SetUpButton(button, UPDATE_ONLY)
            end

            if isVisible then
                button:SetParent(parent)
                if prevButton then
                    subsequentAnchor:SetTarget(prevButton)
                    subsequentAnchor:AddToControl(button)
                else
                    initialConstrainXAnchor:AddToControl(button)
                    initialConstrainYAnchor:AddToControl(button)
                end
                prevButton = button
            end
        end
    end

    function ZO_KeybindStrip:UpdateAnchors()
        local hasCustomStyle = self.styleInfo ~= nil
        local yOffset = hasCustomStyle and self.styleInfo.yAnchorOffset or 0

        local firstAnchor = ZO_Anchor:New()
        firstAnchor:SetFromControlAnchor(self.control, 0)

        local secondAnchor = ZO_Anchor:New()
        secondAnchor:SetFromControlAnchor(self.control, 1)

        self.control:ClearAnchors()

        firstAnchor:SetOffsets(nil, yOffset)
        secondAnchor:SetOffsets(nil, yOffset)

        firstAnchor:AddToControl(self.control)
        secondAnchor:AddToControl(self.control)

        -- collect button alignments
        local buttonsByAlignment =
        {
            [KEYBIND_STRIP_ALIGN_LEFT] = {},
            [KEYBIND_STRIP_ALIGN_CENTER] = {},
            [KEYBIND_STRIP_ALIGN_RIGHT] = {},
        }
        for _, button in ipairs(self.keybindButtons) do
            local alignment = GetValueFromRawOrFunction(button.keybindButtonDescriptor, "alignment") or KEYBIND_STRIP_ALIGN_RIGHT
            table.insert(buttonsByAlignment[alignment], button)
        end

        -- layout the KEYBIND_STRIP_ALIGN_LEFT buttons
        local leftAnchorRelativeToControl = hasCustomStyle and self.styleInfo.leftAnchorRelativeToControl or nil
        local leftAnchorRelativePoint = hasCustomStyle and self.styleInfo.leftAnchorRelativePoint or LEFT
        local leftAnchorOffsetX = hasCustomStyle and self.styleInfo.leftAnchorOffset or 0
        local leftInitialConstrainXAnchor = ZO_Anchor:New(LEFT, leftAnchorRelativeToControl, leftAnchorRelativePoint, leftAnchorOffsetX, 0, ANCHOR_CONSTRAINS_X)
        local leftInitialConstrainYAnchor = ZO_Anchor:New(LEFT, nil, LEFT, 0, 0, ANCHOR_CONSTRAINS_Y)
        local leftSubsequentAnchor = ZO_Anchor:New(LEFT, nil, RIGHT)

        self:UpdateAnchorsInternal(buttonsByAlignment[KEYBIND_STRIP_ALIGN_LEFT], self.control, leftInitialConstrainXAnchor, leftInitialConstrainYAnchor, leftSubsequentAnchor)

        -- layout the KEYBIND_STRIP_ALIGN_RIGHT buttons
        local rightAnchorRelativeToControl = hasCustomStyle and self.styleInfo.rightAnchorRelativeToControl or nil
        local rightAnchorRelativePoint = hasCustomStyle and self.styleInfo.rightAnchorRelativePoint or RIGHT
        local rightAnchorOffsetX = hasCustomStyle and self.styleInfo.rightAnchorOffset or 0
        local rightInitialConstrainXAnchor = ZO_Anchor:New(RIGHT, rightAnchorRelativeToControl, rightAnchorRelativePoint, rightAnchorOffsetX, 0, ANCHOR_CONSTRAINS_X)
        local rightInitialConstrainYAnchor = ZO_Anchor:New(RIGHT, nil, RIGHT, 0, 0, ANCHOR_CONSTRAINS_Y)
        local rightSubsequentAnchor = ZO_Anchor:New(RIGHT, nil, LEFT)

        self:UpdateAnchorsInternal(buttonsByAlignment[KEYBIND_STRIP_ALIGN_RIGHT], self.control, rightInitialConstrainXAnchor, rightInitialConstrainYAnchor, rightSubsequentAnchor)

        -- layout the KEYBIND_STRIP_ALIGN_CENTER buttons
        local centerAnchorOffsetX = hasCustomStyle and self.styleInfo.centerAnchorOffset or 0
        local centerInitialConstrainXAnchor = ZO_Anchor:New(LEFT, nil, LEFT, centerAnchorOffsetX, 0, ANCHOR_CONSTRAINS_X)
        local centerInitialConstrainYAnchor = ZO_Anchor:New(LEFT, nil, LEFT, 0, 0, ANCHOR_CONSTRAINS_Y)
        local centerSubsequentAnchor = ZO_Anchor:New(LEFT, nil, RIGHT)

        self:UpdateAnchorsInternal(buttonsByAlignment[KEYBIND_STRIP_ALIGN_CENTER], self.centerParent, centerInitialConstrainXAnchor, centerInitialConstrainYAnchor, centerSubsequentAnchor)

        -- Clear alignment bumping, then determine if the center section needs to bump.
        self.centerParent:SetAnchor(CENTER)
        local leftGroupLeftEdge, leftGroupRightEdge = self:GetButtonsLeftAndRight(buttonsByAlignment[KEYBIND_STRIP_ALIGN_LEFT])
        local centerGroupLeftEdge, centerGroupRightEdge = self:GetButtonsLeftAndRight(buttonsByAlignment[KEYBIND_STRIP_ALIGN_CENTER])
        local rightGroupLeftEdge, rightGroupRightEdge = self:GetButtonsLeftAndRight(buttonsByAlignment[KEYBIND_STRIP_ALIGN_RIGHT])

        local centerOffsetFix = self:CalculateCenterButtonsOffset(leftGroupLeftEdge, leftGroupRightEdge, centerGroupLeftEdge, centerGroupRightEdge, rightGroupLeftEdge, rightGroupRightEdge)
        self.centerParent:SetAnchor(CENTER, nil, CENTER, centerOffsetFix)
    end

    -- Determine if the center buttons group is overlapping with either side and offset it to avoid overlap.
    function ZO_KeybindStrip:CalculateCenterButtonsOffset(leftGroupLeftEdge, leftGroupRightEdge, centerGroupLeftEdge, centerGroupRightEdge, rightGroupLeftEdge, rightGroupRightEdge)
        if centerGroupRightEdge ~= nil then
            -- As calculated, if either value is > 0, that pair of strips are overlapping.
            local rightSideOverlap = rightGroupLeftEdge ~= nil and centerGroupRightEdge - rightGroupLeftEdge or nil
            local leftSideOverlap = leftGroupRightEdge ~= nil and leftGroupRightEdge - centerGroupLeftEdge or nil
            if rightSideOverlap ~= nil and rightSideOverlap > 0 then
                -- If the right side is overlapping and there's space on the left to accomodate it
                if leftSideOverlap == nil or leftSideOverlap < -rightSideOverlap then
                    return -rightSideOverlap
                end
            elseif leftSideOverlap ~= nil and leftSideOverlap > 0 then
                -- If the left side is overlapping and there's space on the right to accomodate it
                if rightSideOverlap == nil or rightSideOverlap < -leftSideOverlap then
                    return leftSideOverlap
                end
            end
        end

        return 0
    end

    -- Determine the bounding edges of a keybinding button list (left/right/center) by accumulating button edges.
    function ZO_KeybindStrip:GetButtonsLeftAndRight(buttons)
        -- Initialize to ridiculous defaults.
        local min = math.huge
        local max = 0

        for i, button in ipairs(buttons) do
            if not button:IsHidden() then
                min = zo_min(min, button:GetLeft())
                max = zo_max(max, button:GetRight())
            end
        end

        if min < max then
            return min, max
        else
            -- Either there were no items in the list, no visible items, or button positions are messed up.
            return nil, nil
        end
    end
end
