ZO_PRELOGIN_WORLD_SAVED_VARS = nil

function IsPreloginWorldEnabled()
    if not IsInGamepadPreferredMode() then
        return false
    end

    if IsInternalBuild() then
        if ZO_PRELOGIN_WORLD_SAVED_VARS and ZO_PRELOGIN_WORLD_SAVED_VARS.PreloginWorldEnabled ~= nil then
            return tonumber(ZO_PRELOGIN_WORLD_SAVED_VARS.PreloginWorldEnabled) == 1
        end
    end

    return true
end

-- Establish whether we want to use the Prelogin World immediately
-- as the World Manager needs to know ASAP.
SetUsePreloginWorld(IsPreloginWorldEnabled())

function IsPreloginWorldReady()
    return IsPreloginWorldFullyLoaded() or not IsPreloginWorldEnabled() or not IsInGamepadPreferredMode()
end

function ZO_Pregame_CanSkipVideos()
    return GetCVar("HasPlayedPregameVideo") ~= "0" or ZO_IsConsoleUI()
end

function ZO_Pregame_ShouldSkipVideos()
    -- only skip if the cvar is set, _and_ the player has seen the videos at least once
    return GetCVar("SkipPregameVideos") ~= "0" and ZO_Pregame_CanSkipVideos()
end

ZO_PREGAME_CHARACTER_COUNT = 0
ZO_PREGAME_FIRED_CHARACTER_CONSTRUCTION_READY = false
ZO_PREGAME_CHARACTER_LIST_RECEIVED = false
ZO_PREGAME_HAD_GLOBAL_ERROR = false

local QUEUE_VIDEO = false

local g_currentStateName = nil
local g_currentStateData = nil
local g_previousState = nil

local g_loadingUpdates = false

-- We don't want to show the video or the chapter upsell when we're logging out, only when we're logging in
local shouldTryToPlayOpeningCinematic = false
local shouldTryToShowChapterInterstitial = false

function Pregame_ShowScene(sceneName)
    SCENE_MANAGER:Show(sceneName)
    ZO_Dialogs_ReleaseAllDialogsExcept("HANDLE_ERROR", "HANDLE_ERROR_WITH_HELP")
end

function AttemptQuickLaunch()
    if GetCVar("QuickLaunch") == "1" then
        local acctName = GetCVar("AccountName")
        local acctPwd = GetCVar("AccountPassword")

        if acctName ~= "" and acctPwd ~= "" then
            PregameLogin(acctName, acctPwd)
        end
    end
end

function AttemptToFireCharacterConstructionReady()
    if not ZO_PREGAME_FIRED_CHARACTER_CONSTRUCTION_READY and IsPregameCharacterConstructionReady() and ZO_PREGAME_CHARACTER_LIST_RECEIVED then
        ZO_PREGAME_FIRED_CHARACTER_CONSTRUCTION_READY = true
        CALLBACK_MANAGER:FireCallbacks("OnCharacterConstructionReady")
    end
end

local function PlayIntroCinematicAndAdvance()
    SetVideoCancelAllOnCancelAny(true)
    local videoDataId = GetOpeningCinematicVideoDataId()
    ZO_PlayVideoAndAdvance(PlayVideoById, videoDataId, QUEUE_VIDEO, VIDEO_SKIP_MODE_REQUIRE_CONFIRMATION_FOR_SKIP)
end

function ZO_PlayIntroCinematicAndReturn()
    SetVideoCancelAllOnCancelAny(true)
    local videoDataId = GetOpeningCinematicVideoDataId()
    ZO_PlayVideoAndReturn(PlayVideoById, videoDataId, QUEUE_VIDEO, VIDEO_SKIP_MODE_REQUIRE_CONFIRMATION_FOR_SKIP)
end

