//========= Copyright © 1996-2009, Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//=====================================================================================//

#include "mm_framework.h"

#include "leaderboards.h"

#include "fmtstr.h"

// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"

//
// Definition of leaderboard request queue class
//

class CLeaderboardRequestQueue : public ILeaderboardRequestQueue
{
public:
	CLeaderboardRequestQueue();
	~CLeaderboardRequestQueue();

	// ILeaderboardRequestQueue
public:
	virtual void Request( KeyValues *pRequest );
	virtual void Update();

public:
	KeyValues * GetFinishedRequest();

protected:
	CUtlVector< KeyValues * > m_arrRequests;
	bool m_bQueryRunning;
	KeyValues *m_pFinishedRequest;

	void OnStartNewQuery();
	void OnSubmitQuery();
	void OnQueryFinished();
	void Cleanup();

protected:
#ifdef _X360

	// Leaderboard query data
	CUtlVector< XUID > m_arrXuids;
	CUtlVector< XUSER_STATS_SPEC > m_arrSpecs;
	CUtlBuffer m_bufResults;
	XOVERLAPPED m_xOverlapped;

	void ProcessResults( XUSER_STATS_READ_RESULTS const *pResults );

	// Leaderboard description data
	CUtlVector< KeyValues * > m_arrViewDescriptions;

	KeyValues * FindViewDescription( DWORD dwViewId );

#elif !defined( NO_STEAM )

	KeyValues *m_pViewDescription;

	CCallResult< CLeaderboardRequestQueue, LeaderboardFindResult_t > m_CallbackOnLeaderboardFindResult;
	void Steam_OnLeaderboardFindResult( LeaderboardFindResult_t *p, bool bError );

	CCallResult< CLeaderboardRequestQueue, LeaderboardScoresDownloaded_t > m_CallbackOnLeaderboardScoresDownloaded;
	void Steam_OnLeaderboardScoresDownloaded( LeaderboardScoresDownloaded_t *p, bool bError );

	void ProcessResults( LeaderboardEntry_t const &lbe );

#endif
};
CLeaderboardRequestQueue g_LeaderboardRequestQueue;
ILeaderboardRequestQueue *g_pLeaderboardRequestQueue = &g_LeaderboardRequestQueue;




//
// Implementation of leaderboard request queue class
//

CLeaderboardRequestQueue::CLeaderboardRequestQueue() :
	m_bQueryRunning( false ),
#if !defined( _X360 ) && !defined( NO_STEAM )
	m_pViewDescription( NULL ),
#endif
	m_pFinishedRequest( NULL )
{
}

CLeaderboardRequestQueue::~CLeaderboardRequestQueue()
{
	Cleanup();
}

void CLeaderboardRequestQueue::Request( KeyValues *pRequest )
{
	if ( !pRequest )
		return;

	DevMsg( "CLeaderboardRequestQueue::Request\n" );
	KeyValuesDumpAsDevMsg( pRequest, 1 );

	m_arrRequests.AddToTail( pRequest->MakeCopy() );
}

void CLeaderboardRequestQueue::Update()
{
	if ( m_bQueryRunning )
	{
#ifdef _X360
		if ( XHasOverlappedIoCompleted( &m_xOverlapped ) )
		{
			OnQueryFinished();
		}
#endif
	}
	else if ( m_arrRequests.Count() )
	{
		OnStartNewQuery();
	}
	else if ( m_arrRequests.NumAllocated() )
	{
		Cleanup();
	}
}

KeyValues * CLeaderboardRequestQueue::GetFinishedRequest()
{
	return m_pFinishedRequest;
}

