//////////////////////////////////////////////////////////////////////////////
//
// Asura_PS3_Stats.cpp
//
// Copyright (c) 2008 Rebellion. All rights reserved.
//
// Description:
//
//     PS3 classes to manage leaderboard & stats reading/writing
//
//////////////////////////////////////////////////////////////////////////////

// I N C L U D E S ///////////////////////////////////////////////////////////

#include "Core/Asura.h"

#ifdef ASURA_SUPPORT_LEADERBOARDS

// This file's header.
#include "Network/Online/Asura_PS3_Stats.h"

// Asura independent
#include "Core/Memory/Asura_Mem.h"
#include "Debugging/Asura_Assert.h"

// Asura PS3
#include "Network/Online/Asura_PS3_NP.h"

// C O N S T A N T S /////////////////////////////////////////////////////////
static const u_int uLEADERBOARD_TUS_DATA_SIZE = sizeof(SceNpScoreValue) + sizeof(SceNpScoreGameInfo);

// S T A T I C   A S S E R T S ///////////////////////////////////////////////
// leaderboard columns include ranked stat as column 0; we can have  up to 
// uASURA_PS3_MAX_STAT_COLS additional columns.
ASURA_STATIC_ASSERT(uASURA_LEADERBOARD_MAX_COLUMNS <= uASURA_PS3_MAX_STAT_COLS + 1);

// S T A T I C   C L A S S   D A T A /////////////////////////////////////////
int Asura_PS3_NP_Score::s_iTitleContext;
Asura_PS3_StatsPreGameReader* Asura_PS3_StatsPreGameReader::s_pxInstance;

// C L A S S   I M P L E M E N T A T I O N ///////////////////////////////////
void Asura_PS3_NP_Score::Initialise()
{
	// init score utility and create title context
	int iRet = sceNpScoreInit();
	Asura_Assertf(iRet == CELL_OK, ("sceNpScoreInit() failed 0x%x", iRet));

	iRet = sceNpScoreCreateTitleCtx( &Asura_PS3_NP::GetCommunicationID(), 
		&Asura_PS3_NP::GetCommunicationPassphrase(), &Asura_PS3_NP::GetNpID());
	Asura_Assertf(iRet > 0, ("sceNpScoreCreateTitleCtx() failed 0x%x", iRet));
	if (iRet > 0)
	{
		s_iTitleContext = iRet;
	}
}

void Asura_PS3_NP_Score::Shutdown()
{
	// shutdown and destroy title context
	int iRet;

	iRet = sceNpScoreDestroyTitleCtx(s_iTitleContext);
	Asura_Assertf(iRet == CELL_OK, ("sceNpScoreDestroyTitleCtx() failed 0x%x", iRet));

	iRet = sceNpScoreTerm();
	Asura_Assertf(iRet == CELL_OK, ("sceNpScoreTerm() failed 0x%x", iRet));
}



Asura_PS3_StatsTransactionManager::Asura_PS3_StatsTransactionManager()
: m_bError(false)
, m_iTransactionID(0)
, m_iResult(0)
{
}

Asura_PS3_StatsTransactionManager::~Asura_PS3_StatsTransactionManager()
{
	int iRet;
	// see if we must abort a pending transaction
	if (m_iTransactionID > 0)
	{
		iRet = sceNpScoreAbortTransaction(m_iTransactionID);
		Asura_Assertf(iRet == CELL_OK, ("sceNpScoreAbortTransaction() failed 0x%x", iRet));

		if (iRet == CELL_OK)
		{
			int iResult;
			iRet = sceNpScoreWaitAsync(m_iTransactionID, &iResult);
			Asura_Assertf(iRet >= 0, ("sceNpScoreWaitAsync() failed 0x%x", iRet));
		}
	}
}

bool Asura_PS3_StatsTransactionManager::Update()
{
	int iResult = 0;
	int iRet = sceNpScorePollAsync(m_iTransactionID, &iResult);
	Asura_Assertf(iRet >= 0, ("sceNpScorePollAsync() failed 0x%x", iRet));
	if (iRet == 0)
	{
		// finished. Record the result/error code. Calling code must check it.
		m_iResult = iResult;
		m_bError = (iResult < 0);
		iRet = sceNpScoreDestroyTransactionCtx(m_iTransactionID);
		Asura_Assertf(iRet >= 0, ("sceNpScoreDestroyTransactionCtx() failed 0x%x", iRet));
		m_iTransactionID = 0;
		return true;
	}
	else if (iRet == 1)
	{
		// still active
		return false;
	}

	m_bError = true;
	return true;
}