local g_sharedPregameStates =
{
    ["CharacterSelect_FromIngame"] =
    {
        OnEnter = function()
            -- Let the character list receipt determine the state to go to.
            RequestCharacterList()
        end,

        OnExit = function()
        end
    },

    ["PlayOpeningCinematic"] =
    {
        ShouldAdvance = function()
            return false
        end,

        OnEnter = function()
            PlayIntroCinematicAndAdvance()
            SCENE_MANAGER:ShowBaseScene()
        end,

        GetStateTransitionData = function()
            return "WaitForGameDataLoaded"
        end,

        OnExit = function()
        end,
    },

    ["WaitForGameDataLoaded"] =
    {
        ShouldAdvance = function()
            return IsSystemLoaded(LOADING_SYSTEM_GAME_DATA)
        end,

        OnEnter = function()
            SuppressWorldList()
            RegisterForLoadingUpdates()
            -- Make sure we aren't showing a scene here if we
            -- didn't show the cinematic before switching to this scene
            SCENE_MANAGER:ShowBaseScene()
        end,

        OnExit = function()
        end,

        GetStateTransitionData = function()
            return "ChapterUpgradeInterstitial"
        end,
    },

    ["CharacterCreateFadeIn"] =
    {
        ShouldAdvance = function()
            return false
        end,

        OnEnter = function()
            ZO_CharacterCreate_FadeIn()
        end,

        GetStateTransitionData = function()
            return "CharacterCreate"
        end,

        OnExit = function()
        end,
    },

    ["CharacterCreate"] =
    {
        OnEnter = function()
            ZO_CHARACTERCREATE_MANAGER:SetCharacterMode(CHARACTER_MODE_CREATION)
            local characterCreate = SYSTEMS:GetObject(ZO_CHARACTER_CREATE_SYSTEM_NAME)
            characterCreate:Reset()
            characterCreate:InitializeForCharacterCreate()

            if IsInGamepadPreferredMode() then
                if DoesPlatformRequirePregamePEGI() and not HasAgreedToPEGI() then
                    ZO_Dialogs_ShowGamepadDialog("PEGI_COUNTRY_SELECT_GAMEPAD")
                else
                    Pregame_ShowScene("gamepadCharacterCreate")
                end
            else
                Pregame_ShowScene("gameMenuCharacterCreate")
                if DoesPlatformRequirePregamePEGI() and not HasAgreedToPEGI() then
                    ZO_Dialogs_ShowDialog("PEGI_COUNTRY_SELECT")
                end
            end
        end,

        OnExit = function()
            ZO_Dialogs_ReleaseDialog("CHARACTER_CREATE_CREATING")
            SetCharacterCameraZoomAmount(-1) -- zoom all the way out when leaving this state
        end
    },

    ["CharacterCreate_Barbershop"] =
    {
        OnEnter = function()
            if IsInGamepadPreferredMode() then
                Pregame_ShowScene("gamepadCharacterCreate")
            else
                Pregame_ShowScene("gameMenuCharacterCreate")
            end
        end,

        OnExit = function()
            ZO_Dialogs_ReleaseDialog("CHARACTER_CREATE_CREATING")
            SetCharacterCameraZoomAmount(-1) -- zoom all the way out when leaving this state
        end
    },

    ["ChapterUpgrade"] =
    {
        ShouldAdvance = function()
            return false
        end,

        OnEnter = function()
            if IsInGamepadPreferredMode() then
                Pregame_ShowScene("chapterUpgradeGamepad")
            else
                Pregame_ShowScene("chapterUpgradeKeyboard")
            end
        end,

        GetStateTransitionData = function()
            return "CharacterSelect"
        end,

        OnExit = function()
        end,
    },

    ["ChapterUpgradeInterstitial"] =
    {
        ShouldAdvance = function()
            if not shouldTryToShowChapterInterstitial then
                return true
            end

            return not CHAPTER_UPGRADE_MANAGER:ShouldShow()
        end,

        OnEnter = function()
            if IsInGamepadPreferredMode() then
                Pregame_ShowScene("chapterUpgradeGamepad")
            else
                Pregame_ShowScene("chapterUpgradeKeyboard")
            end
        end,

        GetStateTransitionData = function()
            return "WaitForCharacterDataLoaded"
        end,

        OnExit = function()
        end,
    },

    ["WaitForCharacterDataLoaded"] =
    {
        ShouldAdvance = function()
            return IsPregameCharacterConstructionReady()
        end,

        OnEnter = function()
        end,

        GetStateTransitionData = function()
            if ZO_PREGAME_CHARACTER_COUNT > 0 then
                return "CharacterSelect"
            else
                return "CharacterCreateFadeIn"
            end
        end,

        OnExit = function()
        end,
    },

    ["BeginLoadingIntoWorld"] =
    {
        OnEnter = function()
            if IsInGamepadPreferredMode() then
                ZO_CharacterSelect_Gamepad_ShowLoginScreen()
            else
                SCENE_MANAGER:ShowBaseScene()
                ZO_Dialogs_ShowDialog("REQUESTING_CHARACTER_LOAD")
            end
        end,

        OnExit = function()
        end
    },

    ["PlayIntroMovies"] =
    {
        ShouldAdvance = function()
            return ZO_Pregame_ShouldSkipVideos()
        end,

        OnEnter = function()
            -- If you haven't played the videos, you can't skip them until they finish...
            local skipMode = ZO_Pregame_CanSkipVideos() and VIDEO_SKIP_MODE_ALLOW_SKIP or VIDEO_SKIP_MODE_NO_SKIP
            ZO_PlayVideoAndAdvance(PlayVideo, "Video/Bethesda_logo.bk2", QUEUE_VIDEO, skipMode)
        end,

        GetStateTransitionData = function()
            return "ShowZOSVideo"
        end,

        OnExit = function()
        end,
    },

    ["ShowZOSVideo"] =
    {
        ShouldAdvance = function()
            return ZO_Pregame_ShouldSkipVideos()
        end,

        OnEnter = function()
            -- If you haven't played the videos, you can't skip them until they finish...
            local skipMode = ZO_Pregame_CanSkipVideos() and VIDEO_SKIP_MODE_ALLOW_SKIP or VIDEO_SKIP_MODE_NO_SKIP
            ZO_PlayVideoAndAdvance(PlayVideo, "Video/ZOS_logo.bk2", QUEUE_VIDEO, skipMode)
        end,

        GetStateTransitionData = function()
            return "ShowHavokSplashScreen"
        end,

        OnExit = function()
        end,
    },

    ["ShowHavokSplashScreen"] =
    {
        ShouldAdvance = function()
            return ZO_Pregame_ShouldSkipVideos()
        end,

        OnEnter = function()
            SimpleLogoSplash_ShowWithTexture("EsoUI/Art/Login/havok_logo.dds")
        end,

        GetStateTransitionData = function()
            return "ShowDMMVideo"
        end,

        OnExit = function()
        end,
    },

    ["ShowDMMVideo"] =
    {
        ShouldAdvance = function()
            local serviceType = GetPlatformServiceType()
            return serviceType ~= PLATFORM_SERVICE_TYPE_DMM or ZO_Pregame_ShouldSkipVideos()
        end,

        OnEnter = function()
            local skipMode = ZO_Pregame_CanSkipVideos() and VIDEO_SKIP_MODE_ALLOW_SKIP or VIDEO_SKIP_MODE_NO_SKIP
            ZO_PlayVideoAndAdvance(PlayVideo, "Video/jp_DMM_logo.bk2", QUEUE_VIDEO, skipMode)
        end,

        GetStateTransitionData = function()
            return "ShowLegalSplashScreen"
        end,

        OnExit = function()
        end,
    },

    ["ShowLegalSplashScreen"] =
    {
        ShouldAdvance = function()
            return ZO_Pregame_ShouldSkipVideos()
        end,

        OnEnter = function()
            SCENE_MANAGER:Show("copyrightLogosSplash")
        end,

        GetStateTransitionData = function()
            if IsConsoleUI() then
                return "AccountLoginEntryPoint"
            else
                return "ShowAccessibilityModePrompt"
            end
        end,

        OnExit = function()
        end,
    },

    ["ShowAccessibilityModePrompt"] =
    {
        ShouldAdvance = function()
            return IsInGamepadPreferredMode() or GetCVar("PregameAccessibilityPromptEnabled") ~= "1"
        end,

        OnEnter = function()
            SCENE_MANAGER:Show("accessibilityModePrompt")
        end,

        OnExit = function()
            -- Do nothing
        end,

        GetStateTransitionData = function()
            return "FirstTimeAccessibilitySettings"
        end,
    },

    ["FirstTimeAccessibilitySettings"] =
    {
        ShouldAdvance = function()
            local accessibilityModeEnabled = IsConsoleUI() or GetSetting_Bool(SETTING_TYPE_ACCESSIBILITY, ACCESSIBILITY_SETTING_ACCESSIBILITY_MODE)
            return not accessibilityModeEnabled or GetCVar("PregameAccessibilitySettingMenuEnabled") ~= "1"
        end,

        OnEnter = function()
            GAMEPAD_OPTIONS:SetCategory(SETTING_PANEL_ACCESSIBILITY)
            SCENE_MANAGER:Show(GAMEPAD_OPTIONS_PANEL_SCENE:GetName())
            local function BackButtonCallback()
                SetCVar("PregameAccessibilitySettingMenuEnabled", "false")
                PregameStateManager_AdvanceState()
            end
            GAMEPAD_OPTIONS:ReplaceBackKeybind(BackButtonCallback, GetString(SI_ACCESSIBILITY_SETTINGS_CONTINUE_KEYBIND))
        end,

        OnExit = function()
            GAMEPAD_OPTIONS:RevertBackKeybind()
        end,

        GetStateTransitionData = function()
            return "ScreenAdjustIntro"
        end,
    },

    ["ScreenAdjustIntro"] =
    {
        ShouldAdvance = function()
            return not IsConsoleUI() or GetCVar("PregameScreenAdjustEnabled") ~= "1"
        end,

        OnEnter = function()
            SCENE_MANAGER:Show("screenAdjust")
            SetCVar("PregameScreenAdjustEnabled", "false")
        end,

        OnExit = function()
        end,

        GetStateTransitionData = function()
            return "GammaAdjust"
        end,
    },

    ["GammaAdjust"] =
    {
        ShouldAdvance = function()
            return not ZO_GammaAdjust_NeedsFirstSetup()
        end,

        OnEnter = function()
            SCENE_MANAGER:Show("gammaAdjust")
        end,

        OnExit = function()
            SetCVar("PregameGammaCheckEnabled", "false")
        end,

        GetStateTransitionData = function()
            if IsConsoleUI() or not DoesPlatformSelectServer() then
                return "ShowEULA"
            else
                return "AccountLoginEntryPoint"
            end
        end,
    },

    ["Disconnect"] =
    {
        OnEnter = function()
            SetCVar("QuickLaunch", "0")
            PregameDisconnect()
        end,

        OnExit = function ()
        end,
    }
}

