///////////////////////////////////////////////////////////////////////////////
/// @file deProfile.cpp
///
/// @brief Real-time profiling class and manager
///
/// @author Assassin
///
/// This file is the intellectual property of Novus Delta, LLC.. Usage of the
/// contents of this file is subject to the Destiny3D Member License which
/// can be found at http://www.destiny3d.com.  Any other usage is prohibited.
///
/// This file is distributed "AS IS" without warranty of any kind.  Novus
/// Delta, LLC. does not guarantee the fitness of the contents of this file
/// for any particular purpose.
///
/// Copyright (C) 2001-2003 Novus Delta, LLC. All Rights Reserved.
///
/// <hr>
///                                 Change History
/// <hr>
///
/// @date Sept 2002
/// @author Assassin
/// @remarks Creation
///
/// @date May 2003
/// @author Assassin
/// @remarks Updates for enhanced feedback
///
///////////////////////////////////////////////////////////////////////////////

#include <cstdio>
#pragma warning (push)
#pragma warning (disable : 4201)
#include <windows.h> // for DllMain
#include <mmsystem.h> // timeGetTime
#pragma warning (pop)
#pragma comment (lib, "winmm")

#include "deProfile.hpp"
#include "deHash.hpp"
#include "deArray.hpp"
#include "deString.hpp"


#pragma warning (disable : 4251)

struct ProfileEntry
{
	u64 StartTicks;
	u64 FrameTicks;
	u64 TotalTicks;
	u64 AvgTicks;
	u32 IDNum;
	char Name[128];
	int NumCalls;
	float MinPercent, MaxPercent, AvgPercent;
	deBoolean IsOpen;
};

class deProfileManager : public IdeProfileManager
{
public:
	void		StartProfile(deProfile * profile);
	deBoolean	RecordInfo(deProfile * profile);
	long		GrabIDNum();
	long		EstimateFrequency();
	__int64		TimeSpentIn(char * ProfileName, long IDNum);
	__int64		TimeSpentIn_IDByName(char * Name);
	const char*	EndFrame(deBoolean OutputString);
	void		ResetStats();
	
	deProfileManager();
	~deProfileManager();

private:
	u64 m_StartTSC;
	u64 m_LastFrameTSC;
	DWORD m_StartTime;
	DWORD m_NumFrames;
	double m_AvgFPS;
	deTArray <ProfileEntry> m_Entries;
	deString m_StatisticString;
};

static deProfileManager* s_Manager;

static inline void Init_Entry(ProfileEntry * Entry)
{
	memset(Entry, 0, sizeof(ProfileEntry));
	Entry->MinPercent = -1;
	Entry->MaxPercent = -1;
}

IdeProfileManager * IdeProfile_GetTheManager()
{
	if (!s_Manager)
		s_Manager = new deProfileManager;
	return s_Manager;
}

void IdeProfile_ShutDown()
{
	delete s_Manager;
	s_Manager = NULL;
}

deProfileManager::deProfileManager()
{
	GrabTSC(&m_StartTSC);
	m_StartTime = timeGetTime();
	m_LastFrameTSC = m_StartTSC;
	m_NumFrames = 0;
	m_AvgFPS = 0;
}

deProfileManager::~deProfileManager()
{
}

long deProfileManager::GrabIDNum()
{
	deTArray<ProfileEntry>::iterator it = m_Entries.IncrementSizeQuick();
	Init_Entry(&*it);
	return m_Entries.size();
}

long deProfileManager::EstimateFrequency()
{
	DWORD time;
	u64 tsc;

	do {
		time = timeGetTime();
	} while (time - m_StartTime < 500);
	GrabTSC(&tsc);

	time -= m_StartTime;
	tsc -= m_StartTSC;
	tsc /= 1000;
	return (long)(tsc/time);
}

void deProfileManager::StartProfile(deProfile * profile)
{
	ProfileEntry *pEntry;
	if (profile->m_ProfileInfo.IDNum)
	{
		if (profile->m_ProfileInfo.IDNum <= 0 || profile->m_ProfileInfo.IDNum > m_Entries.Length())
			return;
		pEntry = &(m_Entries[profile->m_ProfileInfo.IDNum - 1]);
		if (pEntry->IDNum != profile->m_ProfileInfo.IDNum)
		{
			pEntry->IDNum = profile->m_ProfileInfo.IDNum;
			strncpy(pEntry->Name, profile->m_ProfileInfo.IDName, 127);
			pEntry->Name[127] = '\0';
		}
	}
	else
	{
		size_t i;
		pEntry = NULL;
		for (i = 0; i < m_Entries.Length(); ++i)
		{
			if (!strncmp(profile->m_ProfileInfo.IDName, m_Entries[i].Name, 127))
			{
				pEntry = &(m_Entries[i]);
			}
		}
		if (!pEntry)
		{
			deTArray<ProfileEntry>::iterator it = m_Entries.IncrementSizeQuick();
			pEntry = &*it;
			Init_Entry(pEntry);
			strncpy(it->Name, profile->m_ProfileInfo.IDName, 127);
			it->Name[127] = '\0';
		}
	}
	pEntry->IsOpen = deTRUE;
	pEntry->NumCalls += 1;
	GrabTSC(&(pEntry->StartTicks));
}