int Asura_PS3_StatsTransactionManager::CreateTransactionID()
{
	Asura_Assert(m_iTransactionID == 0, "already have transaction context");
	int iRet = sceNpScoreCreateTransactionCtx(Asura_PS3_NP_Score::GetTitleContext());
	Asura_Assertf(iRet > 0, ("sceNpScoreCreateTransactionCtx() failed 0x%x", iRet));
	if (iRet > 0)
	{
		m_iTransactionID = iRet;
		m_bError = false;
		m_iResult = EINPROGRESS;
	}
	return iRet;
}

Asura_PS3_StatsWriter::Asura_PS3_StatsWriter()
: m_uCurrentLeaderboardIndex(0)
, m_uNumLeaderboards(0)
, m_ePhase(Asura_PS3_StatsWriter::PHASE_NONE)
{
	const u_int uNumLeaderboards = Asura_Leaderboard_System::Project_GetNumLeaderboards();
	m_puBoardIndices = new u_int[uNumLeaderboards];
	m_pxScores = new SceNpScoreValue[uNumLeaderboards];
	m_pxColumns = new Asura_PS3_Stats_ExtraColumns[uNumLeaderboards];
	m_pTUSData = Asura_Allocate(uLEADERBOARD_TUS_DATA_SIZE * uNumLeaderboards);

	Asura_PS3_NP_Score::Initialise();
	Asura_PS3_NP_TUS::Initialise();
}

Asura_PS3_StatsWriter::~Asura_PS3_StatsWriter()
{
	Asura_PS3_NP_TUS::Shutdown();
	Asura_PS3_NP_Score::Shutdown();

	delete[] m_puBoardIndices;
	delete[] m_pxScores;
	delete[] m_pxColumns;
	Asura_Deallocate(m_pTUSData);
}

void Asura_PS3_StatsWriter::WriteStats( bool bDisconnectedEarly, u_int uMyGuid )
{
	m_uCurrentLeaderboardIndex = 0;

	// get project code to fill out the stats
	const u_int uNumLeaderboards = Asura_Leaderboard_System::Project_GetNumLeaderboards();
	u_int uNumLeaderboardsToWrite = 0;
	for (u_int u = 0; u < uNumLeaderboards; ++u)
	{
		const SceNpScoreBoardId uBoardId = Asura_PS3_Stats::Project_GetBoardID(u);
		if (uBoardId != uASURA_PS3_INVALID_BOARD_ID)
		{
			m_puBoardIndices[uNumLeaderboardsToWrite] = uBoardId;
			Asura_PS3_Stats::Project_GetMyStats( bDisconnectedEarly, uMyGuid, 
				u, m_pxScores[uNumLeaderboardsToWrite], m_pxColumns[uNumLeaderboardsToWrite] );
			++uNumLeaderboardsToWrite;
		}
	}
	
	Asura_Assert(uNumLeaderboardsToWrite == Asura_PS3_Stats::Project_GetNumValidBoardIDs(), 
		"unexpected number of leaderboards");

	Asura_Assert(uNumLeaderboardsToWrite, "no leaderboards to write - are they all invalid?");
	m_uNumLeaderboards = uNumLeaderboardsToWrite;
	if (uNumLeaderboardsToWrite)
	{
		m_ePhase = Asura_PS3_StatsWriter::PHASE_WRITING_TO_SCOREBOARDS;
		RecordScoresForCurrentIndex();
	}
}

void Asura_PS3_StatsWriter::RecordScoresForCurrentIndex()
{
	Asura_Assert(m_uCurrentLeaderboardIndex < m_uNumLeaderboards, "writing too many leaderboards");
	const SceNpScoreBoardId uBoardId = Asura_PS3_Stats::Project_GetBoardID(m_uCurrentLeaderboardIndex);
	Asura_Assert(uBoardId != uASURA_PS3_INVALID_BOARD_ID, "invalid board index");
	
	const int iTransactionID = m_xTransactionManager.CreateTransactionID();
	const int iRet = sceNpScoreRecordScoreAsync(iTransactionID, uBoardId, 
		m_pxScores[m_uCurrentLeaderboardIndex], NULL, 
		&m_pxColumns[m_uCurrentLeaderboardIndex].m_xGameInfo, 
		&m_xTempRank, 0, NULL);

	Asura_Assertf(iRet == CELL_OK, ("sceNpScoreRecordScoreAsync() failed 0x%x", iRet));
	if (iRet != CELL_OK)
	{
		m_xTransactionManager.SetError();
	}
}