local g_keyboardPregameStates = {}
function PregameStateManager_AddKeyboardStates(externalStates)
    for key, value in pairs(externalStates) do
        g_keyboardPregameStates[key] = value
    end
end

local g_gamepadPregameStates = {}
function PregameStateManager_AddGamepadStates(externalStates)
    for key, value in pairs(externalStates) do
        g_gamepadPregameStates[key] = value
    end
end

function PregameStateManager_GetState(stateName)
    local state
    if IsInGamepadPreferredMode() then
        state = g_gamepadPregameStates[stateName]
    else
        state = g_keyboardPregameStates[stateName]
    end

    if not state then
        state = g_sharedPregameStates[stateName]
    end

    return state
end

function PregameStateManager_SetState(stateName, ...)
    local newPregameState = PregameStateManager_GetState(stateName)
    internalassert(newPregameState, "missing state for " .. stateName)
    local stateArgs = { ... }

    -- Because GetTransitionData returns the next state as the first argument, insert state name into this table.
    -- The actual arguments passed to OnEnter will be adjusted to account for it.
    table.insert(stateArgs, 1, stateName)

    if g_currentStateData then
        g_currentStateData.OnExit()
    end

    g_previousState = g_currentStateName

    local foundState = false
    while not foundState do
        g_currentStateName = stateName
        local shouldAdvance = (newPregameState.ShouldAdvance == nil) or newPregameState.ShouldAdvance()
        if shouldAdvance then
            if newPregameState and newPregameState.GetStateTransitionData then
                stateArgs = { newPregameState.GetStateTransitionData() }
                stateName = stateArgs[1]
                newPregameState = PregameStateManager_GetState(stateName)
                internalassert(newPregameState, "missing state for " .. stateName)
            else
                foundState = true
            end
        else
            foundState = true
        end
    end

    if not PregameStateManager_CanHideGui() and GetGuiHidden("pregame") then
        if ToggleShowPregameGui then
            ToggleShowPregameGui()
        end
    end

    WriteToInterfaceLog(string.format("PregameStateManager_SetState - from: %s, to: %s", tostring(g_previousState), tostring(g_currentStateName)))

    g_currentStateData = newPregameState
    newPregameState.OnEnter(select(2, unpack(stateArgs)))
    CALLBACK_MANAGER:FireCallbacks("OnPregameEnterState", g_currentStateName)