void CLeaderboardRequestQueue::OnQueryFinished()
{
#ifdef _X360
	// Submit results for processing
	XUSER_STATS_READ_RESULTS const *pResults = ( XUSER_STATS_READ_RESULTS const * ) m_bufResults.Base();
	if ( pResults && pResults->dwNumViews > 0 && pResults->pViews )
		ProcessResults( pResults );
#endif

	// Query is no longer running
	m_bQueryRunning = false;

	DevMsg( "CLeaderboardRequestQueue::OnQueryFinished\n" );
	KeyValuesDumpAsDevMsg( m_pFinishedRequest, 1 );

	// Stuff the data into the players
#ifdef _X360
	for ( int iCtrlr = 0; iCtrlr < XUSER_MAX_COUNT; ++ iCtrlr )
	{
		XUID xuid = 0;
		XUSER_SIGNIN_INFO xsi;
		if ( XUserGetSigninState( iCtrlr ) != eXUserSigninState_NotSignedIn &&
			ERROR_SUCCESS == XUserGetSigninInfo( iCtrlr, XUSER_GET_SIGNIN_INFO_ONLINE_XUID_ONLY, &xsi ) &&
			!(xsi.dwInfoFlags & XUSER_INFO_FLAG_GUEST) )
			xuid = xsi.xuid;
#else
	int iCtrlr = XBX_GetPrimaryUserId();
	{
		XUID xuid = g_pPlayerManager->GetLocalPlayer( iCtrlr )->GetXUID();
#endif

		KeyValues *pUserViews = NULL;
		if ( xuid )
			pUserViews = m_pFinishedRequest->FindKey( CFmtStr( "%llx", xuid ) );

		if ( pUserViews )
		{
			IPlayerLocal *pPlayer = g_pPlayerManager->GetLocalPlayer( iCtrlr );
			if ( pPlayer )
				(( PlayerLocal * ) pPlayer)->OnLeaderboardRequestFinished( pUserViews );
		}

	}
}

void CLeaderboardRequestQueue::Cleanup()
{
#ifdef _X360
	// Clear view descriptions
	while ( m_arrViewDescriptions.Count() )
	{
		m_arrViewDescriptions.Head()->deleteThis();
		m_arrViewDescriptions.FastRemove( 0 );
	}

	m_arrXuids.Purge();
	m_arrSpecs.Purge();
	m_bufResults.Purge();
	m_arrViewDescriptions.Purge();
#elif !defined( NO_STEAM )
	if ( m_pViewDescription )
		m_pViewDescription->deleteThis();
	m_pViewDescription = NULL;
#endif

	// Clear requests
	while ( m_arrRequests.Count() )
	{
		m_arrRequests.Head()->deleteThis();
		m_arrRequests.FastRemove( 0 );
	}

	m_arrRequests.Purge();

	// Clear finished result
	if ( m_pFinishedRequest )
		m_pFinishedRequest->deleteThis();
	m_pFinishedRequest = NULL;
}

void CLeaderboardRequestQueue::OnStartNewQuery()
{
	// When we are starting a new query we need to get rid of the old finished result
	if ( m_pFinishedRequest )
		m_pFinishedRequest->deleteThis();
	m_pFinishedRequest = NULL;

#if !defined( NO_STEAM ) && !defined( SWDS )
	extern CInterlockedInt g_numSteamLeaderboardWriters;
	if ( g_numSteamLeaderboardWriters )
		return; // yield to writers that can alter the leaderboard
#endif

	DevMsg( "CLeaderboardRequestQueue::OnStartNewQuery preparing request...\n" );

#ifdef _X360
	// Prepare the XUIDs first
	m_arrXuids.RemoveAll();
	for ( DWORD k = 0; k < XBX_GetNumGameUsers(); ++ k )
	{
		int iCtrlr = XBX_GetUserId( k );
		XUSER_SIGNIN_INFO xsi;
		if ( XUserGetSigninState( iCtrlr ) != eXUserSigninState_NotSignedIn &&
			ERROR_SUCCESS == XUserGetSigninInfo( iCtrlr, XUSER_GET_SIGNIN_INFO_ONLINE_XUID_ONLY, &xsi ) &&
			!(xsi.dwInfoFlags & XUSER_INFO_FLAG_GUEST) )
		{
			m_arrXuids.AddToTail( xsi.xuid );
			DevMsg( "    XUID: %s (%llx)\n", xsi.szUserName, xsi.xuid );
		}
	}

	// Clear view descriptions
	while ( m_arrViewDescriptions.Count() )
	{
		m_arrViewDescriptions.Head()->deleteThis();
		m_arrViewDescriptions.FastRemove( 0 );
	}

	// Prepare the spec
	m_arrSpecs.RemoveAll();
	for ( int q = 0; q < m_arrRequests.Count(); ++ q )
	{
		KeyValues *pRequest = m_arrRequests[q];
		KeyValues::AutoDelete autodelete_pRequest( pRequest );
		m_arrRequests.Remove( q -- );

		char const *szViewName = pRequest->GetName();

		KeyValues *pDescription = g_pMMF->GetMatchTitle()->DescribeTitleLeaderboard( szViewName );
		if ( !pDescription )
		{
			DevWarning( "   View %s failed to allocate description!\n", szViewName );
		}
		KeyValues::AutoDelete autodelete_pDescription( pDescription );

		// See if we already have a request for this view
		DWORD dwViewId = pDescription->GetInt( ":id" );
		for ( int k = 0; k < m_arrSpecs.Count(); ++ k )
		{
			if ( m_arrSpecs[k].dwViewId == dwViewId )
			{
				dwViewId = 0;
				break;
			}
		}

		// If we already have a request for this view, then continue
		if ( !dwViewId )
			continue;

		// Otherwise add this view to the spec
		XUSER_STATS_SPEC xss = {0};
		xss.dwViewId = dwViewId;
		Assert( !xss.dwNumColumnIds );

		m_arrSpecs.AddToTail( xss );
		pDescription->SetString( ":name", szViewName );
		m_arrViewDescriptions.AddToTail( pDescription );
		autodelete_pDescription.Assign( NULL );	// don't autodelete now

		DevMsg( "    View: %s (%d)\n", szViewName, dwViewId );
	}
#elif !defined( NO_STEAM )
	// Clear view descriptions
	if ( m_pViewDescription )
		m_pViewDescription->deleteThis();
	m_pViewDescription = NULL;

	for ( int q = 0; q < m_arrRequests.Count(); ++ q )
	{
		KeyValues *pRequest = m_arrRequests[q];
		KeyValues::AutoDelete autodelete_pRequest( pRequest );
		m_arrRequests.Remove( q -- );

		char const *szViewName = pRequest->GetName();

		m_pViewDescription = g_pMMF->GetMatchTitle()->DescribeTitleLeaderboard( szViewName );
		if ( !m_pViewDescription )
		{
			DevWarning( "   View %s failed to allocate description!\n", szViewName );
			continue;
		}

		m_pViewDescription->SetString( ":name", szViewName );

		SteamAPICall_t hCall = steamapicontext->SteamUserStats()->FindLeaderboard( szViewName );
		m_CallbackOnLeaderboardFindResult.Set( hCall, this, &CLeaderboardRequestQueue::Steam_OnLeaderboardFindResult );
		m_bQueryRunning = true;
		break;
	}
#endif

	// Clean up all the requests in the queue
	DevMsg( "CLeaderboardRequestQueue::OnStartNewQuery - request prepared.\n" );

	// Run the query
	OnSubmitQuery();
}

void CLeaderboardRequestQueue::OnSubmitQuery()
{
#ifdef _X360
	if ( m_arrXuids.Count() && m_arrSpecs.Count() )
	{
		DWORD dwBytes = 0;
		DWORD ret = XUserReadStats( 0,
			m_arrXuids.Count(), m_arrXuids.Base(),
			m_arrSpecs.Count(), m_arrSpecs.Base(),
			&dwBytes, NULL, NULL );

		if ( ret == ERROR_INSUFFICIENT_BUFFER )
		{
			ZeroMemory( &m_xOverlapped, sizeof( m_xOverlapped ) );
			m_bufResults.EnsureCapacity( dwBytes );

			ret = XUserReadStats( 0,
				m_arrXuids.Count(), m_arrXuids.Base(),
				m_arrSpecs.Count(), m_arrSpecs.Base(),
				&dwBytes, ( PXUSER_STATS_READ_RESULTS ) m_bufResults.Base(),
				&m_xOverlapped );
		}

		if ( ret == ERROR_IO_PENDING )
		{
			DevMsg( "CLeaderboardRequestQueue::OnSubmitQuery - query submitted...\n" );
			m_bQueryRunning = true;
		}
		else
		{
			DevWarning( "CLeaderboardRequestQueue::OnSubmitQuery - failed [code = %d, bytes = %d]\n", ret, dwBytes );
		}
	}
#endif
}

#ifdef _X360
void CLeaderboardRequestQueue::ProcessResults( XUSER_STATS_READ_RESULTS const *pResults )
{
	/*
	901D41D61DC61					// XUID %llx
	{
		survival_c5m2_park
		{
			:rank		=	923		// uint64
			:rows		=	999		// uint64
			:rating		=	600		// uint64
			besttime	=	600		// uint64
		}
		... more views ...
	}
	... more users ...
	*/
	DevMsg( "LeaderboardRequestQueue: ProcessResults ( %d views )\n", pResults->dwNumViews );
	for ( DWORD k = 0; k < pResults->dwNumViews; ++ k )
	{
		XUSER_STATS_VIEW const *pView = &pResults->pViews[k];
		Assert( pView );

		KeyValues *pViewDesc = FindViewDescription( pView->dwViewId );
		Assert( pViewDesc );
		if ( !pViewDesc )
		{
			Warning( "LeaderboardRequestQueue: ProcessResults has no view description for view %d!\n", pView->dwViewId );
			continue;
		}

		char const *szViewName = pViewDesc->GetString( ":name" );
		DevMsg( "    Processing view %d ( %s ), total rows = %d\n", pView->dwViewId, szViewName, pView->dwTotalViewRows );

		for ( DWORD r = 0; r < pView->dwNumRows; ++ r )
		{
			XUSER_STATS_ROW const *pRow = &pView->pRows[r];
			if ( !pRow->dwRank && !pRow->i64Rating )
			{
				DevMsg( "        Gamer %s (%llx) not in view\n", pRow->szGamertag, pRow->xuid );
				continue;	// gamer is not present in the leaderboard
			}
			DevMsg( "        Gamer %s (%llx) data loaded: rank=%d, rating=%lld\n",
				pRow->szGamertag, pRow->xuid, pRow->dwRank, pRow->i64Rating );

			// Gamer is present in the leaderboard and should be included in the results
			if ( !m_pFinishedRequest )
				m_pFinishedRequest = new KeyValues( "Leaderboard" );

			// Find or create the view for this gamer
			KeyValues *pUserInfo = m_pFinishedRequest->FindKey( CFmtStr( "%llx/%s", pRow->xuid, szViewName ), true );

			// Set his rank and rating
			pUserInfo->SetUint64( ":rank", pRow->dwRank );
			pUserInfo->SetUint64( ":rows", pView->dwTotalViewRows );
			pUserInfo->SetUint64( ":rating", pRow->i64Rating );

			if ( char const *szName = pViewDesc->GetString( ":rating/name", NULL ) )
			{
				if ( szName[0] )
					pUserInfo->SetUint64( szName, pRow->i64Rating );
			}

			// Process additional columns that were retrieved
			Assert( !pRow->dwNumColumns );
			// 			for ( DWORD c = 0; c < pRow->dwNumColumns; ++ c )
			// 			{
			// 				XUSER_STATS_COLUMN const *pCol = pRow->pColumns[ c ];
			// 				KeyValues *pColDesc = FindViewColumnDesc( pViewDesc, pCol->wColumnId )
			// 			}
		}
	}
	DevMsg( "LeaderboardRequestQueue: ProcessResults finished.\n" );
}
#elif !defined( NO_STEAM )
void CLeaderboardRequestQueue::ProcessResults( LeaderboardEntry_t const &lbe )
{
	/*
	901D41D61DC61					// XUID %llx
	{
		survival_c5m2_park
		{
			:rank		=	923		// uint64
			:rows		=	999		// uint64
			:rating		=	600		// uint64
			besttime	=	600		// uint64
		}
		... more views ...
	}
	... more users ...
	*/
	KeyValues *pViewDesc = m_pViewDescription;
	Assert( pViewDesc );
	if ( !pViewDesc )
	{
		Warning( "LeaderboardRequestQueue: ProcessResults has no view description for view!\n" );
		return;
	}

	char const *szViewName = pViewDesc->GetString( ":name" );
	DevMsg( "    Processing view %s\n", szViewName );

	DevMsg( "        Gamer data loaded: rank=%d, score=%d\n",
		lbe.m_nGlobalRank, lbe.m_nScore );

	// Gamer is present in the leaderboard and should be included in the results
	if ( !m_pFinishedRequest )
		m_pFinishedRequest = new KeyValues( "Leaderboard" );

	// Find or create the view for this gamer
	KeyValues *pUserInfo = m_pFinishedRequest->FindKey( CFmtStr( "%llx/%s",
		g_pPlayerManager->GetLocalPlayer( XBX_GetPrimaryUserId() )->GetXUID(), szViewName ), true );

	// Set user score
	pUserInfo->SetUint64( pViewDesc->GetString( ":score" ), lbe.m_nScore );

	DevMsg( "LeaderboardRequestQueue: ProcessResults finished.\n" );
}
#endif

#ifdef _X360

KeyValues * CLeaderboardRequestQueue::FindViewDescription( DWORD dwViewId )
{
	if ( !dwViewId )
		return NULL;

	for ( int k = 0; k < m_arrViewDescriptions.Count(); ++ k )
	{
		KeyValues *pDesc = m_arrViewDescriptions[k];
		if ( pDesc->GetInt( ":id" ) == ( int ) dwViewId )
			return pDesc;
	}

	return NULL;
}

#elif !defined( NO_STEAM )

void CLeaderboardRequestQueue::Steam_OnLeaderboardFindResult( LeaderboardFindResult_t *p, bool bError )
{
	if ( bError || !p->m_bLeaderboardFound )
	{
		DevMsg( "Steam leaderboard was not found.\n" );
		OnQueryFinished();
		return;
	}

	// Download the data
	SteamAPICall_t hCall = steamapicontext->SteamUserStats()->DownloadLeaderboardEntries( p->m_hSteamLeaderboard,
		k_ELeaderboardDataRequestGlobalAroundUser, 0, 0 );
	m_CallbackOnLeaderboardScoresDownloaded.Set( hCall, this, &CLeaderboardRequestQueue::Steam_OnLeaderboardScoresDownloaded );
}

void CLeaderboardRequestQueue::Steam_OnLeaderboardScoresDownloaded( LeaderboardScoresDownloaded_t *p, bool bError )
{
	// Fetch the data if found and no error
	LeaderboardEntry_t lbe;
	if ( !bError &&
		p->m_cEntryCount == 1 &&
		steamapicontext->SteamUserStats()->GetDownloadedLeaderboardEntry( p->m_hSteamLeaderboardEntries, 0, &lbe, NULL, 0 ) )
	{
		ProcessResults( lbe );
	}

	OnQueryFinished();
}

#endif