local function IsChatNarrationEnabled()
    return GetSetting_Bool(SETTING_TYPE_ACCESSIBILITY, ACCESSIBILITY_SETTING_TEXT_CHAT_NARRATION)
end

local function IsScreenNarrationEnabled()
    return GetSetting_Bool(SETTING_TYPE_ACCESSIBILITY, ACCESSIBILITY_SETTING_SCREEN_NARRATION)
end

local NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY = 1
local NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY = 2
local NARRATION_ENTRY_TYPE_SORT_HEADER = 3
local NARRATION_ENTRY_TYPE_COMBO_BOX = 4
local NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY = 5
local NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER = 6
local NARRATION_ENTRY_TYPE_DIALOG = 7
local NARRATION_ENTRY_TYPE_FADING_CONTROL_BUFFER = 8
local NARRATION_ENTRY_TYPE_FOCUS_ENTRY = 9
local NARRATION_ENTRY_TYPE_GAMEPAD_GRID_ENTRY = 10
local NARRATION_ENTRY_TYPE_SPINNER = 11
local NARRATION_ENTRY_TYPE_GAMEPAD_BUTTON_TAB_BAR = 12
local NARRATION_ENTRY_TYPE_CUSTOM = 13

--Narration is blocked due to movement, such as scrolling through a list
local SCREEN_NARRATION_BLOCKED_REASON_MOVEMENT = 1
--Narration is blocked due to VO currently playing
local SCREEN_NARRATION_BLOCKED_REASON_VO = 2

local SCREEN_NARRATION_QUEUE_DELAY_MS = 250

internalassert(NARRATION_TYPE_MAX_VALUE == 5, "A new narration type has been added, does it need to be added to the QUEUEABLE_NARRATION_TYPES table?")

--Narration types able to be used when queueing something up for narration. Narration for types not in this table should be sent straight to the C++
local QUEUEABLE_NARRATION_TYPES =
{
    [NARRATION_TYPE_UI_SCREEN] = true,
    [NARRATION_TYPE_HUD] = true,
    [NARRATION_TYPE_ALERT] = true,
}

--The narration queue each entry type will go into by default
local DEFAULT_NARRATION_TYPES =
{
    [NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_SORT_HEADER] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_COMBO_BOX] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_DIALOG] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_FADING_CONTROL_BUFFER] = NARRATION_TYPE_ALERT,
    [NARRATION_ENTRY_TYPE_FOCUS_ENTRY] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_GAMEPAD_GRID_ENTRY] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_SPINNER] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_GAMEPAD_BUTTON_TAB_BAR] = NARRATION_TYPE_UI_SCREEN,
    [NARRATION_ENTRY_TYPE_CUSTOM] = NARRATION_TYPE_UI_SCREEN,
}

local function GetDefaultNarrationType(narrationEntryType)
    return DEFAULT_NARRATION_TYPES[narrationEntryType]
end

---------------------------------------------
-- Narratable Object
---------------------------------------------

ZO_NarratableObject = ZO_PooledObject:Subclass()

function ZO_NarratableObject:Initialize()
    self.narrationStrings = {}
end

function ZO_NarratableObject:Reset()
    ZO_ClearNumericallyIndexedTable(self.narrationStrings)
    self.pauseTime = nil
    self.delimiter = nil
end

--Adds the specified narration text. Narration text can be a string, table of strings, or number
--If addToFront is true, the text will be added to the front of the narration
function ZO_NarratableObject:AddNarrationText(narrationText, addToFront)
    if not narrationText or narrationText == "" then
        return
    end

    if type(narrationText) == "string" then
        if addToFront then
            table.insert(self.narrationStrings, 1, narrationText)
        else
            table.insert(self.narrationStrings, narrationText)
        end
    elseif type(narrationText) == "table" then
        if addToFront then
            ZO_CombineNumericallyIndexedTables(narrationText, self.narrationStrings)
            self.narrationStrings = narrationText
        else
            ZO_CombineNumericallyIndexedTables(self.narrationStrings, narrationText)
        end
    elseif type(narrationText) == "number" then
        if addToFront then
            table.insert(self.narrationStrings, 1, tostring(narrationText))
        else
            table.insert(self.narrationStrings, tostring(narrationText))
        end
    else
        internalassert(false, string.format("%s is not a valid value for narrationText", narrationText))
    end
end

--Sets the pause time to use when forming the final narration string
--Pause time and delimiter are mutually exclusive; setting one will clear the other
function ZO_NarratableObject:SetPauseTimeMS(pauseTime)
    self.pauseTime = pauseTime
    self.delimiter = nil
end

--Sets the delimiter to use when forming the final narration string
--Pause time and delimiter are mutually exclusive; setting one will clear the other
function ZO_NarratableObject:SetDelimiter(delimiter)
    self.delimiter = delimiter
    self.pauseTime = nil
end

function ZO_NarratableObject:AddToPending()
    --If we don't have any strings, don't bother
    if #self.narrationStrings > 0 then
        --If a delimiter is set use that, otherwise fall back to pause time
        if self.delimiter then
            AddPendingNarrationText(ZO_GenerateDelimiterSeparatedList(self.narrationStrings, self.delimiter))
        else
            for _, narrationString in ipairs(self.narrationStrings) do
                AddPartialPendingNarrationText(narrationString)
            end
            FinalizePartialPendingNarrationText(self.pauseTime)
        end
    end
end

---------------------------------------------
-- Screen Narration Params
---------------------------------------------

ZO_ScreenNarrationParams = ZO_PooledObject:Subclass()

function ZO_ScreenNarrationParams:Initialize()
    self:Reset()
end

function ZO_ScreenNarrationParams:Reset()
    --Calling ZO_ClearTable on self will clear out any variables that were set on this object
    ZO_ClearTable(self)
    self.startTimeMs = 0
end

function ZO_ScreenNarrationParams:SetStartTimeMS(startTimeMs)
    self.startTimeMs = startTimeMs
end

function ZO_ScreenNarrationParams:GetStartTimeMS()
    return self.startTimeMs
end

function ZO_ScreenNarrationParams:GetNarrationBlocked()
    if self.blockedReasons then
        for reason, isBlocked in pairs(self.blockedReasons) do
            if isBlocked then
                return true
            end
        end
    end
    return false
end

function ZO_ScreenNarrationParams:SetNarrationBlockedForReason(isBlocked, reason)
    if not self.blockedReasons then
        self.blockedReasons = {}
    end

    self.blockedReasons[reason] = isBlocked
end

function ZO_ScreenNarrationParams:GetNarrationBlockedForReason(reason)
    if self.blockedReasons then
        return self.blockedReasons[reason]
    end

    return false
end

function ZO_ScreenNarrationParams:SetNarrationType(narrationType)
    self.narrationType = narrationType
end

function ZO_ScreenNarrationParams:GetNarrationType()
    if self.narrationType then
        return self.narrationType
    else
        return GetDefaultNarrationType(self:GetNarrationEntryType())
    end
end

function ZO_ScreenNarrationParams:SetNarrationEntryType(narrationEntryType)
    self.narrationEntryType = narrationEntryType
end

function ZO_ScreenNarrationParams:GetNarrationEntryType()
    return self.narrationEntryType
end

function ZO_ScreenNarrationParams:GetNarrateHeader()
    return self.narrateHeader
end

function ZO_ScreenNarrationParams:GetNarrateSubHeader()
    return self.narrateSubHeader
end

function ZO_ScreenNarrationParams:SetParametricList(list, narrateHeader)
    self.parametricList = list
    self.narrateHeader = narrateHeader
end