void Asura_PS3_StatsWriter::RecordScoresInTUS()
{
	// copy stats data to TUS data
	Asura_Assert(m_uNumLeaderboards < Asura_Leaderboard_System::Project_GetNumLeaderboards(), "too many leaderboards");

	char* pcTUSDataWritePointer = reinterpret_cast<char*>(m_pTUSData);
	for (u_int uLBIndex = 0; uLBIndex < m_uNumLeaderboards; ++uLBIndex)
	{
		Asura_Mem::Copy(pcTUSDataWritePointer, &m_pxScores[uLBIndex], sizeof(SceNpScoreValue));
		pcTUSDataWritePointer += sizeof(SceNpScoreValue);
		Asura_Mem::Copy(pcTUSDataWritePointer, &m_pxColumns[uLBIndex].m_xGameInfo, sizeof(SceNpScoreGameInfo));
		pcTUSDataWritePointer += sizeof(SceNpScoreGameInfo);
	}
	Asura_Assert(pcTUSDataWritePointer - (m_uNumLeaderboards * uLEADERBOARD_TUS_DATA_SIZE) == m_pTUSData, "wrote unexpected amount of data");

	const u_int uDataSize = uLEADERBOARD_TUS_DATA_SIZE * m_uNumLeaderboards;
	const int iRet = m_xTransactionManagerTUS.SetDataAsync(Asura_PS3_NP::GetNpID(), 
		ASURA_PS3_TUS_DATA_SLOT_LEADERBOARDS, m_pTUSData, uDataSize);

	Asura_Assertf(iRet == CELL_OK, ("sceNpTusSetDataAsync() failed 0x%x", iRet));
	if (iRet != CELL_OK)
	{
		m_xTransactionManagerTUS.SetError();
	}
}

bool Asura_PS3_StatsWriter::Update()
{
	switch (m_ePhase)
	{
		case PHASE_WRITING_TO_SCOREBOARDS:
		{
			if (UpdateWritingToScoreboards())
			{
				// now write all the data to the TUS
				m_uCurrentLeaderboardIndex = 0;
				m_ePhase = PHASE_WRITING_TO_TUS;
				RecordScoresInTUS();
			}
			return false;
		}

		case PHASE_WRITING_TO_TUS:
		{
			return UpdateWritingToTUS();
		}
		default:
		{
			Asura_Assert(false, "unhandled phase");
		}
	}
	return true;
}

bool Asura_PS3_StatsWriter::UpdateWritingToScoreboards()
{
	bool bDone = m_xTransactionManager.Update();
	if (bDone)
	{
		Asura_Assertf(!m_xTransactionManager.HasErrorOccurred(), 
			("error writing stats: 0x%x", m_xTransactionManager.GetOperationResult()));
		++m_uCurrentLeaderboardIndex;
		if (m_uCurrentLeaderboardIndex < m_uNumLeaderboards)
		{
			// on to the next leaderboard
			RecordScoresForCurrentIndex();
			return false;
		}
	}
	return bDone;
}

bool Asura_PS3_StatsWriter::UpdateWritingToTUS()
{
	bool bDone = m_xTransactionManagerTUS.Update();
	if (bDone)
	{
		Asura_Assertf(!m_xTransactionManagerTUS.HasErrorOccurred(), 
			("error writing stats: 0x%x", m_xTransactionManagerTUS.GetOperationResult()));
	}
	return bDone;
}


Asura_PS3_StatsReader::Asura_PS3_StatsReader()
: m_uTotalEntries(0)
, m_uValidRows(0)
, m_bError(false)
{
	m_xLastSortDate.tick = 0;
}

Asura_PS3_StatsReader::~Asura_PS3_StatsReader()
{

}

