//
// Asura_Entity_PeriodicClientUpdateList.cpp
//
// Created by KOR - 22/03/2006
// 
// Contains Asura_Entity_PeriodicClientUpdateList, which previously resided in Asura_Entity.cpp,
// but was split off for clarity.
//
// Copyright (C) 2006 Rebellion, All Rights Reserved.
//

#include "Core/Asura.h"

// This file's header
#include "Entity/Asura_Entity_PeriodicClientUpdateList.h"

// Asura headers

#include "Core/Asura_Timers.h"
#include "Entity/Asura_Entity.h"
#include "Network/Asura_Network.h"

// C L A S S   I M P L E M E N T A T I O N S /////////////////////////////////

//	A list is maintained of those entities which require MakeClientUpdateMessage() called.
//	The list is actually a hash table, hashed using Guid & (HashSize-1).
//	All server entities are added to the list in the base class constructor. Some entities
//	do not override the virtual function MakeClientUpdateMessage(). These entities should
//	remove themselves from the list in their constructor by calling SetNeverUpdateClient().
//	An assert will fire if the base class version of MakeClientUpdateMessage() is called 
//	(previously this function was pure virtual). Examples of such entities are volume 
//	triggers - these don't have client entities so they don't need client updates.
//
//	For the entities in the list there are 3 modes: "always", "burst" and "heartbeat".
//	If the mode is "always", the entity always makes a client update. This is largely 
//	to help with supporting legacy entity code. It's also better for entities which really 
//	do change every frame, as it avoids some overhead from adding to / removing from lists.
//	When using the NetMsg system for external networking, many such updates will not actually
//	be sent as it will determine that the update does not contain any new information. 
//
//	The other 2 modes allow for optimisations. To use these modes, Entities must tell the
//	system that they don't need an update every 0.1s by calling SetAlwaysUpdateClient(false)
//	Then they must detect when something changesthat the client needs to know about 
//	(e.g. in the Update function, or in a call such as SetHidden()). When a change is 
//	detected, you should call RequestClientUpdate(). The entity then goes into burst mode 
//	and will send out 5 updates. This is in case some packets go missing due to unreliable 
//	UDP networking. After that (if nothing further changed) the entity goes into heartbeat 
//	mode. In that mode it sets a timer (using the millisecond clock) for when its next 
//	update is due. (A future optimisation would be to make heartbeats become less frequent 
//	with each one, as the likelihood that the last change has been received by all clients 
//	increases.) The heartbeat timers are initially spread out randomly to avoid having all 
//	entities sending heartbeats in the same frame. 
//
//	If Asura_Network::IsExternalNetworkActive() returns false, messages will never be lost, 
//	so only one update is sent in burst mode, and heartbeats are not sent. Instead of going 
//	into heartbeat mode, nodes are removed from the list altogether.
//
//	The updates are spread over several frames. This is because Asura_Network only actually
//	sends data over the wire every 0.1 seconds. The first time GetClientUpdates() is 
//	called, and then after each send, the number of updates done each frame is calculated 
//	from the number of entities in the update list and the frame rate. In the frame that the 
//	send will take place, GetClientUpdates is called with (true) and all remaining entities
//	get the update done, even if there are more than the number done in each previous frame.

const u_int uCLIENT_UPDATE_NUM_SENDS = 5;	// number sent in burst mode
const u_int uCLIENT_UPDATE_HEARTBEAT_TIME = 5000;	// millseconds between heartbeats

Asura_Entity_PeriodicClientUpdateList::Asura_Entity_PeriodicClientUpdateList()
{
	Reset();
}

void Asura_Entity_PeriodicClientUpdateList::Reset()
{
	for (u_int u = 0; u < uPERIODICCLIENTUPDATELIST_HASHSIZE; ++u)
	{
		m_apxHashTable[u] = NULL;
	}
	m_uLastHashIndex = 0;
	m_pxNextEntry = NULL;
	m_uNumUpdatesPerFrame = 0;
	m_uNumNodes = 0;
}

Asura_Entity_Server* Asura_Entity_PeriodicClientUpdateList::FindEntity( Asura_Guid uGuid, bool bIgnoreDestroyedEntities /*= true*/ ) const
{
	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = FindEntry( uGuid );
	if (pxEntry && pxEntry->m_pxEntity && (!bIgnoreDestroyedEntities || !pxEntry->m_pxEntity->IsDestroyed()))
	{
		return pxEntry->m_pxEntity;
	}

	return NULL;
}

