//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
#include "cbase.h"
#include "querycache.h"
#include "tier0/vprof.h"
#include "tier1/utlintrusivelist.h"
#include "datacache/imdlcache.h"
#include "vstdlib/jobthread.h"


// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"



#define QUERYCACHE_SIZE 1024

static QueryCacheEntry_t s_QCache[QUERYCACHE_SIZE];

#define QUERYCACHE_HASH_SIZE ( QUERYCACHE_SIZE  * 2 )

// elements available for cache reuse
static CUtlIntrusiveDList<QueryCacheEntry_t> s_VictimList;


static CUtlIntrusiveDList<QueryCacheEntry_t> s_HashChains[QUERYCACHE_HASH_SIZE];



static int s_nReplaceCtr = QUERYCACHE_SIZE - 1;
static int s_nTimeStampCounter = 0 ;
static int s_nNumCacheQueries = 0;
static int s_nNumCacheMisses = 0;
static int s_SuccessfulSpeculatives = 0;
static int s_WastedSpeculativeUpdates = 0;

void QueryCacheKey_t::ComputeHashIndex( void )
{
	unsigned int ret = ( unsigned int ) m_Type;
	for( int i = 0 ; i < m_nNumValidPoints; i++ )
	{
		ret += ( unsigned int ) m_pEntities[i].ToInt();
		ret += ( uintp ) m_nOffsetMode;
	}
	ret += *( ( uint32 *) &m_flMinimumUpdateInterval );
	ret += m_nTraceMask;
	m_nHashIdx = ret % QUERYCACHE_HASH_SIZE;
}


ConVar	sv_disable_querycache("sv_disable_querycache", "0", FCVAR_CHEAT, "debug - disable trace query cache" );

static QueryCacheEntry_t *FindOrAllocateCacheEntry( QueryCacheKey_t const &entry )
{
	QueryCacheEntry_t *pFound = NULL;
	// see if we find it
	for( QueryCacheEntry_t *pNode = s_HashChains[entry.m_nHashIdx].m_pHead; pNode; pNode = pNode->m_pNext )
	{
		if ( pNode->m_QueryParams.Matches( &entry ) )
		{
			pFound = pNode;
			break;
		}
	}
	if (! pFound )
	{
		pFound = s_VictimList.RemoveHead();
		if ( ! pFound )
		{
			// randomly replace one
			pFound = s_QCache + s_nReplaceCtr;
			s_nReplaceCtr--;
			if ( s_nReplaceCtr < 0 )
				s_nReplaceCtr = QUERYCACHE_SIZE - 1;
			if ( pFound->m_QueryParams.m_Type != EQUERY_INVALID )
			{
				s_HashChains[pFound->m_QueryParams.m_nHashIdx].RemoveNode( pFound );
			}
		}
		pFound->m_QueryParams = entry;
		s_HashChains[pFound->m_QueryParams.m_nHashIdx].AddToHead( pFound );
		pFound->m_bSpeculativelyDone = false;
		pFound->IssueQuery();
	}
	else
	{
		if ( sv_disable_querycache.GetInt() || 
			 ( gpGlobals->curtime - pFound->m_flLastUpdateTime >= 
			   pFound->m_QueryParams.m_flMinimumUpdateInterval ) )
		{
			pFound->m_bSpeculativelyDone = false;
			pFound->IssueQuery();
		}
		else
		{
			if ( pFound->m_bSpeculativelyDone )
				s_SuccessfulSpeculatives++;
		}
		
	}
	return pFound;
}

static QueryCacheEntry_t *FindOrAllocateCacheEntry( EQueryType_t nType,
													CBaseEntity *pEntity1, CBaseEntity *pEntity2,
													EEntityOffsetMode_t nMode1, EEntityOffsetMode_t nMode2,
													unsigned int nTraceMask )
{
	QueryCacheKey_t entry;
	entry.m_Type = nType;
	entry.m_pEntities[0] = pEntity1;
	entry.m_pEntities[1] = pEntity2;
	entry.m_nOffsetMode[0] = nMode1;
	entry.m_nOffsetMode[1] = nMode2;
	entry.m_nTraceMask = nTraceMask;
	entry.m_nNumValidPoints = 2;
	entry.ComputeHashIndex();
	return FindOrAllocateCacheEntry( entry );
}