// returns true on completion of a pending request.
bool Asura_PS3_StatsReader::Update()
{
	const bool bFinished = m_xTransactionManager.Update();
	if (bFinished)
	{
		if (m_xTransactionManager.HasErrorOccurred())
		{
			// check for allowed errors
			const u_int uError = static_cast<u_int>(m_xTransactionManager.GetOperationResult());
			if (uError == SCE_NP_COMMUNITY_SERVER_ERROR_GAME_RANKING_NOT_FOUND)
			{
				// not an error
				m_uValidRows = 0;
			}
			else
			{
				Asura_Assertf(false, ("error reading stats: 0x%x", uError));
				m_bError = true;
			}
		}
		else
		{
			const int iValidRows = m_xTransactionManager.GetOperationResult();
			Asura_Assert(iValidRows >= 0, "invalid number of rows");
			m_uValidRows = static_cast<u_int>(iValidRows);
		}
	}
	return bFinished;
}

bool Asura_PS3_StatsReader::FetchStats( u_int uBoardID, u_int uStart, u_int uNumRows )
{
	// docs for sceNpScoreGetRankingByRangeAsync specify max rows to fetch = 100
	ASURA_STATIC_ASSERT(uASURA_LEADERBOARD_MAX_ROWS_IN_MEMORY <= 100); 

	const int iTransactionID = m_xTransactionManager.CreateTransactionID();
	const int iRet = sceNpScoreGetRankingByRangeAsync(
		iTransactionID, 			// int32_t transId,
		uBoardID,					// SceNpScoreBoardId boardId,
		uStart + 1,						// SceNpScoreRankNumber startSerialRank,
		m_axStatsData,				// SceNpScoreRankData *rankArray,
		sizeof(m_axStatsData),		// size_t rankArraySize,
		NULL,						// SceNpScoreComment *commentArray,
		0,							// size_t commentArraySize,
		m_axExtraColumnData,		// SceNpScoreGameInfo *infoArray,
		sizeof(m_axExtraColumnData),// size_t infoArraySize,
		uASURA_LEADERBOARD_MAX_ROWS_IN_MEMORY, // size_t arrayNum,
		&m_xLastSortDate,			// CellRtcTick *lastSortDate,
		&m_uTotalEntries,			// SceNpScoreRankNumber *totalRecord,
		0,							// int32_t prio,
		NULL						// void *option
	);

	Asura_Assertf(iRet == CELL_OK, ("sceNpScoreRecordScoreAsync() failed 0x%x", iRet));
	m_bError = (iRet != CELL_OK);

	m_uValidRows = 0;

	return (iRet == CELL_OK);
}

bool Asura_PS3_StatsReader::FetchStatsForNpIds( u_int uBoardID, const SceNpId* pxNpIds, u_int uNumNpIds )
{
	return false;
}


//////////////////////////////////////////////////////////////////////////
// Asura_PS3_StatsPreGameReader
//////////////////////////////////////////////////////////////////////////
void Asura_PS3_StatsPreGameReader::Initialise()
{
	Asura_Assert(!s_pxInstance, "already initialised");
	s_pxInstance = new Asura_PS3_StatsPreGameReader();
	s_pxInstance->InitialiseInternal();
}

// Asura_PS3_StatsPreGameReader::Update() returns true on completion
bool Asura_PS3_StatsPreGameReader::Update()
{
	if (s_pxInstance)
	{
		return s_pxInstance->InternalUpdate();
	}
	return true;
}

void Asura_PS3_StatsPreGameReader::Shutdown()
{
	delete s_pxInstance;
	s_pxInstance = NULL;
}

bool Asura_PS3_StatsPreGameReader::HasErrorOccurred()
{
	if (s_pxInstance)
	{
		return s_pxInstance->HasErrorOccurredInternal();
	}
	Asura_Assert(false, "pre-game stats reader not initialised");
	return true;
}

const SceNpScoreValue* Asura_PS3_StatsPreGameReader::GetRankingStat(u_int uLeaderboard)
{
	if (s_pxInstance)
	{
		return s_pxInstance->GetRankingStatInternal(uLeaderboard);
	}
	Asura_Assert(false, "pre-game stats reader not initialised");
	return false;
}
const Asura_PS3_Stats_ExtraColumns* Asura_PS3_StatsPreGameReader::GetExtraColumnDataArray(u_int uLeaderboard)
{
	if (s_pxInstance)
	{
		return reinterpret_cast<const Asura_PS3_Stats_ExtraColumns*>
			(s_pxInstance->GetExtraColumnDataArrayInternal(uLeaderboard));
	}
	Asura_Assert(false, "pre-game stats reader not initialised");
	return false;
}