deBoolean deProfileManager::RecordInfo(deProfile * profile)
{
	u64 Diff, Now;

	GrabTSC(&Now);

	ProfileEntry *pEntry;
	size_t i;

	if (profile->m_ProfileInfo.IDNum)
	{
		if (profile->m_ProfileInfo.IDNum <= 0 || profile->m_ProfileInfo.IDNum > m_Entries.Length())
			DEBUGRETURN(deFALSE, "RecordInfo: numbered entry not found");
		pEntry = &(m_Entries[profile->m_ProfileInfo.IDNum - 1]);
	}
	else
	{
		size_t i;
		pEntry = NULL;
		for (i = 0; i < m_Entries.Length(); ++i)
		{
			if (!strncmp(profile->m_ProfileInfo.IDName, m_Entries[i].Name, 127))
			{
				pEntry = &(m_Entries[i]);
			}
		}
	}
	if (!pEntry)
		DEBUGRETURN(deFALSE, "RecordInfo: entry not found");

	if (pEntry->IsOpen)
	{
		u64 Greatest;
		Diff = Now - pEntry->StartTicks;
		pEntry->FrameTicks += Diff;
		pEntry->IsOpen = deFALSE;
		
		// find the immediate parent of this sample
		// (will be the sample that's still open with the latest start time)
		pEntry = NULL;
		Greatest = 0;
		for (i = 0; i < m_Entries.Length(); ++i)
		{
			if (m_Entries[i].IsOpen && m_Entries[i].StartTicks > Greatest)
			{
				Greatest = m_Entries[i].StartTicks;
				pEntry = &(m_Entries[i]);
			}
		}
		// subtract our time from the parent, so that cycles aren't counted multiple times
		if (pEntry)
			pEntry->FrameTicks -= Diff;
	}

	return deTRUE;
}

__int64	deProfileManager::TimeSpentIn(char * ProfileName, long IDNum)
{
	ProfileEntry *pEntry = NULL;

	if (IDNum)
	{
		pEntry = &(m_Entries[IDNum - 1]);
	}
	if (!pEntry)
	{
		size_t i;
		for (i = 0; i < m_Entries.Length(); ++i)
		{
			if (!strncmp(ProfileName, m_Entries[i].Name, 127))
			{
				pEntry = &(m_Entries[i]);
			}
		}
	}

	if (!pEntry)
		return 0;

	return pEntry->TotalTicks;
}

__int64 deProfileManager::TimeSpentIn_IDByName(char * ProfileName)
{
	size_t i;
	for (i = 0; i < m_Entries.Length(); ++i)
	{
		if (!strncmp(ProfileName, m_Entries[i].Name, 127))
		{
			return m_Entries[i].TotalTicks;
		}
	}
	return -1;
}

void deProfileManager::ResetStats()
{
	size_t i;
	__int64 Start;
	deBoolean Open;
	ProfileEntry* pEntry;
	for (i = 0; i < m_Entries.Length(); ++i)
	{
		pEntry = &m_Entries[i];
		Start = pEntry->StartTicks;
		Open = pEntry->IsOpen;
		Init_Entry(pEntry);
		pEntry->StartTicks = Start;
		pEntry->IsOpen = Open;
	}
	m_NumFrames = 0;
}

const char* deProfileManager::EndFrame(deBoolean OutputString)
{
	size_t i;
	u64 FrameTSC, Diff;
	ProfileEntry* pEntry;
	float Percent;
	double contribution;
	double FPS;
	char buffer[256];
	
	GrabTSC(&FrameTSC);
	Diff = FrameTSC - m_LastFrameTSC;
	m_LastFrameTSC = FrameTSC;
	
	m_NumFrames++;
	if (m_NumFrames > 100)
		contribution = 1.0 - 1.0/100; // makes sure that the current frame gets at least 1% contribution
	else
		contribution = (double(m_NumFrames-1))/(m_NumFrames);
	
	long freq = EstimateFrequency();
	FPS = (((double)freq)/Diff) * 1000000;
	m_AvgFPS = (m_AvgFPS * contribution) + (FPS * (1-contribution));
	if (OutputString)
	{
		sprintf(buffer," Avg | Min | Max |Last | MCyc Avg | Name (fps: %6.1f, avg: %6.1f)\n----------------------------------------\n", 
			FPS,
			m_AvgFPS);
		m_StatisticString = buffer;
	}
	
	for (i = 0; i < m_Entries.Length(); ++i)
	{
		pEntry = &(m_Entries[i]);
		if (pEntry->IsOpen)
		{
			pEntry->FrameTicks += FrameTSC - pEntry->StartTicks;
			pEntry->IsOpen = deFALSE;
		}
		pEntry->TotalTicks += pEntry->FrameTicks;
		Percent = (float)((double)pEntry->FrameTicks/(double)Diff);
		if (Percent < pEntry->MinPercent || pEntry->MinPercent < 0)
			pEntry->MinPercent = Percent;
		if (Percent > pEntry->MaxPercent)
			pEntry->MaxPercent = Percent;
		pEntry->AvgPercent = float((pEntry->AvgPercent * contribution) + (Percent * (1-contribution)));
		pEntry->AvgTicks = s64((pEntry->AvgTicks * contribution) + (pEntry->FrameTicks * (1-contribution)));
		if (OutputString)
		{
			_snprintf(buffer, 255, "%4.1f |%4.1f |%4.1f |%4.1f | %4.1e | %s\n", 
				pEntry->AvgPercent*100, 
				pEntry->MinPercent*100, 
				pEntry->MaxPercent*100, 
				Percent*100,
				pEntry->AvgTicks/1000000.0, 
				pEntry->Name);
			m_StatisticString << buffer;
		}
		pEntry->FrameTicks = 0;
		pEntry->NumCalls = 0;
	}
	m_StatisticString << "\n";
	return m_StatisticString.const_str();
}