end

function PregameStateManager_ReenterLoginState()
    PregameStateManager_SetState("WaitForPreloginWorld")
end

function PregameStateManager_AdvanceState()
    local currentStateData = PregameStateManager_GetState(g_currentStateName)
    if currentStateData and currentStateData.GetStateTransitionData then
        PregameStateManager_SetState(currentStateData.GetStateTransitionData())
    else
        -- If there are no transition data, then we're not going anywhere...we'll be locked in the current state.
        -- Do not call this if you're not on a state with transition data
        internalassert(false, string.format("Non-advancable state: %s", tostring(g_currentStateName)))
    end
end

-- this will only advance the state if we are currently in the state passed in
function PregameStateManager_AdvanceStateFromState(state)
    if g_currentStateName == state then
        PregameStateManager_AdvanceState()
    end
end

function PregameStateManager_GetCurrentState()
    return g_currentStateName
end

function PregameStateManager_GetPreviousState()
    return g_previousState
end

local function OnCharacterListReceived(_, characterCount, maxCharacters, mostRecentlyPlayedCharacterId)
    ZO_PREGAME_CHARACTER_LIST_RECEIVED = true
    ZO_PREGAME_CHARACTER_COUNT = characterCount

    local isPlayingVideo = false

    if shouldTryToPlayOpeningCinematic then
        local openingCinematicSeen = GetCVar("OpeningCinematicSeen") == "1"

        if not openingCinematicSeen then
            SetCVar("OpeningCinematicSeen", 1)
            ZO_SavePlayerConsoleProfile()
            -- Play intro movie
            PregameStateManager_SetState("PlayOpeningCinematic")
            isPlayingVideo = true
        end
    end

    if not isPlayingVideo then
        -- Go to character create/select as necessary after we have our data
        -- If we are already at CharacterSelect when we get the character list, then we don't need to move
        -- This could happen when we rename or delete a character
        if PregameStateManager_GetCurrentState() ~= "CharacterSelect" then
            PregameStateManager_SetState("WaitForGameDataLoaded")
            -- If the data isn't fully loaded make sure we're registered for loading updates for things
            -- that depend on the "PregameFullyLoaded" callback.
            -- This can happen when we are returning to character select after being disconnected from the server
            if not PregameIsFullyLoaded() then
                RegisterForLoadingUpdates()
            end
        elseif characterCount == 0 then
            -- However, if we delete our last character then we need to switch to CharacterCreate
            -- so we can create a new character. We also want to avoid CharacterCreateFadeIn since
            -- that won't transition very nicely between CharacterSelect and CharacterCreate
            -- We are also assuming here that we already have character data since we were at character select
            PregameStateManager_SetState("CharacterCreate")
        end
    end

    -- if this hasn't been fired yet, then fire it (could have been a reload or coming from in-game)
    AttemptToFireCharacterConstructionReady()

    if DoesPlatformSupportDisablingShareFeatures() then
        -- re-enabled when the character list is loaded
        EnableShareFeatures()
    end
