ZO_REMOTE_SCENE_NO_SCENE_IDENTIFIER = "INVALID_SCENE"

ZO_SceneManager_Base = ZO_InitializingCallbackObject:Subclass()

function ZO_SceneManager_Base:Initialize()
    self.scenes = {}
    self.sceneGroups = {}
    self.callWhen = {}
        
    self:SetBaseScene(ZO_Scene:New("empty", self):GetName())

    EVENT_MANAGER:RegisterForEvent("SceneManager", EVENT_FOLLOWER_SCENE_FINISHED_FRAGMENT_TRANSITION, function(eventId, ...) self:OnRemoteSceneFinishedFragmentTransition(...) end)
end

-- scenes

function ZO_SceneManager_Base:Add(scene)
    local name = scene:GetName()
    self.scenes[name] = scene
end

function ZO_SceneManager_Base:GetScene(sceneName)
    return self.scenes[sceneName]
end

-- Parent scene
-- Setting a parent scene will constrain the fragments in this scene manager to only show when the parent scene is showing.

function ZO_SceneManager_Base:GetParentScene()
    return self.parentScene
end

function ZO_SceneManager_Base:SetParentScene(parentScene)
    if parentScene ~= self.parentScene then
        if self.parentScene then
            self.parentScene:UnregisterCallback("StateChange", self.onParentSceneStateChangedCallback)
        end
        if parentScene then
            if not self.onParentSceneStateChangedCallback then
                self.onParentSceneStateChangedCallback = function(oldState, newState)
                    if self.currentScene then
                        local wasShowing = oldState == SCENE_SHOWING or oldState == SCENE_SHOWN
                        local isShowing = newState == SCENE_SHOWING or newState == SCENE_SHOWN
                        if wasShowing ~= isShowing then
                            --The part of ZO_SceneFragment:ComputeIfFragmentShouldShow() that cares about the parent scene only cares about the broad strokes of showing/shown vs. hiding/hidden.
                            self.currentScene:RefreshFragments()
                        end
                    end
                end
            end
            self.parentScene = parentScene
            parentScene:RegisterCallback("StateChange", self.onParentSceneStateChangedCallback)
        end
        if self.currentScene then
            self.currentScene:RefreshFragments()
        end
    end
end

-- scene groups

function ZO_SceneManager_Base:AddSceneGroup(sceneGroupName, sceneGroup)
    self.sceneGroups[sceneGroupName] = sceneGroup
end

function ZO_SceneManager_Base:GetSceneGroup(sceneGroupName)
    return self.sceneGroups[sceneGroupName]
end

function ZO_SceneManager_Base:IsSceneGroupShowing(sceneGroupName)
    local sceneGroup = self:GetSceneGroup(sceneGroupName)
    if sceneGroup then
        return sceneGroup:IsShowing()
    end

    return false
end

-- call whens

function ZO_SceneManager_Base:CallWhen(sceneName, state, func)
    local sceneCallbackList = self.callWhen[sceneName]
    if sceneCallbackList == nil then
        sceneCallbackList = {}
        self.callWhen[sceneName] = sceneCallbackList
    end

    table.insert(sceneCallbackList, {state = state, func = func})
end

function ZO_SceneManager_Base:TriggerCallWhens(sceneName, state)
    local sceneCallbackList = self.callWhen[sceneName]
    if sceneCallbackList then
        local i = 1
        while i <= #sceneCallbackList do
            local callWhenInfo = sceneCallbackList[i]
            if callWhenInfo.state == state then
                callWhenInfo.func()
                table.remove(sceneCallbackList, i)
            else
                i = i + 1
            end
        end
    end
end

-- fragments

function ZO_SceneManager_Base:AddFragment(fragment)
    if self.currentScene then
        local state = self.currentScene:GetState()
        if state == SCENE_SHOWING or state == SCENE_SHOWN then
            self.currentScene:AddTemporaryFragment(fragment)
        end
    end
end

function ZO_SceneManager_Base:RemoveFragment(fragment)
    if self.currentScene then
        local state = self.currentScene:GetState()
        if state ~= SCENE_HIDDEN then
            self.currentScene:RemoveTemporaryFragment(fragment)
        end
    end