bool QueryCacheKey_t::Matches( QueryCacheKey_t const *pNode ) const
{
	if (
		( pNode->m_Type != m_Type ) ||
		( pNode->m_nTraceMask != m_nTraceMask ) ||
		( pNode->m_pTraceFilterFunction != m_pTraceFilterFunction ) ||
		( pNode->m_nNumValidPoints != m_nNumValidPoints ) || 
		( pNode->m_flMinimumUpdateInterval != m_flMinimumUpdateInterval )
		)
		return false;
	for( int i = 0; i < m_nNumValidPoints; i++ )
	{
		if (
			( pNode->m_pEntities[i] != m_pEntities[i] ) ||
			( pNode->m_nOffsetMode[i] != m_nOffsetMode[i] )
			)
			return false;
	}
	return true;
}

static void CalculateOffsettedPosition( CBaseEntity *pEntity, EEntityOffsetMode_t nMode, Vector *pVecOut  )
{
	switch( nMode )
	{
		case EOFFSET_MODE_WORLDSPACE_CENTER:
			*pVecOut = pEntity->WorldSpaceCenter();
			break;

		case EOFFSET_MODE_EYEPOSITION:
			*pVecOut = pEntity->EyePosition();
			break;

		case EOFFSET_MODE_NONE:
			pVecOut->Init();
			break;
	}
}



struct QueryCacheUpdateRecord_t
{
	int m_nStartHashChain;
	int m_nNumHashChainsToUpdate;
	CUtlIntrusiveDListWithTailPtr<QueryCacheEntry_t> m_KilledList;
};



void ProcessQueryCacheUpdate( QueryCacheUpdateRecord_t &workItem )
{
	float flCurTime = gpGlobals->curtime;
	// run through all of the cache.
	for( int i = 0; i < workItem.m_nNumHashChainsToUpdate; i++ )
	{
		QueryCacheEntry_t *pNext;
		for( QueryCacheEntry_t *pEntry = s_HashChains[i + workItem.m_nStartHashChain].m_pHead ; pEntry; pEntry = pNext )
		{
			pNext = pEntry->m_pNext;
			if ( pEntry->m_bUsedSinceUpdated )
			{
				if ( flCurTime - pEntry->m_flLastUpdateTime >= 
					 pEntry->m_QueryParams.m_flMinimumUpdateInterval )
				{
					// don't bother updating if we have recently
					pEntry->IssueQuery();
					pEntry->m_bUsedSinceUpdated = false;
					pEntry->m_bSpeculativelyDone = true;
				}
			}
			else
			{
				if ( flCurTime - pEntry->m_flLastUpdateTime > pEntry->m_QueryParams.m_flMinimumUpdateInterval )
				{
					if ( pEntry->m_bSpeculativelyDone  && ( !pEntry->m_bUsedSinceUpdated ) )
					{
						s_WastedSpeculativeUpdates++;
					}
					pEntry->m_QueryParams.m_Type = EQUERY_INVALID;
					s_HashChains[pEntry->m_QueryParams.m_nHashIdx].RemoveNode( pEntry );
					workItem.m_KilledList.AddToHead( pEntry );
				}
			}
		}
	}
}


#define N_WAYS_TO_SPLIT_CACHE_UPDATE 8

static void PreUpdateQueryCache()
{
	//mdlcache->BeginCoarseLock();			// x360 only - will need to port for this in the future
	mdlcache->BeginLock();
}

static void PostUpdateQueryCache()
{
	mdlcache->EndLock();
	//mdlcache->EndCoarseLock();			// x360 only - will need to port for this in the future
}


void UpdateQueryCache( void )
{
	// parallel process all hash chains
	QueryCacheUpdateRecord_t workList[N_WAYS_TO_SPLIT_CACHE_UPDATE];
	int nCurEntry = 0;
	for( int i =0 ; i < N_WAYS_TO_SPLIT_CACHE_UPDATE; i++ )
	{
		workList[i].m_nStartHashChain = nCurEntry;
		if ( i != N_WAYS_TO_SPLIT_CACHE_UPDATE -1 )
			workList[i].m_nNumHashChainsToUpdate = ARRAYSIZE( s_HashChains ) / N_WAYS_TO_SPLIT_CACHE_UPDATE;
		else
			workList[i].m_nNumHashChainsToUpdate = ARRAYSIZE( s_HashChains ) - nCurEntry;
		nCurEntry += ARRAYSIZE( s_HashChains ) / N_WAYS_TO_SPLIT_CACHE_UPDATE;
	}
	ParallelProcess( "ProcessQueryCacheUpdate", workList, N_WAYS_TO_SPLIT_CACHE_UPDATE, ProcessQueryCacheUpdate, PreUpdateQueryCache, PostUpdateQueryCache, ( sv_disable_querycache.GetBool() ) ? 0 : INT_MAX );
	// now, we need to take all of the obsolete cache entries each thread generated and add them to
	// the victim cache
	for( int i = 0 ; i < N_WAYS_TO_SPLIT_CACHE_UPDATE; i++ )
	{
		PrependDListWithTailToDList( workList[i].m_KilledList, s_VictimList );
	}
}