function ZO_ScreenNarrationParams:GetParametricList()
    return self.parametricList
end

function ZO_ScreenNarrationParams:SetGridList(gridList, narrateHeader, narrateSubHeader)
    self.gridList = gridList
    self.narrateHeader = narrateHeader
    self.narrateSubHeader = narrateSubHeader
end

function ZO_ScreenNarrationParams:GetGridList()
    return self.gridList
end

function ZO_ScreenNarrationParams:SetGamepadGrid(gamepadGrid, narrateHeader)
    self.gamepadGrid = gamepadGrid
    self.narrateHeader = narrateHeader
end

function ZO_ScreenNarrationParams:GetGamepadGrid()
    return self.gamepadGrid
end

function ZO_ScreenNarrationParams:SetSelectedSortHeader(sortHeaderGroup, key)
    self.sortHeaderGroup = sortHeaderGroup
    self.selectedSortHeaderKey = key
end

function ZO_ScreenNarrationParams:GetSortHeaderGroup()
    return self.sortHeaderGroup
end

function ZO_ScreenNarrationParams:GetSelectedSortHeaderKey()
    return self.selectedSortHeaderKey
end

function ZO_ScreenNarrationParams:SetComboBox(comboBox)
    self.comboBox = comboBox
end

function ZO_ScreenNarrationParams:GetComboBox()
    return self.comboBox
end

function ZO_ScreenNarrationParams:SetSpinner(spinner, narrateHeader)
    self.spinner = spinner
    self.narrateHeader = narrateHeader
end

function ZO_ScreenNarrationParams:GetSpinner()
    return self.spinner
end

function ZO_ScreenNarrationParams:SetGamepadButtonTabBar(tabBar)
    self.gamepadButtonTabBar = tabBar
end

function ZO_ScreenNarrationParams:GetGamepadButtonTabBar()
    return self.gamepadButtonTabBar
end

function ZO_ScreenNarrationParams:SetTextSearchHeader(textSearchHeader, narrateHeader)
    self.textSearchHeader = textSearchHeader
    self.narrateHeader = narrateHeader
end

function ZO_ScreenNarrationParams:GetTextSearchHeader(textSearchHeader)
    return self.textSearchHeader
end

function ZO_ScreenNarrationParams:SetDialog(dialog, narrateBaseText)
    self.dialog = dialog
    self.narrateDialogBaseText = narrateBaseText
end

function ZO_ScreenNarrationParams:GetDialog()
    return self.dialog
end

function ZO_ScreenNarrationParams:GetNarrateDialogBaseText()
    return self.narrateDialogBaseText
end

function ZO_ScreenNarrationParams:SetFadingControlBuffer(fadingControlBuffer)
    self.fadingControlBuffer = fadingControlBuffer
end

function ZO_ScreenNarrationParams:GetFadingControlBuffer()
    return self.fadingControlBuffer
end

function ZO_ScreenNarrationParams:SetFocus(focus, narrateHeader, narrateSubHeader)
    self.focus = focus
    self.narrateHeader = narrateHeader
    self.narrateSubHeader = narrateSubHeader
end

function ZO_ScreenNarrationParams:GetFocus()
    return self.focus
end

function ZO_ScreenNarrationParams:SetCustomObjectName(objectName, narrateHeader)
    self.customObjectName = objectName
    self.narrateHeader = narrateHeader
end

function ZO_ScreenNarrationParams:GetCustomObjectName()
    return self.customObjectName
end

---------------------------------------------
-- Screen Narration Manager
---------------------------------------------

ZO_ScreenNarrationManager = ZO_InitializingCallbackObject:Subclass()

function ZO_ScreenNarrationManager:Initialize()
    SCREEN_NARRATION_MANAGER = self
    self.parametricListTargetChangedCallback = function(list, targetData, oldTargetData, reachedTarget, targetSelectedIndex, reselectingDuringRebuild)
        local narrationInfo = self.parametricListNarrationInfo[list]
        if narrationInfo:canNarrate() and not reselectingDuringRebuild then
            self:QueueParametricListEntry(list)
        end
    end

    self.parametricListActivatedChangedCallback = function(list, activated)
        if activated then
            local NARRATE_HEADER = true
            self:QueueParametricListEntry(list, NARRATE_HEADER)
        end
    end

    --Re-narrate any active parametric lists upon hiding dialogs
    self.parametricListAllDialogsHiddenCallback = function(list)
        if list:IsActive() then
            local NARRATE_HEADER = true
            self:QueueParametricListEntry(list, NARRATE_HEADER)
        end
    end

    self.parametricListMovementChangedCallback = function(list, isMoving)
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY))
        if queuedNarration and queuedNarration:GetParametricList() == list then
            queuedNarration:SetNarrationBlockedForReason(isMoving, SCREEN_NARRATION_BLOCKED_REASON_MOVEMENT)
        end
    end

    self.comboBoxItemSelectedCallback = function(control, data, comboBox)
        self:QueueComboBox(comboBox)
    end

    self.gridListAllDialogsHiddenCallback = function(gridList)
        if gridList:IsActive() then
            local NARRATE_HEADER = true
            local NARRATE_SUB_HEADER = true
            self:QueueGridListEntry(gridList, NARRATE_HEADER, NARRATE_SUB_HEADER)
        end
    end

    self.gamepadGridAllDialogsHiddenCallback = function(gamepadGrid)
        if gamepadGrid:IsActive() then
            local NARRATE_HEADER = true
            self:QueueGamepadGridEntry(gamepadGrid, NARRATE_HEADER)
        end
    end

    self.spinnerAllDialogsHiddenCallback = function(spinner)
        if spinner:IsActive() then
            local NARRATE_HEADER = true
            self:QueueSpinner(spinner, NARRATE_HEADER)
        end
    end

    self.customObjectAllDialogsHiddenCallback = function(objectName)
        local narrationInfo = self.customObjectNarrationInfo[objectName]
        if narrationInfo and narrationInfo:canNarrate() then
            local NARRATE_HEADER = true
            self:QueueCustomEntry(objectName, NARRATE_HEADER)
        end
    end

    self.parametricListNarrationInfo = {}
    self.customObjectNarrationInfo = {}
    self.textSearchHeaderNarrationInfo = {}
    self.queuedNarrations = {}
    self.isLoading = not GetGuiHidden("app")

    self:RegisterForEvents()
    self:InitializeNarrationPools()
end