end

function ZO_SceneManager_Base:RemoveFragmentImmediately(fragment)
    if fragment:GetHideOnSceneHidden() then
        --The fragment may already be in the hiding state waiting for scene hidden when this happens. So we enable show/hide time updates so it can re-try hiding now that it doesn't have to wait on scene hidden.
        fragment:SetAllowShowHideTimeUpdates(true)
        fragment:SetHideOnSceneHidden(false)
        SCENE_MANAGER:RemoveFragment(fragment)
        fragment:SetHideOnSceneHidden(true)
        fragment:SetAllowShowHideTimeUpdates(false)
    else
        SCENE_MANAGER:RemoveFragment(fragment)
    end
end

function ZO_SceneManager_Base:AddFragmentGroup(fragmentGroup)
    for i, fragment in ipairs(fragmentGroup) do
        self:AddFragment(fragment)
    end
end

function ZO_SceneManager_Base:RemoveFragmentGroup(fragmentGroup)
    for _, fragment in pairs(fragmentGroup) do
        self:RemoveFragment(fragment)
    end
end

-- base scene, next scene, and current scene

function ZO_SceneManager_Base:SetBaseScene(sceneName)
    self.baseScene = self.scenes[sceneName]
end

function ZO_SceneManager_Base:GetBaseScene()
    return self.baseScene
end

function ZO_SceneManager_Base:SetNextScene(nextScene, push, nextSceneClearsSceneStack, numScenesNextScenePops)
    self.nextScene = nextScene
    if nextScene then
        nextScene:SetWasRequestedToShowInGamepadPreferredMode(IsInGamepadPreferredMode())
    end
end

function ZO_SceneManager_Base:GetNextScene()
    return self.nextScene
end

function ZO_SceneManager_Base:ClearNextScene()
    self:SetNextScene()
end

function ZO_SceneManager_Base:SetCurrentScene(currentScene)
    self.currentScene = currentScene
end

function ZO_SceneManager_Base:GetCurrentScene()
    return self.currentScene
end

function ZO_SceneManager_Base:GetCurrentSceneName()
    if self.currentScene then
        return self.currentScene:GetName()
    end

    return nil
end

function ZO_SceneManager_Base:IsCurrentSceneGamepad()
    if self.currentScene then
        return self.currentScene:WasShownInGamepadPreferredMode()
    else
        return false
    end
end

-- Scene logic

function ZO_SceneManager_Base:ShowScene(scene, sequenceNumber)
    if scene:IsRemoteScene() then
        scene:SetSequenceNumber(sequenceNumber)
    end
    scene:SetState(SCENE_SHOWING)
    scene:DetermineIfTransitionIsComplete()
end

function ZO_SceneManager_Base:HideScene(scene, sequenceNumber)
    if scene:IsRemoteScene() then
        scene:SetSequenceNumber(sequenceNumber)
    end
    scene:SetState(SCENE_HIDING)
    scene:DetermineIfTransitionIsComplete()
end

function ZO_SceneManager_Base:ShowBaseScene()
    if self.baseScene then
        if not self.currentScene or (self.currentScene ~= self.baseScene and self.nextScene ~= self.baseScene) then
            self:Show(self.baseScene:GetName())
        end
    end
end

function ZO_SceneManager_Base:Toggle(sceneName)
    if self.currentScene and self.currentScene:GetName() == sceneName and self:IsShowing(sceneName) then
        self:ShowBaseScene()
    else
        self:Show(sceneName)
    end
end

function ZO_SceneManager_Base:IsShowing(sceneName)
    local currentScene = self:GetCurrentScene()
    if currentScene and currentScene:GetName() == sceneName and (currentScene:GetState() == SCENE_SHOWING or currentScene:GetState() == SCENE_SHOWN) then
        return true
    else
        return false
    end
end

function ZO_SceneManager_Base:IsShowingBaseScene()
    if self.baseScene then
        return self:IsShowing(self.baseScene:GetName())
    end
    return false
