/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.

// Description : Dialog Session


#include "CryLegacy_precompiled.h"
#include "DialogSession.h"
#include "DialogSystem.h"
#include "DialogScript.h"
#include "DialogActorContext.h"
#include "DialogCommon.h"
#include "ICommunicationManager.h"
#include "Components/IComponentAudio.h"

static const float END_GRACE_TIMEOUT = 60.0f;
static const CDialogSession::TActorFlags DEFAULT_ACTOR_FLAGS = CDialogSession::eDACF_Default; // CDialogSession::eDACF_NoAbortSound;

////////////////////////////////////////////////////////////////////////////
CDialogSession::CDialogSession(CDialogSystem* pDS, const CDialogScript* pScript, CDialogSystem::SessionID id)
{
    DiaLOG::Log(DiaLOG::eAlways, "CDialogSession::CDialogSession: this=%p SID=%d Script=%s ", this, id, pScript->GetID().c_str());
    assert (pDS != 0);
    assert (pScript != 0);
    m_pEntitySystem = gEnv->pEntitySystem;
    m_pDS = pDS;
    m_pScript = pScript;
    m_sessionID = id;
    m_curTime = 0.0f;
    m_nextTimeDelay = 0.0f;
    m_endGraceTimeOut = END_GRACE_TIMEOUT;
    m_curScriptLine = -1;
    m_nextScriptLine = -1;
    m_bPlaying = false;
    m_bValidated = false;
    m_bOK      = false;
    m_bHaveSchedule = false;
    m_bAutoDelete = false;
    m_bReachedEnd = false;
    m_pendingActors = 0;
    m_playerAwareAngle = 0.0f;
    m_playerAwareDistance = 0.0f;
    m_playerAwareGraceTime = 3.0f;

    for (int i = 0; i < CDialogScript::MAX_ACTORS; ++i)
    {
        m_actorFlags[i] = DEFAULT_ACTOR_FLAGS;
    }
    m_aiBehaviourMode = eDIB_InterruptAlways;
    m_debugName.Format("SID=%d", m_sessionID, this);
    m_alertnessInterruptMode = Alert;
}

