// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file m_perfstats.c
/// \brief Performance measurement tools.

#include "m_perfstats.h"
#include "v_video.h"
#include "i_video.h"
#include "d_netcmd.h"
#include "r_main.h"
#include "i_system.h"
#include "i_time.h"
#include "z_zone.h"
#include "p_local.h"
#include "g_game.h"

#ifdef HWRENDER
#include "hardware/hw_main.h"
#endif

struct perfstatcol;
struct perfstatrow;

typedef struct perfstatcol perfstatcol_t;
typedef struct perfstatrow perfstatrow_t;

struct perfstatcol {
	INT32 lores_x;
	INT32 hires_x;
	INT32 color;
	perfstatrow_t * rows;
};

struct perfstatrow {
	const char * lores_label;
	const char * hires_label;
	void       * value;
};

static precise_t ps_frametime = 0;

precise_t ps_tictime = 0;

precise_t ps_playerthink_time = 0;
precise_t ps_botticcmd_time = 0;
precise_t ps_thinkertime = 0;

precise_t ps_thlist_times[NUM_ACTIVETHINKERLISTS];
precise_t ps_acs_time = 0;

int ps_checkposition_calls = 0;

precise_t ps_lua_thinkframe_time = 0;
int ps_lua_mobjhooks = 0;

// dynamically allocated resizeable array for thinkframe hook stats
ps_hookinfo_t *thinkframe_hooks = NULL;
int thinkframe_hooks_length = 0;
int thinkframe_hooks_capacity = 16;

ps_botinfo_t ps_bots[MAXPLAYERS];

static INT32 draw_row;