end

-- Debugging utility...you must be at character select already to use this.
local function SetupUIReloadAfterLogin()
    RequestCharacterList()

    return "CharacterSelect"
    -- Return an invalid state, allow the character list receipt to figure out what state to advance to
end

local initialStateOverrideFn --= SetupUIReloadAfterLogin -- normally this is nil, it can be set to a custom function to allow the reload to drop into a desired state

function UnregisterForLoadingUpdates()
    if g_loadingUpdates then
        EVENT_MANAGER:UnregisterForEvent("PregameStateManager", EVENT_AREA_LOAD_STARTED)
        EVENT_MANAGER:UnregisterForEvent("PregameStateManager", EVENT_SUBSYSTEM_LOAD_STATE_CHANGED)
        EVENT_MANAGER:UnregisterForEvent("PregameStateManager", EVENT_LUA_ERROR)
        g_loadingUpdates = false
    end
end

local function OnAreaLoadStarted()
    ZO_Dialogs_ReleaseAllDialogs(true)
end

function IsPlayingChapterOpeningCinematic()
    return PregameStateManager_GetCurrentState() == "PlayOpeningCinematic"
end

function IsInCharacterSelectCinematicState()
    return PregameStateManager_GetCurrentState() == "CharacterSelect_PlayCinematic"
end

function IsInCharacterCreateState()
    return PregameStateManager_GetCurrentState() == "CharacterCreate"
end

local function OnCharacterSelected(_, characterId)
    PregameStateManager_SetState("BeginLoadingIntoWorld")
end

function PregameIsFullyLoaded()
    return GetNumLoadedSubsystems() == GetNumTotalSubsystemsToLoad()
end

function AttemptToAdvancePastChapterOpeningCinematic()
    if PregameIsFullyLoaded() then
        PregameStateManager_AdvanceStateFromState("PlayOpeningCinematic")
    end
end

function AttemptToAdvancePastCharacterSelectCinematic()
    if PregameIsFullyLoaded() then
        PregameStateManager_AdvanceStateFromState("CharacterSelect_PlayCinematic")
    end
end