void Asura_Entity_PeriodicClientUpdateList::SetAlwaysUpdate(Asura_Entity_Server* pxEntity, bool bEnable)
{
	Asura_Guid uGuid = pxEntity->GetGuid();

	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = FindEntry(uGuid);

	if (!pxEntry)
	{
		pxEntry = AddToList(pxEntity);
		Asura_Assert(pxEntry, "AddToList failed");
	}

	if (bEnable)
	{
		pxEntry->m_eMode = ClientUpdateList_UpdateMode_Always;
		pxEntry->m_uNextUpdateTime = 0;
	}
	else
	{
		// flag the entry for a burst of updates (only one if not networked)
		pxEntry->m_eMode = ClientUpdateList_UpdateMode_Burst;
		pxEntry->m_uNextUpdateTime = Asura_Network::IsExternalNetworkActive() ? uCLIENT_UPDATE_NUM_SENDS : 1;
	}
}


void Asura_Entity_PeriodicClientUpdateList::UpdateNeeded(Asura_Entity_Server* pxEntity)
{
	Asura_Guid uGuid = pxEntity->GetGuid();

	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = FindEntry(uGuid);

	// if this entity always updates do not switch to burst mode.
	// If the entity wasn't in the update list, assume burst mode is OK.
	bool bBurstModeOK = pxEntry ? (pxEntry->m_eMode != ClientUpdateList_UpdateMode_Always) : true;
	if (!pxEntry)
	{
		pxEntry = AddToList(pxEntity);
		Asura_Assert(pxEntry, "AddToList failed");
	}

	if (bBurstModeOK)
	{
		// flag the entry for a burst of updates (only one if not networked)
		pxEntry->m_eMode = ClientUpdateList_UpdateMode_Burst;
		pxEntry->m_uNextUpdateTime = Asura_Network::IsExternalNetworkActive() ? uCLIENT_UPDATE_NUM_SENDS : 1;;
	}
}



Asura_Entity_PeriodicClientUpdateList_Node* Asura_Entity_PeriodicClientUpdateList::AddToList(Asura_Entity_Server* pxEntity)
{
	Asura_Assert(!FindEntry(pxEntity->GetGuid()), "adding to list twice");

	// add to head of list
	u_int uHash = GetHashIndex(pxEntity->GetGuid());
	Asura_Entity_PeriodicClientUpdateList_Node* pxNewEntry = new Asura_Entity_PeriodicClientUpdateList_Node(pxEntity);
	pxNewEntry->m_pxNext = m_apxHashTable[uHash];
	m_apxHashTable[uHash] = pxNewEntry;

	++m_uNumNodes;

	return pxNewEntry;
}


bool Asura_Entity_PeriodicClientUpdateList::RemoveFromList(const Asura_Entity_Server* pxEntity)
{
	Asura_Guid uGuid = pxEntity->GetGuid();
	u_int uHash = GetHashIndex(uGuid);

	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = m_apxHashTable[uHash];
	Asura_Entity_PeriodicClientUpdateList_Node* pxPrevEntry = NULL;
	while (pxEntry)
	{
		if (pxEntry->GetGuid() == uGuid)
		{
			// check if it's the next entry in the partial iteration
			if (m_pxNextEntry == pxEntry)
			{
				m_pxNextEntry = pxEntry->m_pxNext;
			}

			// fix up the links so we can remove this node
			if (pxPrevEntry)
			{
				pxPrevEntry->m_pxNext = pxEntry->m_pxNext;
			}
			else
			{
				m_apxHashTable[uHash] = pxEntry->m_pxNext;
			}

			// delete the node
			delete pxEntry;
			--m_uNumNodes;
			return true;
		}
		pxPrevEntry = pxEntry;
		pxEntry = pxEntry->m_pxNext;
	}
	return false;
}


Asura_Entity_PeriodicClientUpdateList_Node* Asura_Entity_PeriodicClientUpdateList::FindEntry(Asura_Guid uGuid) const
{
	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = m_apxHashTable[GetHashIndex(uGuid)];
	while (pxEntry)
	{
		if (pxEntry->GetGuid() == uGuid)
		{
			return pxEntry;
		}
		pxEntry = pxEntry->m_pxNext;
	}
	return NULL;
}

int Asura_Entity_PeriodicClientUpdateList::GetUpdateMode(Asura_Guid uGuid) const
{
	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = FindEntry(uGuid);
	if( pxEntry )
	{
		return pxEntry->m_eMode;
	}

	// not found
	return -1;
}


