////////////////////////////////////////////////////////////////////////////
//
//  Crytek Engine Source File.
//  Copyright (C), Crytek Studios, 2006.
// -------------------------------------------------------------------------
//  File name:   DialogSession.cpp
//  Version:     v1.00
//  Created:     07/07/2006 by AlexL
//  Compilers:   Visual Studio.NET
//  Description: Dialog Session
// -------------------------------------------------------------------------
//  History:
//
////////////////////////////////////////////////////////////////////////////
#include "StdAfx.h"
#include "DialogSession.h"
#include "DialogSystem.h"
#include "DialogScript.h"
#include "DialogActorContext.h"
#include "DialogCommon.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 = GetISystem()->GetIEntitySystem();
	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);
}

////////////////////////////////////////////////////////////////////////////
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));
	// it's safe to remove oneself while being called back
	{
		TListenerVec::reverse_iterator iter = m_listenerVec.rbegin();
		while (iter != m_listenerVec.rend())
		{
			TListenerVec::reverse_iterator cur = iter;
			++iter;
			(*cur)->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;

	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;
	}

	PrecacheSounds();

	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);

	{
		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);
}

////////////////////////////////////////////////////////////////////////////
IEntitySoundProxy* CDialogSession::GetEntitySoundProxy(IEntity* pEntity) const
{
	if (!pEntity)
		return 0;

	IEntityProxy* pProxy = pEntity->GetProxy(ENTITY_PROXY_SOUND);
	if (!pProxy)
	{
		if (pEntity->CreateProxy(ENTITY_PROXY_SOUND))
			pProxy = pEntity->GetProxy(ENTITY_PROXY_SOUND);
	}
	return (IEntitySoundProxy*) pProxy;
}

////////////////////////////////////////////////////////////////////////////
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;
		}
	}

	m_pDS->RemoveSession(this);
	m_bPlaying = false;

	UnCacheSounds();

	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;
}

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

////////////////////////////////////////////////////////////////////////////
void CDialogSession::PrecacheSounds()
{
	if (CDialogSystem::sPrecacheSounds == 0)
		return;

	ISoundSystem* pSoundSys = gEnv->pSoundSystem;
	if (!pSoundSys)
		return;

	const int numLines = m_pScript->GetNumLines();
	for (int i=0; i<numLines; ++i)
	{
		const CDialogScript::SScriptLine* pLine = m_pScript->GetLine(i);
		if (pLine->m_sound.empty())
			continue;

		const char* fullSoundPath = CDialogActorContext::GetSoundKey(pLine->m_sound);
		bool ok = (pSoundSys->Precache(fullSoundPath, CDialogActorContext::SOUND_FLAGS, FLAG_SOUND_PRECACHE_DIALOG_DEFAULT) != ePrecacheResult_Error);
		if (ok/* && (pSoundBuffer->Loaded() || pSoundBuffer->Loading() */)  // ISoundBuffer is not exposed from SoundSystem
		{
			// TODO: do something, maybe store for unprecaching?
			DiaLOG::Log(DiaLOG::eAlways, "[DIALOG] CDialogSession::PrecacheSounds: %s [Script=%s] Sound '%s' precached", 
				GetDebugName(), m_pScript->GetID().c_str(), fullSoundPath);
		}
		else
		{
			GameWarning("[DIALOG] CDialogSession::PrecacheSounds: %s [Script=%s] Cannot precache sound '%s'", 
				GetDebugName(), m_pScript->GetID().c_str(), fullSoundPath);
		}
	}
}

////////////////////////////////////////////////////////////////////////////
void CDialogSession::UnCacheSounds()
{
	ISoundSystem* pSoundSys = gEnv->pSoundSystem;
	if (!pSoundSys)
		return;

	// TODO: really un-precache sounds!
}

////////////////////////////////////////////////////////////////////////////
const char* CDialogSession::GetEventName(EDialogSessionEvent event)
{
	static 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);

	// 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::GetMemoryStatistics(ICrySizer * s)
{
	s->Add(*this);
	s->AddContainer(m_idToEntityMap);
	s->AddContainer(m_listenerVec);
	s->AddContainer(m_actorContextMap);
	for (TActorContextMap::iterator iter = m_actorContextMap.begin(); iter != m_actorContextMap.end(); ++iter)
		iter->second->GetMemoryStatistics(s);
}