// private ctor/dtor
Asura_PS3_StatsPreGameReader::Asura_PS3_StatsPreGameReader()
: m_bDataValid(false)
, m_bError(false)
{
	const u_int uDataSize = uLEADERBOARD_TUS_DATA_SIZE * Asura_PS3_Stats::Project_GetNumValidBoardIDs();
	m_pcData = new char[uDataSize];
}

bool Asura_PS3_StatsPreGameReader::InitialiseInternal()
{
	Asura_PS3_NP_TUS::Initialise();

	const u_int uDataSize = uLEADERBOARD_TUS_DATA_SIZE * Asura_PS3_Stats::Project_GetNumValidBoardIDs();
	const int iRet = m_xTransactionManager.GetDataAsync(Asura_PS3_NP::GetNpID(), 
		ASURA_PS3_TUS_DATA_SLOT_LEADERBOARDS, m_pcData, uDataSize);

	Asura_Assertf(iRet == CELL_OK, ("sceNpTusGetDataAsync() failed 0x%x", iRet));
	if (iRet != CELL_OK)
	{
		m_xTransactionManager.SetError();
	}
	return iRet == CELL_OK;
}

Asura_PS3_StatsPreGameReader::~Asura_PS3_StatsPreGameReader()
{
	delete[] m_pcData;
}

bool Asura_PS3_StatsPreGameReader::InternalUpdate()
{
	bool bDone = m_xTransactionManager.Update();
	if (bDone)
	{
		const int iResult = m_xTransactionManager.GetOperationResult();
		if (m_xTransactionManager.HasErrorOccurred())
		{
			// one exception is that if the data hasn't been created yet, we will just assume 
			// it's all zeroes, so don't flag an error, but leave m_bDataValid as false.
			m_bError = (static_cast<u_int>(iResult) != SCE_NP_COMMUNITY_SERVER_ERROR_USER_STORAGE_DATA_NOT_FOUND);
			Asura_Assertf(!m_bError, 
				("error reading pre-game stats: 0x%x", iResult));
			Asura_Assert(!m_bDataValid, "shouldn't have been set");
		}
		else
		{
			m_bError = false;
		}
		if (!m_xTransactionManager.HasErrorOccurred())
		{
			const u_int uExpectedDataSize = uLEADERBOARD_TUS_DATA_SIZE * 
				Asura_PS3_Stats::Project_GetNumValidBoardIDs();
			if (m_xTransactionManager.IsDataValid(m_pcData, uExpectedDataSize))
			{
				m_bDataValid = true;
			}
		}
		Asura_PS3_NP_TUS::Shutdown();
	}
	return bDone;
}

const SceNpScoreValue* Asura_PS3_StatsPreGameReader::GetRankingStatInternal(u_int uLeaderboard) const
{
	Asura_Assert(!m_bError, "shouldn't try to get data when error occurred");
	if (m_bDataValid)
	{
		Asura_Assert(uLeaderboard < Asura_PS3_Stats::Project_GetNumValidBoardIDs(), "invalid leaderboard index");
		return reinterpret_cast<const SceNpScoreValue*>(m_pcData + (uLeaderboard * uLEADERBOARD_TUS_DATA_SIZE));
	}
	return NULL;
}

const SceNpScoreGameInfo* Asura_PS3_StatsPreGameReader::GetExtraColumnDataArrayInternal(u_int uLeaderboard) const
{
	Asura_Assert(!m_bError, "shouldn't try to get data when error occurred");
	if (m_bDataValid)
	{
		Asura_Assert(uLeaderboard < Asura_PS3_Stats::Project_GetNumValidBoardIDs(), "invalid leaderboard index");
		const char* pcBase = m_pcData + (uLeaderboard * uLEADERBOARD_TUS_DATA_SIZE);
		return reinterpret_cast<const SceNpScoreGameInfo*>(pcBase + sizeof(SceNpScoreValue));
	}
	return NULL;
}

#endif // #ifdef ASURA_SUPPORT_LEADERBOARDS