function ZO_ScreenNarrationManager:RegisterForEvents()
    local function OnUpdate()
        --Do not start any narrations while a load screen is active
        if self.isLoading then
            return
        end

        local currentTimeMs = GetFrameTimeMilliseconds()
        --Loop through each queue and try to narrate anything that is able
        for narrationType = NARRATION_TYPE_ITERATION_BEGIN, NARRATION_TYPE_ITERATION_END do
            local queuedNarration = self:GetQueuedNarration(narrationType)
            if queuedNarration then
                --If a narration has been set to blocked, it will stay in the queue until we set blocked to false or it is replaced by something else
                if not queuedNarration:GetNarrationBlocked() and queuedNarration.startTimeMs <= currentTimeMs then
                    self:TryNarration(queuedNarration)
                    self:ClearQueuedNarration(narrationType)
                end
            end
        end
    end

    local function OnAppGuiHiddenStateChanged(_, hidden)
        self.isLoading = not hidden
        SetTextChatNarrationQueueEnabled(hidden)
        SetCenterScreenAnnounceNarrationQueueEnabled(hidden)
        if self.isLoading then
            --Loop through each queueable narration type and clear out the queue
            for narrationType = NARRATION_TYPE_ITERATION_BEGIN, NARRATION_TYPE_ITERATION_END do
                if QUEUEABLE_NARRATION_TYPES[narrationType] then
                    self:ClearQueuedNarration(narrationType)
                end
            end
        end
    end

    local function OnInteractVOPlayingStateUpdated()
        if ZO_IsIngameUI() and IsInteracting() and IsInteractVOPlaying() then
            ClearNarrationQueue(NARRATION_TYPE_UI_SCREEN)
        end

        --If there are any queued parametric list narrations that are blocked by interact VO, update them now
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY))
        if queuedNarration then
            local list = queuedNarration:GetParametricList()
            local narrationInfo = self.parametricListNarrationInfo[list]
            if narrationInfo.isBlockedByInteractVO then
                queuedNarration:SetNarrationBlockedForReason(IsInteractVOPlaying(), SCREEN_NARRATION_BLOCKED_REASON_VO)
            end
        end
    end

    EVENT_MANAGER:RegisterForUpdate("ScreenNarrationUpdate", 0, OnUpdate)
    EVENT_MANAGER:RegisterForEvent("ScreenNarrationManager", EVENT_APP_GUI_HIDDEN_STATE_CHANGED, OnAppGuiHiddenStateChanged)
    EVENT_MANAGER:RegisterForEvent("ScreenNarrationManager", EVENT_INTERACT_VO_PLAYING_STATE_UPDATED, OnInteractVOPlayingStateUpdated)
end

function ZO_ScreenNarrationManager:InitializeNarrationPools()
    local function NarrationParamFactory(pool)
        return ZO_ScreenNarrationParams:New()
    end

    local function NarrationParamReset(params)
        params:Reset()
    end

    local function NarrationParamAcquire(object, objectKey)
        --Narration params need to re-set their pool and key each time we acquire due to us calling ZO_ClearTable(self) in the reset
        object:SetPoolAndKey(self.narrationParamsPool, objectKey)
    end

    self.narrationParamsPool = ZO_ObjectPool:New(NarrationParamFactory, NarrationParamReset)
    self.narrationParamsPool:SetCustomAcquireBehavior(NarrationParamAcquire)

    local function NarratableObjectFactory(pool, key)
        local narratableObject = ZO_NarratableObject:New()
        narratableObject:SetPoolAndKey(pool, key)
        return narratableObject
    end

    local function NarratableObjectReset(narratableObject)
        narratableObject:Reset()
    end

    self.narratableObjectPool = ZO_ObjectPool:New(NarratableObjectFactory, NarratableObjectReset)
end

function ZO_ScreenNarrationManager:RegisterCustomObject(objectName, narrationInfo)
    if self.customObjectNarrationInfo[objectName] then
        local assertMessage = string.format("A custom narration object with the name \"%s\" has already been registered.", objectName)
        internalassert(false, assertMessage)
    end

    CALLBACK_MANAGER:RegisterCallback("AllDialogsHidden", self.customObjectAllDialogsHiddenCallback, objectName)
    self.customObjectNarrationInfo[objectName] = narrationInfo
end

function ZO_ScreenNarrationManager:RegisterParametricList(list, narrationInfo)
    list:RegisterCallback("TargetDataChanged", self.parametricListTargetChangedCallback)
    list:RegisterCallback("ActivatedChanged", self.parametricListActivatedChangedCallback)
    list:RegisterCallback("MovementChanged", self.parametricListMovementChangedCallback)
    CALLBACK_MANAGER:RegisterCallback("AllDialogsHidden", self.parametricListAllDialogsHiddenCallback, list)
    --Store off the associated parametric list narration info for this list, so we can access it later
    self.parametricListNarrationInfo[list] = narrationInfo
end

function ZO_ScreenNarrationManager:RegisterParametricListScreen(list, screen)
    local narrationInfo =
    {
        canNarrate = function()
            return screen:IsShowing()
        end,
        headerNarrationFunction = function()
            return screen:GetHeaderNarration()
        end,
        footerNarrationFunction = function()
            return screen:GetFooterNarration()
        end,
    }
    self:RegisterParametricList(list, narrationInfo)
end

function ZO_ScreenNarrationManager:UnregisterParametricList(list)
    list:UnregisterCallback("TargetDataChanged", self.parametricListTargetChangedCallback)
    list:UnregisterCallback("ActivatedChanged", self.parametricListActivatedChangedCallback)
    list:UnregisterCallback("MovementChanged", self.parametricListMovementChangedCallback)
    CALLBACK_MANAGER:UnregisterCallback("AllDialogsHidden", self.parametricListAllDialogsHiddenCallback, list)
    self.parametricListNarrationInfo[list] = nil
    --Grab the currently queued narration for the matching narration type
    local narrationType = GetDefaultNarrationType(NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY)
    local queuedNarration = self:GetQueuedNarration(narrationType)
    --If the list we are unregistering is currently the queued narration, clear it
    if queuedNarration and queuedNarration:GetParametricList() == list then
        self:ClearQueuedNarration(narrationType)
    end
end

function ZO_ScreenNarrationManager:RegisterParametricListDialog(dialog)
    dialog.entryList:RegisterCallback("TargetDataChanged", function(list, targetData, oldTargetData, reachedTarget, targetSelectedIndex, reselectingDuringRebuild)
        if not reselectingDuringRebuild then
            self:QueueDialog(dialog)
        end
    end)

    dialog.entryList:RegisterCallback("ActivatedChanged", function(list, activated)
        if activated then
            self:QueueDialog(dialog)
        end
    end)
end

function ZO_ScreenNarrationManager:RegisterTextSearchHeader(textSearchHeader, narrationInfo)
    textSearchHeader:RegisterCallback("EditBoxFocusLost", function()
        self:QueueTextSearchHeader(textSearchHeader)
    end)

    textSearchHeader:RegisterCallback("FocusActivated", function()
        local NARRATE_HEADER = true
        self:QueueTextSearchHeader(textSearchHeader, NARRATE_HEADER)
    end)

    --Store off the associated text search header narration info for this text search header, so we can access it later
    self.textSearchHeaderNarrationInfo[textSearchHeader] = narrationInfo
end

function ZO_ScreenNarrationManager:RegisterGridList(gridList)
    gridList:SetOnSelectedDataChangedCallback(function(previousData, newData)
        if gridList:IsActive() and newData then
            local narrateSubHeader = false
            if previousData then
                local newGridHeaderData = newData.gridHeaderName or newData.gridHeaderData
                local previousGridHeaderData = previousData.gridHeaderName or previousData.gridHeaderData
                narrateSubHeader = newGridHeaderData ~= previousGridHeaderData
            end
            local DONT_NARRATE_HEADER = false
            self:QueueGridListEntry(gridList, DONT_NARRATE_HEADER, narrateSubHeader)
        end
    end)

    gridList:RegisterCallback("OnActivated", function(selectedData)
        local NARRATE_HEADER = true
        local NARRATE_SUB_HEADER = true
        self:QueueGridListEntry(gridList, NARRATE_HEADER, NARRATE_SUB_HEADER)
    end)

    CALLBACK_MANAGER:RegisterCallback("AllDialogsHidden", self.gridListAllDialogsHiddenCallback, gridList)
end

function ZO_ScreenNarrationManager:RegisterGamepadGrid(gamepadGrid)
    gamepadGrid:RegisterCallback("OnActivated", function()
        local NARRATE_HEADER = true
        self:QueueGamepadGridEntry(gamepadGrid, NARRATE_HEADER)
    end)

    gamepadGrid:RegisterCallback("FocusChanged", function()
        if gamepadGrid:IsActive() then
            self:QueueGamepadGridEntry(gamepadGrid)
        end
    end)

    CALLBACK_MANAGER:RegisterCallback("AllDialogsHidden", self.gamepadGridAllDialogsHiddenCallback, gamepadGrid)