void InvalidateQueryCache( void )
{
	s_VictimList.RemoveAll();
	for( int i = 0; i < ARRAYSIZE( s_HashChains); i++ )
		s_HashChains[i].RemoveAll();
	// now, invalidate all cache entries and add them to the victims
	for( int i = 0; i < ARRAYSIZE( s_QCache ); i++ )
	{
		s_QCache[i].m_QueryParams.m_Type = EQUERY_INVALID;
		s_VictimList.AddToHead( s_QCache + i );
	}
}


void QueryCacheEntry_t::IssueQuery( void )
{
	for( int i = 0 ; i < m_QueryParams.m_nNumValidPoints; i++ )
	{
		CBaseEntity *pEntity = m_QueryParams.m_pEntities[i];
		if (! pEntity )
		{
			m_QueryParams.m_Type = EQUERY_INVALID;
			s_HashChains[m_QueryParams.m_nHashIdx].RemoveNode( this );
			s_VictimList.AddToHead( this );
			return;
		}
		CalculateOffsettedPosition( pEntity, m_QueryParams.m_nOffsetMode[i],
									&( m_QueryParams.m_Points[i] ) );
	}
	CTraceFilterSimple filter( m_QueryParams.m_pEntities[2],
							   m_QueryParams.m_nCollisionGroup,
							   m_QueryParams.m_pTraceFilterFunction );
	trace_t result;
	s_nNumCacheMisses++;
	UTIL_TraceLine( m_QueryParams.m_Points[0], m_QueryParams.m_Points[1],
					m_QueryParams.m_nTraceMask, &filter, &result );
	m_bResult = ! ( result.DidHit() );
	m_flLastUpdateTime = gpGlobals->curtime;
}


bool IsLineOfSightBetweenTwoEntitiesClear( CBaseEntity *pSrcEntity,
										   EEntityOffsetMode_t nSrcOffsetMode,
										   CBaseEntity *pDestEntity,
										   EEntityOffsetMode_t nDestOffsetMode,
										   CBaseEntity *pSkipEntity,
										   int nCollisionGroup,
										   unsigned int nTraceMask,
										   ShouldHitFunc_t pTraceFilterCallback,
										   float flMinimumUpdateInterval )
{
	QueryCacheKey_t entry;
	entry.m_Type = EQUERY_ENTITY_LOS_CHECK;
	entry.m_pEntities[0] = pSrcEntity;
	entry.m_pEntities[1] = pDestEntity;
	entry.m_pEntities[2] = pSkipEntity;
	entry.m_nOffsetMode[0] = nSrcOffsetMode;
	entry.m_nOffsetMode[1] = nDestOffsetMode;
	entry.m_nOffsetMode[2] = EOFFSET_MODE_NONE;
	entry.m_nTraceMask = nTraceMask;
	entry.m_nNumValidPoints = 3;
	entry.m_nCollisionGroup = nCollisionGroup;
	entry.m_pTraceFilterFunction = pTraceFilterCallback;
	entry.m_flMinimumUpdateInterval = flMinimumUpdateInterval;
	entry.ComputeHashIndex();

	s_nNumCacheQueries++;
	QueryCacheEntry_t *pNode = FindOrAllocateCacheEntry( entry );
	pNode->m_bUsedSinceUpdated = true;
	return pNode->m_bResult;
}


#if defined( CLIENT_DLL )
CON_COMMAND_F( cl_querycache_stats, "Display status of the query cache (client only)", FCVAR_CHEAT )
#else
CON_COMMAND( sv_querycache_stats, "Display status of the query cache (client only)" )
#endif
{
#ifndef CLIENT_DLL
	if ( !UTIL_IsCommandIssuedByServerAdmin() )
		return;
#endif

	Warning( "%d queries, %d misses (%d free) suc spec = %d wasted spec=%d\n",
			 s_nNumCacheQueries, s_nNumCacheMisses, s_VictimList.Count(),
			 s_SuccessfulSpeculatives, s_WastedSpeculativeUpdates );
}