local function OnSubsystemLoadStateChanged(_, subSystem, isComplete)
    if not isComplete then
        return
    end

    if subSystem == LOADING_SYSTEM_GAME_DATA or subSystem == LOADING_SYSTEM_SHARED_CHARACTER_OBJECT then
        AttemptToFireCharacterConstructionReady()
        -- LOADING_SYSTEM_GAME_DATA loads before LOADING_SYSTEM_SHARED_CHARACTER_OBJECT so if we hit either
        -- of those then the game data is loaded
        if subSystem == LOADING_SYSTEM_GAME_DATA then
            PregameStateManager_AdvanceStateFromState("WaitForGameDataLoaded")
        elseif subSystem == LOADING_SYSTEM_SHARED_CHARACTER_OBJECT then
            PregameStateManager_AdvanceStateFromState("WaitForCharacterDataLoaded")
        end
    end

    if PregameIsFullyLoaded() then
        if IsPlayingChapterOpeningCinematic() then
            AttemptToAdvancePastChapterOpeningCinematic()
        end

        CALLBACK_MANAGER:FireCallbacks("PregameFullyLoaded")
        UnregisterForLoadingUpdates()
    end
end

local function OnLuaErrorWhileLoading(_)
    -- Errors triggered during a loading screen will prevent some loading
    -- subsystems from completing, so the loading screen will never go away unless
    -- we do something. Let's immediately disconnect and bail back to the initial
    -- screen to handle this. We're calling lua code to handle errors in lua code,
    -- so we may still end up in the situation where our error recovery code never
    -- gets called, but the list of places where that could happen are very low;
    -- basically just this function.
    EVENT_MANAGER:UnregisterForEvent("PregameStateManager", EVENT_LUA_ERROR)
    ZO_PREGAME_HAD_GLOBAL_ERROR = true
    PregameDisconnectOnLuaError()
end

function RegisterForLoadingUpdates()
    if not g_loadingUpdates then
        EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_AREA_LOAD_STARTED, OnAreaLoadStarted)
        EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_SUBSYSTEM_LOAD_STATE_CHANGED, OnSubsystemLoadStateChanged)
        EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_LUA_ERROR, OnLuaErrorWhileLoading)
        g_loadingUpdates = true
    end
end

local function OnShowPregameGuiInState(_, desiredState)
    SetGuiHidden("pregame", false)

    if initialStateOverrideFn then
        desiredState = initialStateOverrideFn()
    end

    if desiredState and desiredState ~= "" then
        PregameStateManager_SetState(desiredState, true)
    end
end

function PregameStateManager_PlayCharacter(charId, loadOption)
    if type(loadOption) == "string" then
        PregameStateManager_SetState(loadOption)
    else --We will need to revisit this once the tutorial gate is integrated into the build
        CALLBACK_MANAGER:FireCallbacks("OnCharacterLoadRequested")
        SelectCharacterForPlay(charId, loadOption)
    end
end

do
    local g_currentVideoPregameState = nil
    local function OnVideoPlaybackComplete()
        EVENT_MANAGER:UnregisterForEvent("ZO_PlayVideoAndAdvance", EVENT_VIDEO_PLAYBACK_COMPLETE)
        EVENT_MANAGER:UnregisterForEvent("ZO_PlayVideoAndAdvance", EVENT_VIDEO_PLAYBACK_ERROR)

        if not ZO_PREGAME_HAD_GLOBAL_ERROR then
            PregameStateManager_AdvanceStateFromState(g_currentVideoPregameState)
        end
    end

    function ZO_PlayVideoAndAdvance(playVideoFunction, ...)
        g_currentVideoPregameState = PregameStateManager_GetCurrentState()
        EVENT_MANAGER:RegisterForEvent("ZO_PlayVideoAndAdvance", EVENT_VIDEO_PLAYBACK_COMPLETE, OnVideoPlaybackComplete)
        EVENT_MANAGER:RegisterForEvent("ZO_PlayVideoAndAdvance", EVENT_VIDEO_PLAYBACK_ERROR, OnVideoPlaybackComplete)
        playVideoFunction(...)
    end
end

do
    local g_wasVideoStartedInGamepadPreferredMode = false
    local function OnVideoPlaybackComplete()
        EVENT_MANAGER:UnregisterForEvent("ZO_PlayVideoAndReturn", EVENT_VIDEO_PLAYBACK_COMPLETE)
        EVENT_MANAGER:UnregisterForEvent("ZO_PlayVideoAndReturn", EVENT_VIDEO_PLAYBACK_ERROR)

        if g_wasVideoStartedInGamepadPreferredMode ~= IsInGamepadPreferredMode() then
            -- The gamepad preferred mode changed event is supressed during while the video is playing, let's bring it back now
            ZO_Pregame_OnGamepadPreferredModeChanged()
        end
    end

    function ZO_PlayVideoAndReturn(playVideoFunction, ...)
        g_wasVideoStartedInGamepadPreferredMode = IsInGamepadPreferredMode()
        EVENT_MANAGER:RegisterForEvent("ZO_PlayVideoAndReturn", EVENT_VIDEO_PLAYBACK_COMPLETE, OnVideoPlaybackComplete)
        EVENT_MANAGER:RegisterForEvent("ZO_PlayVideoAndReturn", EVENT_VIDEO_PLAYBACK_ERROR, OnVideoPlaybackComplete)
        playVideoFunction(...)
    end