end

function ZO_ScreenNarrationManager:RegisterComboBox(comboBox)
    comboBox:RegisterCallback("OnItemSelected", self.comboBoxItemSelectedCallback)
end

function ZO_ScreenNarrationManager:RegisterDialogDropdown(dialog, dropdown)
    dropdown:RegisterCallback("OnDeactivated", function()
        self:QueueDialog(dialog)
    end)
end

function ZO_ScreenNarrationManager:RegisterSpinner(spinner)
    spinner:RegisterCallback("OnValueChanged", function()
        if spinner:IsActive() then
            self:QueueSpinner(spinner)
        end
    end)

    spinner:RegisterCallback("OnActivated", function()
        local NARRATE_HEADER = true
        self:QueueSpinner(spinner, NARRATE_HEADER)
    end)

    CALLBACK_MANAGER:RegisterCallback("AllDialogsHidden", self.spinnerAllDialogsHiddenCallback, spinner)
end

function ZO_ScreenNarrationManager:RegisterGamepadButtonTabBar(tabBar)
    tabBar:RegisterCallback("OnActivated", function()
        self:QueueGamepadButtonTabBar(tabBar)
    end)

    tabBar:RegisterCallback("OnSelectionChanged", function()
        if tabBar:IsActivated() then
            self:QueueGamepadButtonTabBar(tabBar)
        end
    end)
end

function ZO_ScreenNarrationManager:RegisterFadingControlBuffer(fadingControlBuffer)
    fadingControlBuffer:RegisterCallback("OnEntryUpdated", function()
        self:QueueFadingControlBuffer(fadingControlBuffer)
    end)
end

function ZO_ScreenNarrationManager:CreateNarrationParams(narrationEntryType, delayMS)
    local narrationParams = self.narrationParamsPool:AcquireObject()
    narrationParams:SetNarrationEntryType(narrationEntryType)
    narrationParams:SetStartTimeMS(delayMS + GetFrameTimeMilliseconds())
    return narrationParams
end

function ZO_ScreenNarrationManager:CreateNarratableObject(narrationText, pauseTime)
    local narratableObject = self.narratableObjectPool:AcquireObject()
    narratableObject:AddNarrationText(narrationText)
    narratableObject:SetPauseTimeMS(pauseTime)
    return narratableObject
end

--Queues the given narrationParams into its associated queue
function ZO_ScreenNarrationManager:SetQueuedNarration(narrationParams)
    local narrationType = narrationParams:GetNarrationType()
    if QUEUEABLE_NARRATION_TYPES[narrationType] then
        --Make sure to release any already existing queued narrations before setting the new one
        local queuedNarration = self:GetQueuedNarration(narrationType)
        if queuedNarration then
            queuedNarration:ReleaseObject()
        end
        self.queuedNarrations[narrationType] = narrationParams
    else
        internalassert(false, "Attempting to queue an invalid narration type, please see the QUEUEABLE_NARRATION_TYPES table for the queueable narration types")
    end
end

--Get the narration params currently queued for the specified narration type
function ZO_ScreenNarrationManager:GetQueuedNarration(narrationType)
    return self.queuedNarrations[narrationType]
end

--Clears the queued narration associated with the specified narration type
function ZO_ScreenNarrationManager:ClearQueuedNarration(narrationType)
    if self.queuedNarrations[narrationType] then
        self.queuedNarrations[narrationType]:ReleaseObject()
    end
    self.queuedNarrations[narrationType] = nil
end