////////////////////////////////////////////////////////////////////////////
CDialogSession::~CDialogSession()
{
    m_actorContextMap.clear(); // not necessary, but be a bit more explicit
    DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession::~CDialogSession: %s", GetDebugName());
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::Release()
{
    DoStop(); // force stopping [most important thing is un-precaching sounds]
    NotifyListeners(eDSE_SessionDeleted);
    delete this;
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::AddListener(IDialogSessionListener* pListener)
{
    return stl::push_back_unique(m_listenerVec, pListener);
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::RemoveListener(IDialogSessionListener* pListener)
{
    return stl::find_and_erase(m_listenerVec, pListener);
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::NotifyListeners(EDialogSessionEvent event)
{
    DiaLOG::Log(DiaLOG::eAlways, "[DIALOG} CDialogSession: %s Notifying listeners on Event %s", GetDebugName(), GetEventName(event));

    m_listenerVecTemp.reserve(m_listenerVec.size());
    m_listenerVecTemp.resize(0);
    m_listenerVecTemp.insert(m_listenerVecTemp.end(), m_listenerVec.begin(), m_listenerVec.end());

    // it's safe to remove oneself while being called back
    for (int i = (int)m_listenerVecTemp.size() - 1; i >= 0; i--)
    {
        m_listenerVecTemp[i]->SessionEvent(this, event);
    }
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::Play(int fromScriptLine)
{
    return InternalPlay(fromScriptLine, true);
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::InternalPlay(int fromScriptLine, bool bNotify)
{
    if (!m_bValidated)
    {
        m_bOK = Validate();
    }

    if (!m_bOK)
    {
        GameWarning("[DIALOG] CDialogSession::Play: %s Parameters for Script '%s' not valid.", GetDebugName(), m_pScript->GetID().c_str());
        return false;
    }

    if (m_bPlaying)
    {
        return false;
    }

    // fromScriptLine == m_pScript->GetNumLines()    is a valid situation: sometimes the dialog reachs the last line, but the dialogactorcontext is not finished so the dialog is not "done" yet.
    // if a saveload happens in that situation and we return "false" here after loading the saveload, the dialog will never notify as "done".
    // when fromScriptLine == m_pScript->GetNumLines() and we call this function, the dialog will just gracefully finish in the next DoPlay() call.
    if (fromScriptLine < 0 || fromScriptLine > m_pScript->GetNumLines())
    {
        GameWarning("[DIALOG] CDialogSession::Play: %s FromScriptLine %d exceeds number of lines [0..%d] of Script '%s'.",
            GetDebugName(), fromScriptLine, m_pScript->GetNumLines() - 1, m_pScript->GetID().c_str());
        return false;
    }

    m_pDS->AddSession(this);
    m_bPlaying       = true;
    m_bReachedEnd    = true;  // will be set to false by DoPlay
    m_bFirstUpdate   = true;
    m_curTime        = 0.0f;
    m_endGraceTimeOut = END_GRACE_TIMEOUT;
    m_curScriptLine  = -1;
    m_nextScriptLine = fromScriptLine - 1;
    m_abortReason    = eAR_None;
    m_pendingActors  = 0;
    ScheduleNextLine(0.0f);

    if (bNotify)
    {
        NotifyListeners(eDSE_SessionStart);
    }

    //Restrict communication manager from using these actors
    {
        ICommunicationManager* pCommunicationManager = gEnv->pAISystem->GetCommunicationManager();

        TIdToEntityMap::iterator iter = m_idToEntityMap.begin();
        while (iter != m_idToEntityMap.end())
        {
            EntityId actorId = (*iter).second;
            pCommunicationManager->AddActorRestriction(actorId, true, true);
            ++iter;
        }
    }

    {
        TActorContextMap::iterator iter = m_actorContextMap.begin();
        while (iter != m_actorContextMap.end())
        {
            (*iter).second->BeginSession();
            ++iter;
        }
    }

    // AlexL 20/08/2007: don't update here
    // because other subsystems (FG) might process the BeginSession command
    // but get the abortion in the same frame.
    // to fix this, we delay the real playback of the first line to the next frame
    // so other subsystems get a chance to process it
    // DoPlay(0.0f);
    return true;
}

////////////////////////////////////////////////////////////////////////////
int CDialogSession::ScheduleNextLine(float dt)
{
    m_bHaveSchedule = true;
    m_nextTimeDelay = dt;
    ++m_nextScriptLine;

    if (m_nextScriptLine < m_pScript->GetNumLines())
    {
        DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession: %s Scheduling next line %d/%d [cur=%d] at %f",
            GetDebugName(), m_nextScriptLine, m_pScript->GetNumLines() - 1, m_curScriptLine, m_curTime + m_nextTimeDelay);
    }
    else
    {
        DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession: %s Scheduling END [cur=%d] at %f",
            GetDebugName(), m_curScriptLine, m_curTime + m_nextTimeDelay);
    }
    return m_nextScriptLine;
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::DoPlay(float dt)
{
    if (m_bFirstUpdate == false)
    {
        m_curTime += dt;
    }
    else
    {
        m_bFirstUpdate = false;
    }

    // delayed aborting
    if (m_abortReason != eAR_None)
    {
        TActorContextMap::iterator iter = m_actorContextMap.begin();
        while (iter != m_actorContextMap.end())
        {
            CDialogActorContextPtr pContext = (*iter).second;
            if (pContext->IsStillPlaying())
            {
                m_endGraceTimeOut -= dt;
                if (m_endGraceTimeOut >= 0.0f)
                {
                    return;
                }
                else
                {
                    break;
                }
            }
            ++iter;
        }
        DoStop();
        NotifyListeners(eDSE_Aborted);
        return;
    }

    bool bContinue = true;
    if (m_bHaveSchedule)
    {
        m_nextTimeDelay -= dt;
        if (m_nextTimeDelay <= 0.0f)
        {
            m_bHaveSchedule = false;
            m_curScriptLine = m_nextScriptLine;
            if (m_curScriptLine < m_pScript->GetNumLines())
            {
                const CDialogScript::SScriptLine* pLine = m_pScript->GetLine(m_curScriptLine);
                if (pLine)
                {
                    m_bReachedEnd = false;
                    DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession: %s Playing new line %d: now=%f", GetDebugName(), m_curScriptLine, m_curTime);
                    bool ok = PlayLine(pLine);
                    bContinue = ok; // PlayLine can vote for immediate stop by returning false!
                    NotifyListeners(eDSE_LineStarted);
                }
            }
            else
            {
                // we're past end
                m_bReachedEnd = true;
            }
        }
    }

    int nStillPlaying = 0;
    int nSoundWantContinue = 0;

    // update all contexts
    CDialogScript::SActorSet aborted = 0;
    {
        TActorContextMap::iterator iter = m_actorContextMap.begin();
        while (iter != m_actorContextMap.end())
        {
            CDialogActorContextPtr pContext = (*iter).second;
            pContext->Update(dt);
            if (pContext->IsAborted())
            {
                // First one which aborts, aborts the whole session
                if (m_abortReason == eAR_None)
                {
                    m_abortReason = pContext->GetAbortReason();
                }
                aborted.SetActor(iter->first);
            }
            if (pContext->IsStillPlaying())
            {
                ++nStillPlaying;
            }
            if (pContext->CheckActorFlags(CDialogSession::eDACF_NoAbortSound))
            {
                ++nSoundWantContinue;
            }
            ++iter;
        }
    }

    // when we've reached the end there could still be some contexts playing
    if (m_bReachedEnd)
    {
        if (nStillPlaying == 0)
        {
            // no context playing, do a real end now
            bContinue = false;
        }
        else
        {
            if (nStillPlaying != m_pendingActors)
            {
                m_pendingActors = nStillPlaying;
                DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession: %s End of Script reached. Waiting for %d pending Actors.", GetDebugName(), nStillPlaying);
            }
            // wait for some grace time and stop if timed out
            m_endGraceTimeOut -= dt;
            bContinue = m_endGraceTimeOut >= 0.0f;
        }
    }

    if (aborted.NumActors() > 0)
    {
        if (nSoundWantContinue == 0 || nStillPlaying == 0)
        {
            DoStop();
            NotifyListeners(eDSE_Aborted);
        }
        else
        {
            DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession: %s Delaying abortion.", GetDebugName());
        }
    }
    else if (bContinue == false)
    {
        DoStop();
        NotifyListeners(eDSE_EndOfDialog);
    }
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::PlayLine(const CDialogScript::SScriptLine* pLine)
{
    bool bOk = false;
    IEntity* pActorEntity = GetActorEntity(pLine->m_actor);
    if (pActorEntity)
    {
        CDialogActorContextPtr pContext = GetContext(pLine->m_actor);
        if (pContext)
        {
            bOk = pContext->PlayLine(pLine);
        }
        else
        {
            assert (pContext != 0);
            GameWarning("[DIALOG] CDialogSession::PlayLine: %s [Script=%s] No Context for Actor %d",
                GetDebugName(), m_pScript->GetID().c_str(), pLine->m_actor);
        }
    }

    return bOk;
}

////////////////////////////////////////////////////////////////////////////
IEntity* CDialogSession::GetActorEntity(CDialogScript::TActorID actorID) const
{
    EntityId id = stl::find_in_map(this->m_idToEntityMap, actorID, 0);
    if (id == 0)
    {
        return 0;
    }
    return m_pEntitySystem->GetEntity(id);
}

////////////////////////////////////////////////////////////////////////////
EntityId CDialogSession::GetActorEntityId(CDialogScript::TActorID actorID) const
{
    return stl::find_in_map(this->m_idToEntityMap, actorID, 0);
}

////////////////////////////////////////////////////////////////////////////
IComponentAudioPtr CDialogSession::GetEntityAudioComponent(IEntity* pEntity) const
{
    if (!pEntity)
    {
        return 0;
    }

    return pEntity->GetOrCreateComponent<IComponentAudio>();
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::Stop()
{
    bool bStopped = DoStop();
    if (bStopped)
    {
        NotifyListeners(eDSE_UserStopped);
    }

    return bStopped;
}

////////////////////////////////////////////////////////////////////////////
CDialogSession::EAbortReason CDialogSession::GetAbortReasonForActor(CDialogScript::TActorID actorID) const
{
    const CDialogActorContextPtr pContext = GetContext(actorID);
    if (pContext)
    {
        return pContext->GetAbortReason();
    }
    return eAR_None;
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::DoStop()
{
    DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession::DoStop: %s", GetDebugName());

    if (!m_bPlaying)
    {
        return false;
    }

    {
        TActorContextMap::iterator iter = m_actorContextMap.begin();
        while (iter != m_actorContextMap.end())
        {
            (*iter).second->EndSession();
            ++iter;
        }
    }
    //Remove actor restrictions from this sessions on the communication manager.
    {
        ICommunicationManager* pCommunicationManager = gEnv->pAISystem->GetCommunicationManager();

        TIdToEntityMap::iterator iter = m_idToEntityMap.begin();
        while (iter != m_idToEntityMap.end())
        {
            EntityId actorId = (*iter).second;
            pCommunicationManager->RemoveActorRestriction(actorId, true, true);
            ++iter;
        }
    }
    m_pDS->RemoveSession(this);
    m_bPlaying = false;

    if (m_bAutoDelete)
    {
        m_bAutoDelete = false;
        m_pDS->DeleteSession(m_sessionID);
    }

    return true;
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::SetAutoDelete(bool bAutoDelete)
{
    m_bAutoDelete = bAutoDelete;
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::Update(float dt)
{
    if (!m_bPlaying)
    {
        return false;
    }

    if (dt > 0.0f)
    {
        DoPlay(dt);
    }

    return true;
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::SetActor(CDialogScript::TActorID actorID, EntityId entityId)
{
    // Setting an actor invalidates Session
    m_bValidated = false;

    if (actorID >= CDialogScript::MAX_ACTORS)
    {
        return false;
    }

    m_idToEntityMap[actorID] = entityId;
    if (entityId != 0)
    {
        m_actorSet.SetActor(actorID);
        m_actorContextMap[actorID] = new CDialogActorContext(this, actorID);
    }
    else
    {
        m_actorSet.ResetActor(actorID);
        m_actorContextMap.erase(actorID);
    }
    return true;
}

////////////////////////////////////////////////////////////////////////////
CDialogSession::CDialogActorContextPtr CDialogSession::GetContext(CDialogScript::TActorID actorID) const
{
    CDialogActorContextPtr pContext = stl::find_in_map(m_actorContextMap, actorID, 0);
    return pContext;
}
////////////////////////////////////////////////////////////////////////////

CDialogSession::CDialogActorContextPtr CDialogSession::GetContext(CDialogSystem::ActorContextID contextID) const
{
    for (TActorContextMap::const_iterator it = m_actorContextMap.begin(); it != m_actorContextMap.end(); ++it)
    {
        if (it->second->GetContextID() == contextID)
        {
            return it->second;
        }
    }

    return 0;
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::Validate()
{
    m_bOK = m_actorSet.Satisfies(m_pScript->GetRequiredActorSet());
    m_bValidated = true;
    return m_bOK;
}


////////////////////////////////////////////////////////////////////////////
const char* CDialogSession::GetEventName(EDialogSessionEvent event)
{
    static const char* names [] = {
        "eDSE_SessionStart",
        "eDSE_EndOfDialog",
        "eDSE_UserStopped",
        "eDSE_Aborted",
        "eDSE_SessionDeleted",
        "eDSE_LineStarted"
    };
    static const int numNames = sizeof(names) / sizeof(*names);
    int index = (int) event;
    if (index < 0 || index >= numNames)
    {
        return "eDSE_UNKNOWN!";
    }
    return names[index];
}


template<typename T>
void SerializeArray(TSerialize ser, const char* szName, T* vec, uint32 count)
{
    ser.BeginGroup(szName);
    if (ser.IsWriting())
    {
        ser.Value("count", count);
        for (uint32 i = 0; i < count; ++i)
        {
            ser.BeginGroup("i");
            ser.Value("Value", vec[i]);
            ser.EndGroup();
        }
    }
    else
    {
        uint32 storedCount = 0;
        ser.Value("count", storedCount);
        uint32 toFill = storedCount < count ? storedCount : count;
        uint32 i = 0;
        while (toFill--)
        {
            ser.BeginGroup("i");
            ser.Value("Value", vec[i++]);
            ser.EndGroup();
        }
    }
    ser.EndGroup();
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::Serialize(TSerialize ser)
{
    //IEntitySystem*           m_pEntitySystem;     ctor
    //CDialogSystem*           m_pDS;               ctor
    //const CDialogScript*     m_pScript;           ctor
    //TIdToEntityMap           m_idToEntityMap;     SetActor
    //TListenerVec             m_listenerVec;       cleared
    //TActorContextMap         m_actorContextMap;   SetActor
    //CDialogSystem::SessionID m_sessionID;         ctor
    //string                   m_debugName;         ctor
    //CDialogScript::SActorSet m_actorSet;          ctor(=0) then SetActor

    //float                    m_curTime;           ctor 0
    //float                    m_nextTimeDelay;     ctor 0
    //int                      m_curScriptLine;     ctor -1
    //int                      m_nextScriptLine;    ctor -1

    //unsigned int             m_bPlaying     : 1;  ctor false
    //unsigned int             m_bValidated   : 1;  ctor false
    //unsigned int             m_bOK          : 1;  ctor false
    //unsigned int             m_bHaveSchedule: 1;  ctor false
    bool playing = m_bPlaying;
    ser.Value("m_bPlaying", playing);
    ser.Value("m_curScriptLine", m_curScriptLine);
    ser.Value("m_playerAwareAngle", m_playerAwareAngle);
    ser.Value("m_playerAwareDistance", m_playerAwareDistance);
    ser.Value("m_playerAwareGraceTime", m_playerAwareGraceTime);
    ser.Value("m_aiBehaviourMode", m_aiBehaviourMode);
    ser.Value("m_alertnessInterruptMode", *(alias_cast<int*>(&m_alertnessInterruptMode)));

    // IdToEntityMap
    if (ser.IsWriting())
    {
        ser.Value("m_idToEntityMap", m_idToEntityMap);
    }
    else
    {
        TIdToEntityMap idmap;
        ser.Value("m_idToEntityMap", idmap);
        DiaLOG::Log(DiaLOG::eAlways, "CDialogSession::Serialize: %s was playing=%d --> ID2EntityMap", GetDebugName(), playing);
        TIdToEntityMap::iterator iter = idmap.begin();
        TIdToEntityMap::iterator end  = idmap.end();
        while (iter != end)
        {
            DiaLOG::Log(DiaLOG::eAlways, "ID %d -> Entity %d", iter->first, iter->second);
            ++iter;
        }
        m_idToEntityMap = idmap;
    }

    // Serialize actor flags
    if (ser.IsReading())
    {
        for (int i = 0; i < CDialogScript::MAX_ACTORS; ++i)
        {
            m_actorFlags[i] = DEFAULT_ACTOR_FLAGS;
        }
    }

    if (!m_idToEntityMap.empty())
    {
        static const size_t maxCount = sizeof(m_actorFlags) / sizeof(*m_actorFlags);
        assert (m_idToEntityMap.size() <= maxCount);
        const uint32 count = m_idToEntityMap.size() < maxCount ? m_idToEntityMap.size() : maxCount;
        SerializeArray(ser, "m_actorFlags", m_actorFlags, count);
    }
}

////////////////////////////////////////////////////////////////////////////
bool CDialogSession::RestoreAndPlay()
{
    // create actor contexts
    TIdToEntityMap::iterator iter = m_idToEntityMap.begin();
    TIdToEntityMap::iterator end = m_idToEntityMap.end();
    for (; iter != end; ++iter)
    {
        const CDialogScript::TActorID& actorID = iter->first;
        if (actorID >= CDialogScript::MAX_ACTORS)
        {
            continue;
        }
        const EntityId& entityId = iter->second;
        if (entityId != 0)
        {
            m_actorSet.SetActor(actorID);
            m_actorContextMap[actorID] = new CDialogActorContext(this, actorID);
        }
        else
        {
            m_actorSet.ResetActor(actorID);
            m_actorContextMap.erase(actorID);
        }
    }
    return InternalPlay(m_curScriptLine, false);
}

////////////////////////////////////////////////////////////////////////////
CDialogScript::TActorID CDialogSession::GetActorIdForEntity(EntityId entityId) const
{
    TIdToEntityMap::const_iterator iter = m_idToEntityMap.begin();
    TIdToEntityMap::const_iterator end = m_idToEntityMap.end();
    while (iter != end)
    {
        if (iter->second == entityId)
        {
            return iter->first;
        }
        ++iter;
    }
    return CDialogScript::NO_ACTOR_ID;
}

////////////////////////////////////////////////////////////////////////////
CDialogSession::TActorFlags CDialogSession::GetActorFlags(CDialogScript::TActorID actorID) const
{
    if (actorID >= CDialogScript::MAX_ACTORS)
    {
        return 0;
    }
    return m_actorFlags[actorID];
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::SetActorFlags(CDialogScript::TActorID actorID, CDialogSession::TActorFlags inFlags)
{
    if (actorID >= CDialogScript::MAX_ACTORS)
    {
        return;
    }
    m_actorFlags[actorID] = inFlags;
}

void CDialogSession::GetMemoryUsage(ICrySizer* pSizer) const
{
    pSizer->AddObject(m_idToEntityMap);
    pSizer->AddObject(m_listenerVec);
    pSizer->AddObject(m_actorContextMap);
}