end

function PregameStateManager_ClearError()
    ZO_PREGAME_HAD_GLOBAL_ERROR = false
end

do
    local g_validStatesForHideGui =
    {
        "CharacterSelect",
        "CharacterSelect_FromIngame",
        "CharacterCreate",
        "CharacterCreate_Barbershop",
    }

    function PregameStateManager_CanHideGui()
        local currentState = PregameStateManager_GetCurrentState()
        for _, state in ipairs(g_validStatesForHideGui) do
            if state == currentState then
                return true
            end
        end

        return false
    end
end

local function OnDisplayNameReady()
    shouldTryToPlayOpeningCinematic = true
    shouldTryToShowChapterInterstitial = true
end

function ZO_Pregame_DisplayServerDisconnectedError()
    if not IsErrorQueuedFromIngame() then
        return
    end

    local logoutError, globalErrorCode = GetErrorQueuedFromIngame()

    ZO_PREGAME_HAD_GLOBAL_ERROR = true

    local errorString
    local errorStringFormat

    if logoutError ~= LOGOUT_ERROR_NO_ERROR and logoutError ~= LOGOUT_ERROR_UNKNOWN_ERROR and logoutError ~= LOGOUT_ERROR_TRANSFER_FAILED then
        errorStringFormat = GetString("SI_LOGOUTERROR", logoutError)

        if errorStringFormat ~= ""  then
            errorString = zo_strformat(errorStringFormat, GetGameURL())
        end
    elseif globalErrorCode ~= GLOBAL_ERROR_CODE_NO_ERROR then
        -- if the error code is not in LogoutReason then it is probably in the GlobalErrorCode enum
        errorStringFormat = GetString("SI_GLOBALERRORCODE", globalErrorCode)

        if errorStringFormat ~= ""  then
            errorString = zo_strformat(errorStringFormat, globalErrorCode)
        end
    end

    if errorString == nil or errorString == "" then
        if IsInGamepadPreferredMode() then
            errorString = zo_strformat(SI_UNEXPECTED_ERROR, GetURLTextByType(APPROVED_URL_ESO_HELP))
        else
            errorString = GetString(SI_UNKNOWN_ERROR)
        end
    end

    local shouldReenterLoginState = true
    if logoutError == LOGOUT_ERROR_TRANSFER_FAILED then
        shouldReenterLoginState = false
    end

    if IsInGamepadPreferredMode() then
        if shouldReenterLoginState then
            -- Showing the error here also sets the state to AccountLogin
            PREGAME_INITIAL_SCREEN_GAMEPAD:ShowError(nil, errorString)
        end
    else
        if shouldReenterLoginState then
            PregameStateManager_ReenterLoginState()
        end

        ZO_Dialogs_ShowDialog("HANDLE_ERROR", nil, {mainTextParams = {errorString}})
    end
end

local function OnDisconnectedFromServer()
    ZO_Dialogs_ReleaseAllDialogsExcept("HANDLE_ERROR", "HANDLE_ERROR_WITH_HELP")

    ZO_Pregame_DisplayServerDisconnectedError()

    local NUM_FLASHES_BEFORE_SOLID = 7
    FlashTaskbarWindow("DISCONNECTED", NUM_FLASHES_BEFORE_SOLID)
end

local IS_WORLD_SELECT_STATE = ZO_CreateSetFromArguments("WorldSelect_Requested", "WorldSelect_ShowList", "WorldSelect")