function ZO_ScreenNarrationManager:TryNarration(narrationParams)
    if IsScreenNarrationEnabled() then
        local narrationType = narrationParams:GetNarrationType()
        local narrationEntryType = narrationParams:GetNarrationEntryType()
        if narrationEntryType == NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY then
            local list = narrationParams:GetParametricList()
            local narrationInfo = self.parametricListNarrationInfo[list]
            if narrationInfo:canNarrate() and list:IsActive() then
                self:NarrateParametricListEntry(narrationInfo, list, list:GetTargetData(), list:GetTargetControl(), narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY then
            local list = narrationParams:GetParametricList()
            if list:IsActivated() then
                self:NarrateSortFilterListEntry(list, narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_SORT_HEADER then
            local sortHeaderGroup = narrationParams:GetSortHeaderGroup()
            local list = narrationParams:GetParametricList()
            local key = narrationParams:GetSelectedSortHeaderKey()
            if list:IsActive() and sortHeaderGroup:IsEnabled() and sortHeaderGroup:IsKeyCurrentSelectedIndex(key) then
                self:NarrateSelectedSortHeader(list, sortHeaderGroup, key, narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_COMBO_BOX then
            self:NarrateComboBox(narrationParams:GetComboBox(), narrationType)
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY then
            local gridList = narrationParams:GetGridList()
            if gridList:IsActive() then
                self:NarrateGridListEntry(gridList, narrationParams:GetNarrateHeader(), narrationParams:GetNarrateSubHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_GAMEPAD_GRID_ENTRY then
            local gamepadGrid = narrationParams:GetGamepadGrid()
            if gamepadGrid:IsActive() then
                self:NarrateGamepadGridEntry(gamepadGrid, narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER then
            local textSearchHeader = narrationParams:GetTextSearchHeader()
            local narrationInfo = self.textSearchHeaderNarrationInfo[textSearchHeader]
            if textSearchHeader:IsActive() then
                self:NarrateTextSearchHeader(textSearchHeader, narrationInfo, narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_DIALOG then
            local dialog = narrationParams:GetDialog()
            --ZO_Dialogs_IsShowing will continue to return true until the dialog is hidden, so we need to check if it's in the process of hiding separately
            if ZO_Dialogs_IsShowing(dialog.name) and not ZO_Dialogs_IsDialogHiding(dialog.name) then
                self:NarrateDialog(dialog, narrationParams:GetNarrateDialogBaseText(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_FADING_CONTROL_BUFFER then
            self:NarrateFadingControlBuffer(narrationParams:GetFadingControlBuffer(), narrationType)
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_FOCUS_ENTRY then
            local focus = narrationParams:GetFocus()
            if focus:IsActive() then
                self:NarrateFocusEntry(focus, narrationParams:GetNarrateHeader(), narrationParams:GetNarrateSubHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_SPINNER then
            local spinner = narrationParams:GetSpinner()
            if spinner:IsActive() then
                self:NarrateSpinner(spinner, narrationParams:GetNarrateHeader(), narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_GAMEPAD_BUTTON_TAB_BAR then
            local tabBar = narrationParams:GetGamepadButtonTabBar()
            if tabBar:IsActivated() then
                self:NarrateGamepadButtonTabBar(tabBar, narrationType)
            end
        elseif narrationEntryType == NARRATION_ENTRY_TYPE_CUSTOM then
            local customObjectName = narrationParams:GetCustomObjectName()
            local narrationInfo = self.customObjectNarrationInfo[customObjectName]
            if narrationInfo:canNarrate() then
                self:NarrateCustomEntry(narrationInfo, narrationParams:GetNarrateHeader(), narrationType)
            end
        else
            internalassert(false, "Unhandled narration entry type")
        end
    end
end

function ZO_ScreenNarrationManager:QueueParametricListEntry(list, narrateHeader, overrideQueueDelayMs)
    if IsScreenNarrationEnabled() then
        local narrationInfo = self.parametricListNarrationInfo[list]
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY, overrideQueueDelayMs or SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetNarrationType(narrationInfo.narrationType)

        local queuedNarration = self:GetQueuedNarration(narrationParams:GetNarrationType())
        local narrationBlockedByMovement = false
        local narrationBlockedByVO = narrationInfo.isBlockedByInteractVO and IsInteractVOPlaying()

        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_PARAMETRIC_LIST_ENTRY then
            local queuedList = queuedNarration:GetParametricList()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            local queuedNarrationBlocked = queuedNarration:GetNarrationBlocked()
            --If the list we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedList == list and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end

            --If the list we are queueing is the same one we are overwriting, then make sure we remain blocked if necessary
            if queuedList == list and queuedNarrationBlocked then
                narrationBlockedByMovement = queuedNarration:GetNarrationBlockedForReason(SCREEN_NARRATION_BLOCKED_REASON_MOVEMENT)
            end
        end

        narrationParams:SetParametricList(list, narrateHeader)
        narrationParams:SetNarrationBlockedForReason(narrationBlockedByMovement, SCREEN_NARRATION_BLOCKED_REASON_MOVEMENT)
        narrationParams:SetNarrationBlockedForReason(narrationBlockedByVO, SCREEN_NARRATION_BLOCKED_REASON_VO)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueSortFilterListEntry(list, narrateHeader)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY then
            local queuedList = queuedNarration:GetParametricList()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            --If the list we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedList == list and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end
        end
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_SORT_FILTER_LIST_ENTRY, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetParametricList(list, narrateHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueSelectedSortHeader(list, sortHeaderGroup, key, narrateHeader)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_SORT_HEADER))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_SORT_HEADER then
            local queuedList = queuedNarration:GetParametricList()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            --If the list we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedList == list and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end
        end
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_SORT_HEADER, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetParametricList(list, narrateHeader)
        narrationParams:SetSelectedSortHeader(sortHeaderGroup, key)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueComboBox(comboBox)
    if IsScreenNarrationEnabled() then
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_COMBO_BOX, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetComboBox(comboBox)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueGridListEntry(gridList, narrateHeader, narrateSubHeader)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY then
            local queuedGridList = queuedNarration:GetGridList()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            local queuedNarrateSubHeader = queuedNarration:GetNarrateSubHeader()
            --If the grid list we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedGridList == gridList and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end

            --If the grid list we are queueing is the same one we are overwriting, then make sure we still read the subheader if necessary
            if queuedGridList == gridList and queuedNarrateSubHeader then
                narrateSubHeader = queuedNarrateSubHeader
            end
        end
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_GRID_LIST_ENTRY, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetGridList(gridList, narrateHeader, narrateSubHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueGamepadGridEntry(gamepadGrid, narrateHeader)
    if IsScreenNarrationEnabled() then
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_GAMEPAD_GRID_ENTRY, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetGamepadGrid(gamepadGrid, narrateHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueTextSearchHeader(textSearchHeader, narrateHeader)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER then
            local queuedTextSearchHeader = queuedNarration:GetTextSearchHeader()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            --If the text search header we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedTextSearchHeader == textSearchHeader and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end
        end
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_TEXT_SEARCH_HEADER, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetTextSearchHeader(textSearchHeader, narrateHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueDialog(dialog, narrateBaseText)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_DIALOG))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_DIALOG then
            local queuedDialog = queuedNarration:GetDialog()
            local queuedNarrateBaseText = queuedNarration:GetNarrateDialogBaseText()
            --If the dialog we are queuing is the same one we are overwriting, then make sure we still read the base text if necessary
            if queuedDialog == dialog and queuedNarrateBaseText then
                narrateBaseText = queuedNarrateBaseText
            end
        end
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_DIALOG, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetDialog(dialog, narrateBaseText)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueFadingControlBuffer(fadingControlBuffer)
    if IsScreenNarrationEnabled() then
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_FADING_CONTROL_BUFFER, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetFadingControlBuffer(fadingControlBuffer)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueFocus(focus, narrateHeader, narrateSubHeader)
    if IsScreenNarrationEnabled() then
        local queuedNarration = self:GetQueuedNarration(GetDefaultNarrationType(NARRATION_ENTRY_TYPE_FOCUS_ENTRY))
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_FOCUS_ENTRY then
            local queuedFocus = queuedNarration:GetFocus()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            --If the focus we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedFocus == focus and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end

            local queuedNarrateSubHeader = queuedNarration:GetNarrateSubHeader()
            --If the focus we are queueing is the same one we are overwriting, then make sure we still read the sub header if necessary
            if queuedFocus == focus and queuedNarrateSubHeader then
                narrateSubHeader = queuedNarrateSubHeader
            end
        end

        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_FOCUS_ENTRY, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetFocus(focus, narrateHeader, narrateSubHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueSpinner(spinner, narrateHeader)
    if IsScreenNarrationEnabled() then
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_SPINNER, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetSpinner(spinner, narrateHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueCustomEntry(objectName, narrateHeader)
    if IsScreenNarrationEnabled() then
        local narrationInfo = self.customObjectNarrationInfo[objectName]
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_CUSTOM, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetNarrationType(narrationInfo.narrationType)

        local queuedNarration = self:GetQueuedNarration(narrationParams:GetNarrationType())
        if queuedNarration and queuedNarration:GetNarrationEntryType() == NARRATION_ENTRY_TYPE_CUSTOM then
            local queuedCustomObjectName = queuedNarration:GetCustomObjectName()
            local queuedNarrateHeader = queuedNarration:GetNarrateHeader()
            --If the custom object we are queueing is the same one we are overwriting, then make sure we still read the header if necessary
            if queuedCustomObjectName == objectName and queuedNarrateHeader then
                narrateHeader = queuedNarrateHeader
            end
        end

        narrationParams:SetCustomObjectName(objectName, narrateHeader)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueGamepadButtonTabBar(tabBar)
    if IsScreenNarrationEnabled() then
        local narrationParams = self:CreateNarrationParams(NARRATION_ENTRY_TYPE_GAMEPAD_BUTTON_TAB_BAR, SCREEN_NARRATION_QUEUE_DELAY_MS)
        narrationParams:SetGamepadButtonTabBar(tabBar)
        self:SetQueuedNarration(narrationParams)
    end
end

function ZO_ScreenNarrationManager:QueueCSA(messageParams)
    if IsScreenNarrationEnabled() then
        --CSAs do not need to wait before sending to the C++, so attempt to narrate them immediately
        self:NarrateText(messageParams:GetNarrationText(), NARRATION_TYPE_CENTER_SCREEN_ANNOUNCEMENT)
    end
end

function ZO_ScreenNarrationManager:QueueCountdownCSA(countdownTime)
    if IsScreenNarrationEnabled() then
        --CSAs do not need to wait before sending to the C++, so attempt to narrate them immediately
        --Countdown CSAs should not queue up behind other CSAs, so put them in the higher priority alert queue instead
        self:NarrateText(self:CreateNarratableObject(countdownTime), NARRATION_TYPE_ALERT)
    end
end

do
    local KEYBIND_PAUSE_TIME_MS = 100
    --Gets a narratable object for the currently active keybinds on the keybind strip
    --If additionalInputNarrationFunction is specified, it will append the return keybind info to the end of the narration of the keybind strip
    --If overrideInputNarrationFunction is specified, only the keybind info it returns will be narrated
    function ZO_ScreenNarrationManager:GetKeybindNarration(additionalInputNarrationFunction, overrideInputNarrationFunction)
        local keybindButtonInfo
        if overrideInputNarrationFunction then
            keybindButtonInfo = overrideInputNarrationFunction()
        else
            keybindButtonInfo = KEYBIND_STRIP:GetOrderedNarratableKeybindButtonInfo()
            if additionalInputNarrationFunction then
                ZO_CombineNumericallyIndexedTables(keybindButtonInfo, additionalInputNarrationFunction())
            end
        end

        local keybindNarration = self:CreateNarratableObject(nil, KEYBIND_PAUSE_TIME_MS)
        for i, buttonInfo in ipairs(keybindButtonInfo) do
            local narrationText
            if buttonInfo.name then
                local formatter
                if i == 1 then
                    formatter = buttonInfo.enabled and SI_SCREEN_NARRATION_FIRST_KEYBIND_FORMATTER or SI_SCREEN_NARRATION_DISABLED_FIRST_KEYBIND_FORMATTER
                else
                    formatter = buttonInfo.enabled and SI_SCREEN_NARRATION_KEYBIND_FORMATTER or SI_SCREEN_NARRATION_DISABLED_KEYBIND_FORMATTER
                end
                narrationText = zo_strformat(formatter, buttonInfo.keybindName, buttonInfo.name)
            else
                local formatter
                if i == 1 then
                    formatter = buttonInfo.enabled and SI_SCREEN_NARRATION_FIRST_KEYBIND_FORMATTER_NO_LABEL or SI_SCREEN_NARRATION_DISABLED_FIRST_KEYBIND_FORMATTER_NO_LABEL
                else
                    formatter = buttonInfo.enabled and SI_SCREEN_NARRATION_KEYBIND_FORMATTER_NO_LABEL or SI_SCREEN_NARRATION_DISABLED_KEYBIND_FORMATTER_NO_LABEL
                end
                narrationText = zo_strformat(formatter, buttonInfo.keybindName)
            end
            keybindNarration:AddNarrationText(narrationText)
        end
        return keybindNarration
    end

    function ZO_ScreenNarrationManager:GetKeybindNarrationFromData(name, keybind, enabled)
        local keybindNarration = self:CreateNarratableObject(nil, KEYBIND_PAUSE_TIME_MS)
        local keybindName = ZO_Keybindings_GetHighestPriorityNarrationStringFromAction(keybind) or GetString(SI_ACTION_IS_NOT_BOUND)
        local narrationText
        if name then
            local formatter = enabled and SI_SCREEN_NARRATION_KEYBIND_FORMATTER or SI_SCREEN_NARRATION_DISABLED_KEYBIND_FORMATTER
            narrationText = zo_strformat(formatter, keybindName, name)
        else
            local formatter = enabled and SI_SCREEN_NARRATION_KEYBIND_FORMATTER_NO_LABEL or SI_SCREEN_NARRATION_DISABLED_KEYBIND_FORMATTER_NO_LABEL
            narrationText = zo_strformat(formatter, keybindName)
        end
        keybindNarration:AddNarrationText(narrationText)
        return keybindNarration
    end
end

--Recursively populates a narratable object representing a tooltip read in the order it is laid out visually
function ZO_ScreenNarrationManager:FormatTooltipStrings(narration, tooltipString)
    --If this is just a string, we can just add it to the narration as-is
    if type(tooltipString) == "string" then
        narration:AddNarrationText(tooltipString)
    elseif type(tooltipString) == "table" then
        --If this is a table, loop through each entry and add it to the narration
        for i, tooltipEntry in ipairs(tooltipString) do
            self:FormatTooltipStrings(narration, tooltipEntry)
        end
    elseif type(tooltipString) == "function" then
        --If this is a function, attempt to add the return value to the narration
        self:FormatTooltipStrings(narration, tooltipString())
    end
end

--Gets a table of narratable objects representing the narration text for a tooltip or tooltips
--If optionalTooltipType is not specified, the table returned will contain a narratable object for each visible tooltip
function ZO_ScreenNarrationManager:GetTooltipNarration(optionalTooltipType)
    --Get the table of tooltip strings
    --The table is ordered based on the order the strings should be read visually
    local tooltipStrings = GAMEPAD_TOOLTIPS:GetNarrationText(optionalTooltipType)

    --If an optional tooltip type was not specified, then tooltipStrings will be a table of tables for each visible tooltip
    --If an optional tooltip type was specified, then tooltipStrings will only contain the narration for that tooltip
    --In this situation, we loop and create a narratable object for each individual tooltip
    local narrations = {}
    for i, tooltipNarrationText in ipairs(tooltipStrings) do
        if ZO_IsNarratableObject(tooltipNarrationText) then
            table.insert(narrations, tooltipNarrationText)
        else
            local narration = self:CreateNarratableObject()
            self:FormatTooltipStrings(narration, tooltipNarrationText)
            table.insert(narrations, narration)
        end
    end
    return narrations
end

function ZO_ScreenNarrationManager:NarrateParametricListEntry(narrationInfo, list, entryData, entryControl, narrateHeader, narrationType)
    local narrations = {}
    local additionalInputNarrationFunction

    if narrateHeader and narrationInfo.headerNarrationFunction then
        local headerNarration = narrationInfo:headerNarrationFunction()
        ZO_AppendNarration(narrations, headerNarration)
    end

    if list:IsEmpty() then
        --If the list is empty, then narrate the no item text instead
        ZO_AppendNarration(narrations, self:CreateNarratableObject(list:GetNoItemText()))
    elseif entryData and (entryData.text or entryData.narrationText) then
        additionalInputNarrationFunction = entryData.additionalInputNarrationFunction

        if entryData.headerNarrationFunction then
            ZO_AppendNarration(narrations, entryData.headerNarrationFunction(entryData, entryControl))
        elseif entryData.header then
            ZO_AppendNarration(narrations, self:CreateNarratableObject(entryData.header))
        end

        if entryData.narrationText then
            if type(entryData.narrationText) == "function" then
                --If the narration text is a function it should return either a narratable object or table of narratable objects
                local narration = entryData.narrationText(entryData, entryControl)
                ZO_AppendNarration(narrations, narration)
            else
                ZO_AppendNarration(narrations, self:CreateNarratableObject(entryData.narrationText))
            end
        else
            --Entries using a custom value via entryData.narrationText need to handle entryData.text, sublabels, stack count, status indicators, and progress bars themselves if they want it
            ZO_AppendNarration(narrations, ZO_GetSharedGamepadEntryDefaultNarrationText(entryData, entryControl))
        end

        ZO_AppendNarration(narrations, self:GetTooltipNarration())
    end

    ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction))

    if narrationInfo.footerNarrationFunction then
        local footerNarration = narrationInfo:footerNarrationFunction()
        ZO_AppendNarration(narrations, footerNarration)
    end

    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateGridListEntry(gridList, narrateHeader, narrateSubHeader, narrationType)
    local narrations = {}
    if narrateHeader then
        ZO_AppendNarration(narrations, gridList:GetHeaderNarration())
    end

    local entryData = gridList:GetSelectedData()
    if entryData then
        if narrateSubHeader then
            --The subheader information can be stored in either the gridHeaderName or gridHeaderData fields
            local entryHeaderData = entryData.gridHeaderName or entryData.gridHeaderData
            if entryHeaderData then
                if type(entryHeaderData) == "string" then
                    ZO_AppendNarration(narrations, self:CreateNarratableObject(entryHeaderData))
                elseif entryHeaderData.headerNarrationFunction then
                    ZO_AppendNarration(narrations, entryHeaderData.headerNarrationFunction(entryHeaderData))
                end
            end
        end

        if entryData.narrationText then
            if type(entryData.narrationText) == "function" then
                --If the narration text is a function it should return either a narratable object or table of narratable objects
                local narration = entryData.narrationText(entryData)
                ZO_AppendNarration(narrations, narration)
            else
                ZO_AppendNarration(narrations, self:CreateNarratableObject(entryData.narrationText))
            end
        end
    end

    ZO_AppendNarration(narrations, self:GetTooltipNarration())
    ZO_AppendNarration(narrations, self:GetKeybindNarration())

    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateGamepadGridEntry(gamepadGrid, narrateHeader, narrationType)
    local narrations = {}
    if narrateHeader then
        local headerNarration = gamepadGrid:GetHeaderNarration()
        ZO_AppendNarration(narrations, headerNarration)
    end

    local narration = gamepadGrid:GetNarrationText()
    ZO_AppendNarration(narrations, narration)
    ZO_AppendNarration(narrations, self:GetTooltipNarration())
    ZO_AppendNarration(narrations, self:GetKeybindNarration())

    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateSortFilterListEntry(list, narrateHeader, narrationType)
    local narrations = {}
    if narrateHeader then
        local headerNarration = list:GetHeaderNarration()
        ZO_AppendNarration(narrations, headerNarration)
    end

    ZO_AppendNarration(narrations, list:GetNarrationText())
    ZO_AppendNarration(narrations, self:GetTooltipNarration())

    local additionalInputNarrationFunction = list:GetAdditionalInputNarrationFunction()
    ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction))
    ZO_AppendNarration(narrations, list:GetFooterNarration())
    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateSelectedSortHeader(list, sortHeaderGroup, key, narrateHeader, narrationType)
    local narrations = {}
    if narrateHeader then
        local headerNarration = list:GetHeaderNarration()
        ZO_AppendNarration(narrations, headerNarration)
    end
    ZO_AppendNarration(narrations, sortHeaderGroup:GetSortHeaderNarrationText(key))
    ZO_AppendNarration(narrations, list:GetEmptyRowNarration())
    ZO_AppendNarration(narrations, self:GetKeybindNarration())
    ZO_AppendNarration(narrations, list:GetFooterNarration())
    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateComboBox(comboBox, narrationType)
    local narrations = {}
    ZO_AppendNarration(narrations, comboBox:GetNarrationText())

    local tooltipType = comboBox:GetNarrationTooltipType()
    -- Only narrate tooltips if a particular tooltip as been specified for the comboBox
    if tooltipType then
        ZO_AppendNarration(narrations, self:GetTooltipNarration(tooltipType))
    end

    ZO_AppendNarration(narrations, self:GetKeybindNarration())
    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateTextSearchHeader(textSearchHeader, narrationInfo, narrateHeader, narrationType)
    local narrations = {}

    --If we have a narration for the header, include if applicable
    if narrateHeader and narrationInfo.headerNarrationFunction then
        local headerNarration = narrationInfo:headerNarrationFunction()
        ZO_AppendNarration(narrations, headerNarration)
    end

    --Get the main narration for the text search header
    ZO_AppendNarration(narrations, textSearchHeader:GetNarrationText())

    --If we have a narration for the results, include that
    if narrationInfo.resultsNarrationFunction then
        local resultsNarration = narrationInfo.resultsNarrationFunction()
        ZO_AppendNarration(narrations, resultsNarration)
    end

    ZO_AppendNarration(narrations, self:GetKeybindNarration())
    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateFocusEntry(focus, narrateHeader, narrateSubHeader, narrationType)
    if focus then
        local narrations = {}
        if narrateHeader then
            local headerNarration = focus:GetHeaderNarration()
            ZO_AppendNarration(narrations, headerNarration)
        end

        if narrateSubHeader then
            local subHeaderNarration = focus:GetSubHeaderNarration()
            ZO_AppendNarration(narrations, subHeaderNarration)
        end

        local focusNarration = focus:GetNarrationText()
        ZO_AppendNarration(narrations, focusNarration)

        local additionalInputNarrationFunction = focus:GetAdditionalInputNarrationFunction()
        ZO_AppendNarration(narrations, self:GetTooltipNarration())
        ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction))

        local footerNarration = focus:GetFooterNarration()
        ZO_AppendNarration(narrations, footerNarration)

        self:NarrateText(narrations, narrationType)
    end
end

function ZO_ScreenNarrationManager:NarrateSpinner(spinner, narrateHeader, narrationType)
    if spinner then
        local narrations = {}
        if narrateHeader then
            local headerNarration = spinner:GetHeaderNarration()
            ZO_AppendNarration(narrations, headerNarration)
        end

        local spinnerNarration = spinner:GetNarrationText()
        ZO_AppendNarration(narrations, spinnerNarration)

        if spinner:CanNarrateTooltips() then
            ZO_AppendNarration(narrations, self:GetTooltipNarration())
        end
        local additionalInputNarrationFunction = spinner:GetAdditionalInputNarrationFunction()
        ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction))

        self:NarrateText(narrations, narrationType)
    end
end

function ZO_ScreenNarrationManager:NarrateGamepadButtonTabBar(tabBar, narrationType)
    if tabBar then
        local narrations = {}
        local tabBarNarration = tabBar:GetNarrationText()
        ZO_AppendNarration(narrations, tabBarNarration)
        ZO_AppendNarration(narrations, self:GetTooltipNarration())
        ZO_AppendNarration(narrations, self:GetKeybindNarration())

        self:NarrateText(narrations, narrationType)
    end
end

--Forms a table of the narrations that are common amongst all the dialog types
function ZO_ScreenNarrationManager:GetDialogBaseNarration(dialog, dialogInfo, dialogType, textParams, narrateBaseText)
    local narrations = {}
    --narrateBaseText should only be true upon first opening the dialog
    if narrateBaseText then
        if dialogInfo.headerNarrationFunction then
            ZO_AppendNarration(narrations, dialogInfo.headerNarrationFunction(dialog))
        elseif dialog.header and dialog.headerData then
            ZO_AppendNarration(narrations, ZO_GamepadGenericHeader_GetNarrationText(dialog.header, dialog.headerData))
        elseif dialogInfo.title then
            local titleText = ZO_GetFormattedDialogText(dialog, dialogInfo.title, textParams.titleParams)
            ZO_AppendNarration(narrations, self:CreateNarratableObject(titleText))
        end

        if dialogInfo.mainText then
            local mainTextParams = textParams.mainTextNarrationParams or textParams.mainTextParams
            local mainText = ZO_GetFormattedDialogText(dialog, dialogInfo.mainText, mainTextParams)
            ZO_AppendNarration(narrations, self:CreateNarratableObject(mainText))
        end

        --Centered dialogs do not support sub text or warning
        if dialogType ~= GAMEPAD_DIALOGS.CENTERED then
            if dialogInfo.subText then
                local subText = ZO_GetFormattedDialogText(dialog, dialogInfo.subText, textParams.subTextParams)
                ZO_AppendNarration(narrations, self:CreateNarratableObject(subText))
            end

            if dialogInfo.warning then
                local warningText = ZO_GetFormattedDialogText(dialog, dialogInfo.warning, textParams.warningParams)
                ZO_AppendNarration(narrations, self:CreateNarratableObject(warningText))
            end
        end

        if dialogInfo.baseNarrationTooltip then
            ZO_AppendNarration(narrations, self:GetTooltipNarration(dialogInfo.baseNarrationTooltip))
        end
    end

    return narrations
end

function ZO_ScreenNarrationManager:NarrateDialog(dialog, narrateBaseText, narrationType)
    local dialogInfo = dialog.info
    if dialogInfo.gamepadInfo then
        local dialogType = dialogInfo.gamepadInfo.dialogType
        local textParams = dialog.textParams or {}
        local additionalInputNarrationFunction

        --First, get the base narration
        local narrations = self:GetDialogBaseNarration(dialog, dialogInfo, dialogType, textParams, narrateBaseText)
        if dialogType == GAMEPAD_DIALOGS.BASIC then
            --Do nothing, this dialog type is handled fully by the base narration
        elseif dialogType == GAMEPAD_DIALOGS.PARAMETRIC then
            --If the dialog type is parametric, attempt to narrate the currently selected entry in the list
            local list = dialog.entryList
            if list then
                local entryData = list:GetTargetData()
                local entryControl = list:GetTargetControl()
                if entryData and (entryData.text or entryData.narrationText) then
                    additionalInputNarrationFunction = entryData.additionalInputNarrationFunction

                    if entryData.header then
                        ZO_AppendNarration(narrations, self:CreateNarratableObject(entryData.header))
                    end

                    if entryData.narrationText then
                        if type(entryData.narrationText) == "function" then
                            local narration = entryData.narrationText(entryData, entryControl)
                            ZO_AppendNarration(narrations, narration)
                        else
                            ZO_AppendNarration(narrations, self:CreateNarratableObject(entryData.narrationText))
                        end
                    else
                        --Entries using a custom value via entryData.narrationText need to handle entryData.text, sublabels, stack count, status indicators, and progress bars themselves if they want it
                        ZO_AppendNarration(narrations, ZO_GetSharedGamepadEntryDefaultNarrationText(entryData, entryControl))
                    end

                    --Dialogs only narrate tooltips when one is specifically specified
                    if entryData.narrationTooltip then
                        ZO_AppendNarration(narrations, self:GetTooltipNarration(entryData.narrationTooltip))
                    end
                end
            end
        elseif dialogType == GAMEPAD_DIALOGS.COOLDOWN then
            --If this is a cooldown dialog, attempt to narrate the loading text
            if dialogInfo.loading then
                local loadingText = dialogInfo.loading.text
                if type(loadingText) == "function" then
                    loadingText = loadingText(dialog)
                end
                ZO_AppendNarration(narrations, self:CreateNarratableObject(loadingText))
            end
        elseif dialogType == GAMEPAD_DIALOGS.CENTERED then
            --Do nothing, this dialog type is handled fully by the base narration
        elseif dialogType == GAMEPAD_DIALOGS.STATIC_LIST then
            --If the dialog type is static list, attempt to narrate each entry in the list
            if dialogInfo.itemInfo then
                local itemInfo = dialogInfo.itemInfo
                if type(itemInfo) == "function" then
                    itemInfo = itemInfo(dialog)
                end

                for i, info in ipairs(itemInfo) do
                    ZO_AppendNarration(narrations, self:CreateNarratableObject(info.label))
                end
            end
        elseif dialogType == GAMEPAD_DIALOGS.ITEM_SLIDER then
            --If the dialog type is item slider, attempt to narrate the associated slider
            if dialog.slider then
                local itemName = ""
                local bagId = dialog.data.bagId
                local slotIndex = dialog.data.slotIndex
                if bagId and slotIndex then
                    --Use the name of the item as the name of the slider
                    itemName = GetItemName(bagId, slotIndex)
                end

                --Item sliders can assume numeric directional input unless we are told otherwise
                additionalInputNarrationFunction = dialogInfo.additionalInputNarrationFunction or ZO_GetNumericHorizontalDirectionalInputNarrationData

                if dialogInfo.narrationText then
                    if type(dialogInfo.narrationText) == "function" then
                        --If the narration text is a function it should return either a narratable object or table of narratable objects
                        local narration = dialogInfo.narrationText(dialog, itemName)
                        ZO_AppendNarration(narrations, narration)
                    else
                        ZO_AppendNarration(narrations, self:CreateNarratableObject(dialogInfo.narrationText))
                    end
                else
                    ZO_AppendNarration(narrations, ZO_FormatSliderNarrationText(dialog.slider, itemName))
                end
            end
        elseif dialogType == GAMEPAD_DIALOGS.CUSTOM then
            if dialogInfo.narrationText then
                if type(dialogInfo.narrationText) == "function" then
                    local narration = dialogInfo.narrationText(dialog)
                    ZO_AppendNarration(narrations, narration)
                else
                    ZO_AppendNarration(narrations, self:CreateNarratableObject(dialogInfo.narrationText))
                end
            end
            additionalInputNarrationFunction = dialogInfo.additionalInputNarrationFunction
        else
            internalassert(false, "Unhandled dialog type")
        end

        --Always add keybinds at the end
        ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction))
        self:NarrateText(narrations, narrationType)
    end
end

function ZO_ScreenNarrationManager:NarrateFadingControlBuffer(fadingControlBuffer, narrationType)
    local narrations = {}
    ZO_AppendNarration(narrations, fadingControlBuffer:GetNarrationText())
    self:NarrateText(narrations, narrationType)
end

function ZO_ScreenNarrationManager:NarrateCustomEntry(narrationInfo, narrateHeader, narrationType)
    local narrations = {}

    if narrateHeader and narrationInfo.headerNarrationFunction then
        local headerNarration = narrationInfo:headerNarrationFunction()
        ZO_AppendNarration(narrations, headerNarration)
    end

    if narrationInfo.selectedNarrationFunction then
        local selectedNarration = narrationInfo:selectedNarrationFunction()
        ZO_AppendNarration(narrations, selectedNarration)
    end

    --Default to true if nothing was specified for canNarrateTooltips
    local canNarrateTooltips = narrationInfo.canNarrateTooltips == nil and true or narrationInfo.canNarrateTooltips
    if type(canNarrateTooltips) == "function" then
        canNarrateTooltips = canNarrateTooltips()
    end

    if canNarrateTooltips then
        ZO_AppendNarration(narrations, self:GetTooltipNarration())
    end

    local additionalInputNarrationFunction = narrationInfo.additionalInputNarrationFunction
    local overrideInputNarrationFunction = narrationInfo.overrideInputNarrationFunction
    ZO_AppendNarration(narrations, self:GetKeybindNarration(additionalInputNarrationFunction, overrideInputNarrationFunction))

    if narrationInfo.footerNarrationFunction then
        local footerNarration = narrationInfo:footerNarrationFunction()
        ZO_AppendNarration(narrations, footerNarration)
    end
    self:NarrateText(narrations, narrationType)
end

--TODO XAR: Do we need category here?
function ZO_ScreenNarrationManager:NarrateChatMessage(chatMessage, category)
    if IsChatNarrationEnabled() then
        if chatMessage ~= "" then
            RequestReadTextChatToClient(chatMessage)
        end
    end
end

--Adds a single narratable object to the pending narration and then releases the object
function ZO_ScreenNarrationManager:AddPendingNarration(narratableObject)
    narratableObject:AddToPending()
    narratableObject:ReleaseObject()
end

function ZO_ScreenNarrationManager:NarrateText(narration, narrationType)
    if type(narration) == "table" then
        if ZO_IsNarratableObject(narration) then
            self:AddPendingNarration(narration)
        else
            for _, narratableObject in ipairs(narration) do
                self:AddPendingNarration(narratableObject)
            end
        end
    end

    RequestReadPendingNarrationTextToClient(narrationType)
end

-- Global singleton

-- The global singleton moniker is assigned by the Narration Manager's constructor in order to
-- allow data objects to reference the singleton during their construction.
ZO_ScreenNarrationManager:New()