end

function ZO_SceneManager_Base:IsShowingNext(sceneName)
    if self.nextScene then
        return self.nextScene:GetName() == sceneName
    else
        return false
    end
end

function ZO_SceneManager_Base:IsShowingBaseSceneNext()
    if self.baseScene then
        return self:IsShowingNext(self.baseScene:GetName())
    end
    return false
end

function ZO_SceneManager_Base:OnNextSceneRemovedFromQueue(oldNextScene, newNextScene)
    oldNextScene:OnRemovedFromQueue(newNextScene)
end

function ZO_SceneManager_Base:GetPreviousSceneName()
    if self.previousScene then
        return self.previousScene:GetName()
    end

    return nil
end

function ZO_SceneManager_Base:OnSceneStateChange(scene, oldState, newState)
    if scene == self:GetCurrentScene() then
        if oldState == SCENE_HIDING and newState == SCENE_HIDDEN then
            self:OnSceneStateHidden(scene)
        elseif newState == SCENE_HIDING then
            self:OnSceneStateHiding(scene)
        elseif newState == SCENE_SHOWN then
            self:OnSceneStateShown(scene)
        end
    end
    self:TriggerCallWhens(scene:GetName(), newState)
    self:FireCallbacks("SceneStateChanged", scene, oldState, newState)
end

function ZO_SceneManager_Base:OnSceneStateHiding(scene)
    local lastSceneGroup = scene:GetSceneGroup()
    local nextSceneGroup
    if self.nextScene then
        nextSceneGroup = self.nextScene:GetSceneGroup()
    end

    if lastSceneGroup ~= nextSceneGroup and lastSceneGroup ~= nil then
        lastSceneGroup:SetState(SCENE_GROUP_HIDING)
    end
end

function ZO_SceneManager_Base:OnSceneStateHidden(scene)
    -- To be overridden
end

function ZO_SceneManager_Base:OnSceneStateShown(scene)
    local sceneGroup = scene:GetSceneGroup()
    if sceneGroup then
        sceneGroup:SetState(SCENE_GROUP_SHOWN)
    end
end

function ZO_SceneManager_Base:OnRemoteSceneFinishedFragmentTransition(sceneChangeOrigin, sceneName, sequenceNumber)
    local scene = self:GetScene(sceneName)
    if scene and scene:IsRemoteScene() and (sceneChangeOrigin ~= ZO_REMOTE_SCENE_CHANGE_ORIGIN) then
        scene:OnRemoteSceneFinishedFragmentTransition(sequenceNumber)
    end
end

function ZO_SceneManager_Base:OnPreSceneStateChange(scene, currentState, nextState)
    -- optional override
end

function ZO_SceneManager_Base:HideCurrentScene(bypassHideSceneConfirmationReason)
    if self.currentScene then
        self:Hide(self.currentScene:GetName(), bypassHideSceneConfirmationReason)
    end
end

function ZO_SceneManager_Base:Push(sceneName)
    assert(false) -- This function should be overridden
end

function ZO_SceneManager_Base:SwapCurrentScene(newCurrentSceneName)
    assert(false) -- This function should be overridden
end

function ZO_SceneManager_Base:Show(sceneName)
    assert(false) -- This function should be overridden
end

function ZO_SceneManager_Base:Hide(sceneName)
    assert(false) -- This function should be overridden
end

function ZO_SceneManager_Base:RequestShowLeaderBaseScene()
    assert(false) -- This function should be overridden
end

function ZO_SceneManager_Base:SendFragmentCompleteMessage()
    assert(false) -- This function should be overridden
end

-- We don't have a scene stack in ZO_SceneManager_Base, but
-- there are still some scenes/fragments using these functions,
-- so we'll maintain these functions as shared for now
function ZO_SceneManager_Base:IsSceneOnStack(sceneName)
    return false -- Optional override
end

function ZO_SceneManager_Base:WasSceneOnStack(sceneName)
    return false -- Optional override
end

function ZO_SceneManager_Base:WasSceneOnTopOfStack(sceneName)
    return false -- Optional override
end