void Asura_Entity_PeriodicClientUpdateList::CalculateNumUpdatesPerFrame()
{
	float fUpdatesPerSend = Asura_Timers::GetFrameRate() * ASURA_NETWORK_TIME_BETWEEN_ENTITY_UPDATES;
	float fTotalUpdatesRequired = static_cast<float>(m_uNumNodes);
	m_uNumUpdatesPerFrame = Asura_Maths::FloatToInt(fTotalUpdatesRequired / fUpdatesPerSend);
}


void Asura_Entity_PeriodicClientUpdateList::DoSomeClientUpdates()
{
	if (!m_uNumUpdatesPerFrame)
	{
		CalculateNumUpdatesPerFrame();
	}
	DoClientUpdates();
}


void Asura_Entity_PeriodicClientUpdateList::FinishClientUpdates()
{
	// allow all remaining nodes to be calculated. Do this by 
	m_uNumUpdatesPerFrame = m_uNumNodes;
	DoClientUpdates();

	// clear this so it gets recalculated next time
	m_uNumUpdatesPerFrame = 0;

	// reset current iteration
	m_uLastHashIndex = 0;
	m_pxNextEntry = NULL;
}


void Asura_Entity_PeriodicClientUpdateList::DoClientUpdates()
{
	u_int uNumRemaining = m_uNumUpdatesPerFrame;
	u_int uTime = Asura_Timers::GetMSecsSinceStart();

	u_int uHashIndex;
	Asura_Entity_PeriodicClientUpdateList_Node* pxEntry = NULL;

	for (uHashIndex = m_uLastHashIndex; uHashIndex < uPERIODICCLIENTUPDATELIST_HASHSIZE; ++uHashIndex)
	{
		if (m_pxNextEntry)
		{
			// we're part-way through an iteration. move to this point in the list
			pxEntry = m_pxNextEntry;
			m_pxNextEntry = NULL;
		}
		else
		{
			// we're starting from the head of the table
			pxEntry = m_apxHashTable[uHashIndex];
		}

		while (pxEntry && uNumRemaining)
		{
			--uNumRemaining;

			bool bRemoveNode = false;
			Asura_Entity_Server* pxEntity = pxEntry->m_pxEntity;

			// Set up the next entry here, in case the entry gets deleted as a result of convoluted messages going back to the server.
			m_pxNextEntry = pxEntry->m_pxNext;
			if (pxEntity->IsDestroyed())
			{
				// do nothing, we're going to be gone soon.
			}
			else if (pxEntry->m_eMode == ClientUpdateList_UpdateMode_Always)
			{
				pxEntity->MakeClientUpdateMessage();
			}
			else if (pxEntry->m_eMode == ClientUpdateList_UpdateMode_Burst)
			{
				// decrease the number of updates remaining
				if (--pxEntry->m_uNextUpdateTime == 0)
				{
					// if this is the last one, switch to heartbeat mode (or remove if no network)
					if (Asura_Network::IsExternalNetworkActive())
					{
						pxEntry->m_eMode = ClientUpdateList_UpdateMode_Heartbeat;
						// next heartbeat will at a random time between the next client updates
						// and the next heartbeat interval. This helps avoid all the heartbeats 
						// happening together. Could use a global var to space them evenly instead.
						const int iTimeBetweenUpdates = 100;
						pxEntry->m_uNextUpdateTime = uTime + Asura_Random::GetSeededInt(iTimeBetweenUpdates, static_cast<int>(uCLIENT_UPDATE_HEARTBEAT_TIME));
					}
					else
					{
						bRemoveNode = true;
					}
				}

				pxEntity->MakeClientUpdateMessage();
			}
			else
			{
				if (uTime >= pxEntry->m_uNextUpdateTime)
				{
					// register next heartbeat - to preserve the spacing, next update is done at the 
					// heartbeat interval after this update was due, rather than a set interval from now.
					pxEntry->m_uNextUpdateTime += uCLIENT_UPDATE_HEARTBEAT_TIME;
					pxEntity->MakeClientUpdateMessage();
				}
			}

			if (bRemoveNode)
			{
				// finished with pxEntry associated with pxEntity, so safe to remove it
				RemoveFromList(pxEntity);
			}
			pxEntry = m_pxNextEntry;
		}

		if (!uNumRemaining)
		{
			// exit the for loop if we've more to do
			if (pxEntry)
			{
				break;
			}
		}
	}

	// remember where we got to in the iteration so we can continue next frame.
	m_uLastHashIndex = uHashIndex;
	m_pxNextEntry = pxEntry;
}

Asura_Guid Asura_Entity_PeriodicClientUpdateList_Node::GetGuid() const
{
	return m_pxEntity->GetGuid();
}