void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
{
	if (!thinkframe_hooks)
	{
		// array needs to be initialized
		thinkframe_hooks = Z_Malloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
	}
	if (index >= thinkframe_hooks_capacity)
	{
		// array needs more space, realloc with double size
		thinkframe_hooks_capacity *= 2;
		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
	}
	thinkframe_hooks[index].time_taken = time_taken;
	memcpy(thinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
	// since the values are set sequentially from begin to end, the last call should leave
	// the correct value to this variable
	thinkframe_hooks_length = index + 1;
}

void PS_ResetBotInfo(void)
{
	memset(ps_bots, 0, sizeof(ps_bots));
	ps_botticcmd_time = 0;
}

static void PS_SetFrameTime(void)
{
	precise_t currenttime = I_GetPreciseTime();
	ps_frametime = currenttime - ps_prevframetime;
	ps_prevframetime = currenttime;
}

static boolean M_HighResolution(void)
{
	return (vid.width >= 640 && vid.height >= 400);
}

enum {
	PERF_TIME,
	PERF_COUNT,
};

static void M_DrawPerfString(perfstatcol_t *col, int type)
{
	const boolean hires = M_HighResolution();

	INT32 draw_flags = V_MONOSPACE | col->color;

	perfstatrow_t * row;

	int value;

	if (hires)
		draw_flags |= V_ALLOWLOWERCASE;

	for (row = col->rows; row->lores_label; ++row)
	{
		if (type == PERF_TIME)
			value = (*(precise_t *)row->value) / (I_GetPrecisePrecision() / 1000000);
		else
			value = *(int *)row->value;

		if (hires)
		{
			V_DrawSmallString(col->hires_x, draw_row, draw_flags,
					va("%s %d", row->hires_label, value));

			draw_row += 5;
		}
		else
		{
			V_DrawThinString(col->lores_x, draw_row, draw_flags,
					va("%s %d", row->lores_label, value));

			draw_row += 8;
		}
	}
}

static void M_DrawPerfTiming(perfstatcol_t *col)
{
	M_DrawPerfString(col, PERF_TIME);
}

static void M_DrawPerfCount(perfstatcol_t *col)
{
	M_DrawPerfString(col, PERF_COUNT);
}

static void M_DrawRenderStats(void)
{
	const boolean hires = M_HighResolution();

	const int half_row = hires ? 5 : 4;

	precise_t extrarendertime;

	perfstatrow_t frametime_row[] = {
		{"frmtime", "Frame time:    ", &ps_frametime},
		{0}
	};

	perfstatrow_t rendercalltime_row[] = {
		{"drwtime", "3d rendering:  ", &ps_rendercalltime},
		{0}
	};

	perfstatrow_t opengltime_row[] = {
		{"skybox ", "Skybox render: ", &ps_hw_skyboxtime},
		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
		{"nodesrt", "Drwnode sort:  ", &ps_hw_nodesorttime},
		{"nodedrw", "Drwnode render:", &ps_hw_nodedrawtime},
		{"sprsort", "Sprite sort:   ", &ps_hw_spritesorttime},
		{"sprdraw", "Sprite render: ", &ps_hw_spritedrawtime},
		{"other  ", "Other:         ", &extrarendertime},
		{0}
	};

	perfstatrow_t softwaretime_row[] = {
		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
		{"sprclip", "R_ClipSprites: ", &ps_sw_spritecliptime},
		{"portals", "Portals+Skybox:", &ps_sw_portaltime},
		{"planes ", "R_DrawPlanes:  ", &ps_sw_planetime},
		{"masked ", "R_DrawMasked:  ", &ps_sw_maskedtime},
		{"other  ", "Other:         ", &extrarendertime},
		{0}
	};

	perfstatrow_t uiswaptime_row[] = {
		{"ui     ", "UI render:     ", &ps_uitime},
		{"finupdt", "I_FinishUpdate:", &ps_swaptime},
		{0}
	};

	perfstatrow_t tictime_row[] = {
		{"logic  ", "Game logic:    ", &ps_tictime},
		{0}
	};

	perfstatrow_t rendercalls_row[] = {
		{"bspcall", "BSP calls:   ", &ps_numbspcalls},
		{"sprites", "Sprites:     ", &ps_numsprites},
		{"drwnode", "Drawnodes:   ", &ps_numdrawnodes},
		{"plyobjs", "Polyobjects: ", &ps_numpolyobjects},
		{0}
	};

	perfstatrow_t batchtime_row[] = {
		{"batsort", "Batch sort:  ", &ps_hw_batchsorttime},
		{"batdraw", "Batch render:", &ps_hw_batchdrawtime},
		{0}
	};

	perfstatrow_t batchcount_row[] = {
		{"polygon", "Polygons:  ", &ps_hw_numpolys},
		{"vertex ", "Vertices:  ", &ps_hw_numverts},
		{0}
	};

	perfstatrow_t batchcalls_row[] = {
		{"drwcall", "Draw calls:", &ps_hw_numcalls},
		{"shaders", "Shaders:   ", &ps_hw_numshaders},
		{"texture", "Textures:  ", &ps_hw_numtextures},
		{"polyflg", "Polyflags: ", &ps_hw_numpolyflags},
		{"colors ", "Colors:    ", &ps_hw_numcolors},
		{0}
	};

	perfstatcol_t      frametime_col =  {20,  20, V_YELLOWMAP,      frametime_row};
	perfstatcol_t rendercalltime_col =  {20,  20, V_YELLOWMAP, rendercalltime_row};

	perfstatcol_t     opengltime_col =  {24,  24, V_YELLOWMAP,     opengltime_row};
	perfstatcol_t   softwaretime_col =  {24,  24, V_YELLOWMAP,   softwaretime_row};

	perfstatcol_t     uiswaptime_col =  {20,  20, V_YELLOWMAP,     uiswaptime_row};
	perfstatcol_t        tictime_col =  {20,  20, V_GRAYMAP,          tictime_row};

	perfstatcol_t    rendercalls_col =  {90, 115, V_BLUEMAP,      rendercalls_row};

	perfstatcol_t      batchtime_col =  {90, 115, V_REDMAP,         batchtime_row};

	perfstatcol_t     batchcount_col = {155, 200, V_PURPLEMAP,     batchcount_row};
	perfstatcol_t     batchcalls_col = {220, 200, V_PURPLEMAP,     batchcalls_row};


	boolean rendering = G_GamestateUsesLevel();

	draw_row = 10;
	M_DrawPerfTiming(&frametime_col);

	if (rendering)
	{
		M_DrawPerfTiming(&rendercalltime_col);

		// Remember to update this calculation when adding more 3d rendering stats!
		extrarendertime = ps_rendercalltime - ps_bsptime;

#ifdef HWRENDER
		if (rendermode == render_opengl)
		{
			extrarendertime -=
				ps_hw_skyboxtime +
				ps_hw_nodesorttime +
				ps_hw_nodedrawtime +
				ps_hw_spritesorttime +
				ps_hw_spritedrawtime;

			if (cv_glbatching.value)
			{
				extrarendertime -=
					ps_hw_batchsorttime +
					ps_hw_batchdrawtime;
			}

			M_DrawPerfTiming(&opengltime_col);
		}
		else
#endif
		{
			extrarendertime -=
				ps_sw_spritecliptime +
				ps_sw_portaltime +
				ps_sw_planetime +
				ps_sw_maskedtime;

			M_DrawPerfTiming(&softwaretime_col);
		}
	}

	M_DrawPerfTiming(&uiswaptime_col);

	draw_row += half_row;
	M_DrawPerfTiming(&tictime_col);

	if (rendering)
	{
		draw_row = 10;
		M_DrawPerfCount(&rendercalls_col);

#ifdef HWRENDER
		if (rendermode == render_opengl && cv_glbatching.value)
		{
			draw_row += half_row;
			M_DrawPerfTiming(&batchtime_col);

			draw_row = 10;
			M_DrawPerfCount(&batchcount_col);

			if (hires)
				draw_row += half_row;
			else
				draw_row  = 10;

			M_DrawPerfCount(&batchcalls_col);
		}
#endif
	}
}

static void M_DrawTickStats(void)
{
	int i = 0;
	thinker_t *thinker;
	int thinkercount = 0;
	int polythcount = 0;
	int mainthcount = 0;
	int mobjcount = 0;
	int nothinkcount = 0;
	int scenerycount = 0;
	int regularcount = 0;
	int dynslopethcount = 0;
	int precipcount = 0;
	int removecount = 0;

	precise_t extratime =
		ps_tictime -
		ps_playerthink_time -
		ps_thinkertime -
		ps_lua_thinkframe_time -
		ps_acs_time;

	perfstatrow_t tictime_row[] = {
		{"logic  ", "Game logic:     ", &ps_tictime},
		{0}
	};

	perfstatrow_t thinker_time_row[] = {
		{"plrthnk", "P_PlayerThink:  ", &ps_playerthink_time},
		{"thnkers", "P_RunThinkers:  ", &ps_thinkertime},
		{0}
	};

	perfstatrow_t detailed_thinker_time_row[] = {
		{"plyobjs", "Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ]},
		{"main   ", "Main:           ", &ps_thlist_times[THINK_MAIN]},
		{"mobjs  ", "Mobjs:          ", &ps_thlist_times[THINK_MOBJ]},
		{"dynslop", "Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE]},
		{0}
	};

	perfstatrow_t bot_time_row[] = {
		{"botcmd ", "Bot logic:      ", &ps_botticcmd_time},
		{0}
	};

	perfstatrow_t extra_thinker_time_row[] = {
		{"lthinkf", "LUAh_ThinkFrame:", &ps_lua_thinkframe_time},
		{"acs    ", "ACS_Tick:       ", &ps_acs_time},
		{"other  ", "Other:          ", &extratime},
		{0}
	};

	perfstatrow_t thinkercount_row[] = {
		{"thnkers", "Thinkers:       ", &thinkercount},
		{0}
	};

	perfstatrow_t detailed_thinkercount_row[] = {
		{"plyobjs", "Polyobjects:    ", &polythcount},
		{"main   ", "Main:           ", &mainthcount},
		{"mobjs  ", "Mobjs:          ", &mobjcount},
		{0}
	};

	perfstatrow_t mobjthinkercount_row[] = {
		{"regular", "Regular:        ", &regularcount},
		{"scenery", "Scenery:        ", &scenerycount},
		{0}
	};

	perfstatrow_t nothinkcount_row[] = {
		{"nothink", "Nothink:        ", &nothinkcount},
		{0}
	};

	perfstatrow_t detailed_thinkercount_row2[] = {
		{"dynslop", "Dynamic slopes: ", &dynslopethcount},
		{"precip ", "Precipitation:  ", &precipcount},
		{"remove ", "Pending removal:", &removecount},
		{0}
	};

	perfstatrow_t misc_calls_row[] = {
		{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks},
		{"chkpos", "P_CheckPosition:", &ps_checkposition_calls},
		{0}
	};

	perfstatcol_t               tictime_col  =  {20,  20, V_YELLOWMAP,               tictime_row};
	perfstatcol_t          thinker_time_col  =  {24,  24, V_YELLOWMAP,          thinker_time_row};
	perfstatcol_t detailed_thinker_time_col  =  {28,  28, V_YELLOWMAP, detailed_thinker_time_row};
	perfstatcol_t              bot_time_col  =  {24,  24, V_YELLOWMAP,              bot_time_row};
	perfstatcol_t    extra_thinker_time_col  =  {24,  24, V_YELLOWMAP,    extra_thinker_time_row};

	perfstatcol_t          thinkercount_col  =  {90, 115, V_BLUEMAP,            thinkercount_row};
	perfstatcol_t detailed_thinkercount_col  =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row};
	perfstatcol_t      mobjthinkercount_col  =  {98, 123, V_BLUEMAP,        mobjthinkercount_row};
	perfstatcol_t          nothinkcount_col  =  {98, 123, V_BLUEMAP,            nothinkcount_row};
	perfstatcol_t detailed_thinkercount_col2 =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row2};
	perfstatcol_t            misc_calls_col  = {170, 216, V_PURPLEMAP,            misc_calls_row};

	if (G_GamestateUsesLevel())
	{
		for (i = 0; i < NUM_THINKERLISTS; i++)
		{
			for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
			{
				thinkercount++;
				if (thinker->function == (actionf_p1)P_RemoveThinkerDelayed)
					removecount++;
				else if (i == THINK_POLYOBJ)
					polythcount++;
				else if (i == THINK_MAIN)
					mainthcount++;
				else if (i == THINK_MOBJ)
				{
					if (thinker->function == (actionf_p1)P_MobjThinker)
					{
						mobj_t *mobj = (mobj_t*)thinker;
						mobjcount++;
						if (mobj->flags & MF_NOTHINK)
							nothinkcount++;
						else if (mobj->flags & MF_SCENERY)
							scenerycount++;
						else
							regularcount++;
					}
				}
				else if (i == THINK_DYNSLOPE)
					dynslopethcount++;
				else if (i == THINK_PRECIP)
					precipcount++;
			}
		}
	}

	draw_row = 10;
	M_DrawPerfTiming(&tictime_col);
	if (G_GamestateUsesLevel())
	{
		M_DrawPerfTiming(&thinker_time_col);
		M_DrawPerfTiming(&detailed_thinker_time_col);
		if (server)
			M_DrawPerfTiming(&bot_time_col);
		M_DrawPerfTiming(&extra_thinker_time_col);

		draw_row = 10;
		M_DrawPerfCount(&thinkercount_col);
		M_DrawPerfCount(&detailed_thinkercount_col);
		M_DrawPerfCount(&mobjthinkercount_col);

		if (nothinkcount)
			M_DrawPerfCount(&nothinkcount_col);

		M_DrawPerfCount(&detailed_thinkercount_col2);


		if (M_HighResolution())
		{
			V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");

			draw_row = 15;
		}
		else
		{
			draw_row = 10;
		}

		M_DrawPerfCount(&misc_calls_col);
	}
}

void M_DrawPerfStats(void)
{
	char s[363];

	PS_SetFrameTime();

	if (cv_perfstats.value == PS_RENDER) // rendering
	{
		M_DrawRenderStats();
	}
	else if (cv_perfstats.value == PS_LOGIC) // logic
	{
		M_DrawTickStats();
	}
	else if (cv_perfstats.value == PS_BOT) // bot ticcmd
	{
		if (!server)
		{
			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not applicable to clients");
		}
		else if (vid.width < 640 || vid.height < 400) // low resolution
		{
			// it's not gonna fit very well..
			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
		}
		else // high resolution
		{
			precise_t otherTime = 0;
			int i;

			// text writing position
			int x = 2;
			int y = 4;

			for (i = 0; i < MAXPLAYERS; i++)
			{
				if (ps_bots[i].isBot == false)
				{
					continue;
				}

				snprintf(s, sizeof s - 1, "Bot %d (%s):", i + 1, player_names[i]);
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);

				snprintf(s, sizeof s - 1, "%ld", (long)((ps_bots[i].total) / (I_GetPrecisePrecision() / 1000000)));
				V_DrawRightAlignedSmallString(x + 98, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);

				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}

				snprintf(s, sizeof s - 1, "Prediction:");
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);

				snprintf(s, sizeof s - 1, "%ld", (long)((ps_bots[i].prediction) / (I_GetPrecisePrecision() / 1000000)));
				V_DrawRightAlignedSmallString(x + 98, y, V_MONOSPACE | V_ALLOWLOWERCASE, s);

				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}

				snprintf(s, sizeof s - 1, "Nudge:");
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);

				snprintf(s, sizeof s - 1, "%ld", (long)((ps_bots[i].nudge) / (I_GetPrecisePrecision() / 1000000)));
				V_DrawRightAlignedSmallString(x + 98, y, V_MONOSPACE | V_ALLOWLOWERCASE, s);

				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}

				snprintf(s, sizeof s - 1, "Item:");
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);

				snprintf(s, sizeof s - 1, "%ld", (long)((ps_bots[i].item) / (I_GetPrecisePrecision() / 1000000)));
				V_DrawRightAlignedSmallString(x + 98, y, V_MONOSPACE | V_ALLOWLOWERCASE, s);

				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}

				snprintf(s, sizeof s - 1, "Other:");
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);

				otherTime = ps_bots[i].total - ps_bots[i].prediction - ps_bots[i].nudge - ps_bots[i].item;
				snprintf(s, sizeof s - 1, "%ld", (long)(otherTime / (I_GetPrecisePrecision() / 1000000)));
				V_DrawRightAlignedSmallString(x + 98, y, V_MONOSPACE | V_ALLOWLOWERCASE, s);

				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}

				// add an extra space
				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}
			}
		}
	}
	else if (cv_perfstats.value == PS_THINKFRAME) // lua thinkframe
	{
		if (!G_GamestateUsesLevel())
			return;
		if (vid.width < 640 || vid.height < 400) // low resolution
		{
			// it's not gonna fit very well..
			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
		}
		else // high resolution
		{
			int i;
			// text writing position
			int x = 2;
			int y = 4;
			UINT32 text_color;
			char tempbuffer[LUA_IDSIZE];
			char last_mod_name[LUA_IDSIZE];
			last_mod_name[0] = '\0';
			for (i = 0; i < thinkframe_hooks_length; i++)
			{
				char* str = thinkframe_hooks[i].short_src;
				char* tempstr = tempbuffer;
				int len = (int)strlen(str);
				char* str_ptr;
				if (fastcmp(".lua", str + len - 4))
				{
					str[len-4] = '\0'; // remove .lua at end
					len -= 4;
				}
				// we locate the wad/pk3 name in the string and compare it to
				// what we found on the previous iteration.
				// if the name has changed, print it out on the screen
				strcpy(tempstr, str);
				str_ptr = strrchr(tempstr, '|');
				if (str_ptr)
				{
					*str_ptr = '\0';
					str = str_ptr + 1; // this is the name of the hook without the mod file name
					str_ptr = strrchr(tempstr, PATHSEP[0]);
					if (str_ptr)
						tempstr = str_ptr + 1;
					// tempstr should now point to the mod name, (wad/pk3) possibly truncated
					if (!fastcmp(tempstr, last_mod_name))
					{
						strcpy(last_mod_name, tempstr);
						len = (int)strlen(tempstr);
						if (len > 25)
							tempstr += len - 25;
						snprintf(s, sizeof s - 1, "%s", tempstr);
						V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
						y += 4; // repeated code!
						if (y > 192)
						{
							y = 4;
							x += 106;
							if (x > 214)
								break;
						}
					}
					text_color = V_YELLOWMAP;
				}
				else
				{
					// probably a standalone lua file
					// cut off the folder if it's there
					str_ptr = strrchr(tempstr, PATHSEP[0]);
					if (str_ptr)
						str = str_ptr + 1;
					text_color = 0; // white
				}
				len = (int)strlen(str);
				if (len > 20)
					str += len - 20;
				snprintf(s, sizeof s - 1, "%20s: %ld", str, (long)((thinkframe_hooks[i].time_taken) / (I_GetPrecisePrecision() / 1000000)));
				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
				y += 4; // repeated code!
				if (y > 192)
				{
					y = 4;
					x += 106;
					if (x > 214)
						break;
				}
			}
		}
	}
}