function ZO_Pregame_OnGamepadPreferredModeChanged()
    -- Signal whether we need the Prelogin World.
    local enablePreloginWorld = IsPreloginWorldEnabled()
    SetUsePreloginWorld(enablePreloginWorld)

    local currentState = PregameStateManager_GetCurrentState()
    if currentState == nil then
        -- The initial state has not been set up yet, let's wait for that
        return
    end

    if IsAnyVideoPlaying() then
        -- Allow the video to finish. All states that play a video should be
        -- able to be used in either keyboard or gamepad flows, to properly
        -- shuffle the player on to the next state
        return
    end

    if currentState == "GammaAdjust" then
        -- switching between kb/gamepad will be handled by platform style
        -- TODO: for ingame scenes, we can use the
        -- SetHandleGamepadPreferredModeChangedCallback to preempt any UI mode
        -- switching. ideally we could do the same for pregame scenes, so we could
        -- share code between the two
        return
    end

    local FORCE_CLOSE = true
    ZO_Dialogs_ReleaseAllDialogs(FORCE_CLOSE)

    if currentState == "ShowAccessibilityModePrompt" then
        PregameStateManager_SetState("FirstTimeAccessibilitySettings")
    elseif currentState == "FirstTimeAccessibilitySettings" then
        PregameStateManager_SetState("ShowAccessibilityModePrompt")
    elseif not IsAccountLoggedIn() or IS_WORLD_SELECT_STATE[currentState] then -- While in world select, we're logged in but haven't yet started the character loading process
        PregameStateManager_SetState("AccountLoginEntryPoint")
    elseif not IsPregameCharacterConstructionReady() then
        PregameStateManager_SetState("WaitForCharacterDataLoaded")
    elseif PregameStateManager_GetCurrentState() == "CharacterCreate" or GetNumCharacters() == 0 then
        PregameStateManager_SetState("CharacterCreate")
    else
        local wasLoadingIntoWorld = PregameStateManager_GetCurrentState() == "BeginLoadingIntoWorld"
        MoveCameraToCurrentCharacter()
        PregameStateManager_SetState("CharacterSelect")
        if wasLoadingIntoWorld then
            -- pop up the loading dialog.
            -- this needs to happen as a transition from character select so the input-appropriate version of that scene is visible in the background
            PregameStateManager_SetState("BeginLoadingIntoWorld")
        end
    end
end

function ZO_RegisterForSavedVars(systemName, version, defaults, callback)
    local function OnReady()
        local savedVars = ZO_SavedVars:NewAccountWide("ZO_Pregame_SavedVariables", version, systemName, defaults)
        callback(savedVars)
    end

    local function OnAddonLoaded(_, name)
        if name == "ZO_Pregame" then
            EVENT_MANAGER:UnregisterForEvent(systemName, EVENT_ADD_ON_LOADED)

            if IsAccountLoggedIn() then
                OnReady()
            end
        end
    end

    EVENT_MANAGER:RegisterForEvent(systemName, EVENT_ADD_ON_LOADED, OnAddonLoaded)
    -- Every time we log in, we need a new saved vars for that account
    EVENT_MANAGER:RegisterForEvent(systemName, EVENT_DISPLAY_NAME_READY, OnReady)
end

EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_DISPLAY_NAME_READY, OnDisplayNameReady)
EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_CHARACTER_LIST_RECEIVED, OnCharacterListReceived)
EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_SHOW_PREGAME_GUI_IN_STATE, OnShowPregameGuiInState)
EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_CHARACTER_SELECTED_FOR_PLAY, OnCharacterSelected)
EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_GAMEPAD_PREFERRED_MODE_CHANGED, ZO_Pregame_OnGamepadPreferredModeChanged)
EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_DISCONNECTED_FROM_SERVER, OnDisconnectedFromServer)

--[[#$ internal:   Support for overriding Pre-Login World settings via the Pregame Animated Background Dev Tools
if IsInternalBuild() then
    function OnPreloginWorldSavedVarsUpdated()
        local enablePreloginWorld = IsPreloginWorldEnabled()
        SetUsePreloginWorld(enablePreloginWorld)
        PREGAME_ANIMATED_BACKGROUND_FRAGMENT:Refresh()
    end

    EVENT_MANAGER:RegisterForEvent("PregameStateManager", EVENT_ADD_ON_LOADED, function(_, addOnName)
        if addOnName == "ZO_Pregame" then
            local defaultVars =
            {
                PreloginWorldEnabled = 1,
            }
            ZO_PRELOGIN_WORLD_SAVED_VARS = ZO_SavedVars:NewAccountWide("ZO_Pregame_SavedVariables", 2, "PreloginWorld", defaultVars)
            OnPreloginWorldSavedVarsUpdated()

            EVENT_MANAGER:UnregisterForEvent("PregameStateManager", EVENT_ADD_ON_LOADED)
        end
    end)
end
-- internal:        Support for overriding Pre-Login World settings via the Pregame Animated Background Dev Tools  #$]]