/*
cl_netgraph.c - Draw Net statistics (borrowed from Xash3D SDL code)
Copyright (C) 2016 Uncle Mike

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/

#include "common.h"
#include "client.h"
#include "kbutton.h"

#if XASH_LOW_MEMORY == 0
#define NET_TIMINGS			1024
#elif XASH_LOW_MEMORY == 1
#define NET_TIMINGS			256
#elif XASH_LOW_MEMORY == 2
#define NET_TIMINGS			64
#endif
#define NET_TIMINGS_MASK		(NET_TIMINGS - 1)
#define LATENCY_AVG_FRAC		0.5f
#define FRAMERATE_AVG_FRAC		0.5f
#define PACKETLOSS_AVG_FRAC		0.5f
#define PACKETCHOKE_AVG_FRAC		0.5f
#define NETGRAPH_LERP_HEIGHT		24
#define NETGRAPH_NET_COLORS		5
#define NUM_LATENCY_SAMPLES		8

CVAR_DEFINE_AUTO( net_graph, "0", FCVAR_ARCHIVE, "draw network usage graph" );
static CVAR_DEFINE_AUTO( net_graphpos, "1", FCVAR_ARCHIVE, "network usage graph position" );
static CVAR_DEFINE_AUTO( net_scale, "5", FCVAR_ARCHIVE, "network usage graph scale level" );
static CVAR_DEFINE_AUTO( net_graphwidth, "192", FCVAR_ARCHIVE, "network usage graph width" );
static CVAR_DEFINE_AUTO( net_graphheight, "64", FCVAR_ARCHIVE, "network usage graph height" );
static CVAR_DEFINE_AUTO( net_graphsolid, "1", FCVAR_ARCHIVE, "fill segments in network usage graph" );

static struct packet_latency_t
{
	int	latency;
	int	choked;
} netstat_packet_latency[NET_TIMINGS];

static struct cmdinfo_t
{
	float	cmd_lerp;
	int	size;
	qboolean	sent;
} netstat_cmdinfo[NET_TIMINGS];

static byte netcolors[NETGRAPH_NET_COLORS+NETGRAPH_LERP_HEIGHT][4] =
{
	{ 255, 0,   0,   255 },
	{ 0,   0,   255, 255 },
	{ 240, 127, 63,  255 },
	{ 255, 255, 0,   255 },
	{ 63,  255, 63,  150 }
	// other will be generated through NetGraph_InitColors()
};

static byte sendcolor[4] = { 88, 29, 130, 255 };
static byte holdcolor[4] = { 255, 0, 0, 200 };
static byte extrap_base_color[4] = { 255, 255, 255, 255 };
static netbandwidthgraph_t	netstat_graph[NET_TIMINGS];
static float		packet_loss;
static float		packet_choke;
static float		framerate = 0.0;
static int		maxmsgbytes = 0;

/*
==========
NetGraph_DrawRect

NetGraph_FillRGBA shortcut
==========
*/
static void NetGraph_DrawRect( wrect_t *rect, byte colors[4] )
{
	ref.dllFuncs.Color4ub( colors[0], colors[1], colors[2], colors[3] );	// color for this quad

	ref.dllFuncs.Vertex3f( rect->left, rect->top, 0 );
	ref.dllFuncs.Vertex3f( rect->left + rect->right, rect->top, 0 );
	ref.dllFuncs.Vertex3f( rect->left + rect->right, rect->top + rect->bottom, 0 );
	ref.dllFuncs.Vertex3f( rect->left, rect->top + rect->bottom, 0 );
}

/*
==========
NetGraph_AtEdge

edge detect
==========
*/
static qboolean NetGraph_AtEdge( int x, int width )
{
	if( x > 3 )
	{
		if( x >= width - 4 )
			return true;
		return false;
	}
	return true;
}

/*
==========
NetGraph_InitColors

init netgraph colors
==========
*/
static void NetGraph_InitColors( void )
{
	byte	mincolor[2][3];
	byte	maxcolor[2][3];
	float	dc[2][3];
	int	i, hfrac;
	float	f;

	mincolor[0][0] = 63;
	mincolor[0][1] = 0;
	mincolor[0][2] = 100;

	maxcolor[0][0] = 0;
	maxcolor[0][1] = 63;
	maxcolor[0][2] = 255;

	mincolor[1][0] = 255;
	mincolor[1][1] = 127;
	mincolor[1][2] = 0;

	maxcolor[1][0] = 250;
	maxcolor[1][1] = 0;
	maxcolor[1][2] = 0;

	for( i = 0; i < 3; i++ )
	{
		dc[0][i] = (float)(maxcolor[0][i] - mincolor[0][i]);
		dc[1][i] = (float)(maxcolor[1][i] - mincolor[1][i]);
	}

	hfrac = NETGRAPH_LERP_HEIGHT / 3;

	for( i = 0; i < NETGRAPH_LERP_HEIGHT; i++ )
	{
		if( i < hfrac )
		{
			f = (float)i / (float)hfrac;
			VectorMA( mincolor[0], f, dc[0], netcolors[NETGRAPH_NET_COLORS + i] );
		}
		else
		{
			f = (float)(i - hfrac) / (float)(NETGRAPH_LERP_HEIGHT - hfrac );
			VectorMA( mincolor[1], f, dc[1], netcolors[NETGRAPH_NET_COLORS + i] );
		}
		netcolors[NETGRAPH_NET_COLORS + i][3] = 255;
	}
}

/*
==========
NetGraph_GetFrameData

get frame data info, like chokes, packet losses, also update graph, packet and cmdinfo
==========
*/
static void NetGraph_GetFrameData( float *latency, int *latency_count )
{
	int		i, choke_count = 0, loss_count = 0;
	double		newtime = Sys_DoubleTime();
	static double	nexttime = 0;
	float		loss, choke;

	*latency_count = 0;
	*latency = 0.0f;

	if( newtime >= nexttime )
	{
		// soft fading of net peak usage
		maxmsgbytes = Q_max( 0, maxmsgbytes - 50 );
		nexttime = newtime + 0.05;
	}

	for( i = cls.netchan.incoming_sequence - CL_UPDATE_BACKUP + 1; i <= cls.netchan.incoming_sequence; i++ )
	{
		frame_t *f = cl.frames + ( i & CL_UPDATE_MASK );
		struct packet_latency_t *p = netstat_packet_latency + ( i & NET_TIMINGS_MASK );
		netbandwidthgraph_t *g = netstat_graph + ( i & NET_TIMINGS_MASK );

		p->choked = f->choked;
		if( p->choked ) choke_count++;

		if( !f->valid )
		{
			p->latency = 9998; // broken delta
		}
		else if( f->receivedtime == -1.0 )
		{
			p->latency = 9999; // dropped
			loss_count++;
		}
		else if( f->receivedtime == -3.0 )
		{
			p->latency = 9997; // skipped
		}
		else
		{
			int frame_latency = Q_min( 1.0f, f->latency );
			p->latency = (( frame_latency + 0.1f ) / 1.1f ) * ( net_graphheight.value - NETGRAPH_LERP_HEIGHT - 2 );

			if( i > cls.netchan.incoming_sequence - NUM_LATENCY_SAMPLES )
			{
				(*latency) += 1000.0f * f->latency;
				(*latency_count)++;
			}
		}

		memcpy( g, &f->graphdata, sizeof( netbandwidthgraph_t ));

		if( g->msgbytes > maxmsgbytes )
			maxmsgbytes = g->msgbytes;
	}

	if( maxmsgbytes > 1000 )
		maxmsgbytes = 1000;

	for( i = cls.netchan.outgoing_sequence - CL_UPDATE_BACKUP + 1; i <= cls.netchan.outgoing_sequence; i++ )
	{
		netstat_cmdinfo[i & NET_TIMINGS_MASK].cmd_lerp = cl.commands[i & CL_UPDATE_MASK].frame_lerp;
		netstat_cmdinfo[i & NET_TIMINGS_MASK].sent = cl.commands[i & CL_UPDATE_MASK].heldback ? false : true;
		netstat_cmdinfo[i & NET_TIMINGS_MASK].size = cl.commands[i & CL_UPDATE_MASK].sendsize;
	}

	// packet loss
	loss = 100.0f * (float)loss_count / CL_UPDATE_BACKUP;
	packet_loss = PACKETLOSS_AVG_FRAC * packet_loss + ( 1.0f - PACKETLOSS_AVG_FRAC ) * loss;

	// packet choke
	choke = 100.0f * (float)choke_count / CL_UPDATE_BACKUP;
	packet_choke = PACKETCHOKE_AVG_FRAC * packet_choke + ( 1.0f - PACKETCHOKE_AVG_FRAC ) * choke;
}

/*
===========
NetGraph_DrawTimes

===========
*/
static void NetGraph_DrawTimes( wrect_t rect, int x, int w )
{
	int	i, j, extrap_point = NETGRAPH_LERP_HEIGHT / 3, a, h;
	rgba_t	colors = { 0.9 * 255, 0.9 * 255, 0.7 * 255, 255 };
	wrect_t	fill;

	for( a = 0; a < w; a++ )
	{
		i = ( cls.netchan.outgoing_sequence - a ) & NET_TIMINGS_MASK;
		h = Q_min(( netstat_cmdinfo[i].cmd_lerp / 3.0f ) * NETGRAPH_LERP_HEIGHT, net_graphheight.value * 0.7f);

		fill.left = x + w - a - 1;
		fill.right = fill.bottom = 1;
		fill.top = rect.top + rect.bottom - 4;

		if( h >= extrap_point )
		{
			int	start = 0;

			h -= extrap_point;
			fill.top -= extrap_point;

			if( !net_graphsolid.value )
			{
				fill.top -= (h - 1);
				start = (h - 1);
			}

			for( j = start; j < h; j++ )
			{
				int color = NETGRAPH_NET_COLORS + j + extrap_point;
				color = Q_min( color, ARRAYSIZE( netcolors ) - 1 );

				NetGraph_DrawRect( &fill, netcolors[color] );
				fill.top--;
			}
		}
		else
		{
			int	oldh = h;

			fill.top -= h;
			h = extrap_point - h;

			if( !net_graphsolid.value )
				h = 1;

			for( j = 0; j < h; j++ )
			{
				int color = NETGRAPH_NET_COLORS + j + oldh;
				color = Q_min( color, ARRAYSIZE( netcolors ) - 1 );

				NetGraph_DrawRect( &fill, netcolors[color] );
				fill.top--;
			}
		}

		fill.top = rect.top + rect.bottom - 4 - extrap_point;

		if( NetGraph_AtEdge( a, w ))
			NetGraph_DrawRect( &fill, extrap_base_color );

		fill.top = rect.top + rect.bottom - 4;

		if( netstat_cmdinfo[i].sent )
			NetGraph_DrawRect( &fill, sendcolor );
		else NetGraph_DrawRect( &fill, holdcolor );
	}
}

//left = x
//right = width
//top = y
//bottom = height

/*
===========
NetGraph_DrawHatches

===========
*/
static void NetGraph_DrawHatches( int x, int y )
{
	int	ystep = (int)( 10.0f / net_scale.value );
	byte	colorminor[4] = { 0, 63, 63, 200 };
	byte	color[4] = { 0, 200, 0, 255 };
	wrect_t	hatch = { x, 4, y, 1 };
	int	starty;

	ystep = Q_max( ystep, 1 );

	for( starty = hatch.top; hatch.top > 0 && ((starty - hatch.top) * net_scale.value < (maxmsgbytes + 50)); hatch.top -= ystep )
	{
		if(!((int)((starty - hatch.top) * net_scale.value ) % 50 ))
		{
			NetGraph_DrawRect( &hatch, color );
		}
		else if( ystep > 5 )
		{
			NetGraph_DrawRect( &hatch, colorminor );
		}
	}
}

/*
===========
NetGraph_DrawTextFields

===========
*/
static void NetGraph_DrawTextFields( int x, int y, int w, wrect_t rect, int count, float avg, int packet_loss, int packet_choke, int graphtype )
{
	static int	lastout;
	cl_font_t *font = Con_GetFont( 0 );
	rgba_t		colors = { 0.9 * 255, 0.9 * 255, 0.7 * 255, 255 };
	int		ptx = Q_max( x + w - NETGRAPH_LERP_HEIGHT - 1, 1 );
	int		pty = Q_max( rect.top + rect.bottom - NETGRAPH_LERP_HEIGHT - 3, 1 );
	int		out, i = ( cls.netchan.outgoing_sequence - 1 ) & NET_TIMINGS_MASK;
	int		j = cls.netchan.incoming_sequence & NET_TIMINGS_MASK;
	int		last_y = y - net_graphheight.value;

	if( count > 0 )
	{
		avg = avg / (float)( count - ( host.frametime * FRAMERATE_AVG_FRAC ));

		if( cl_updaterate.value > 0.0f )
			avg -= 1000.0f / cl_updaterate.value;

		// can't be below zero
		avg = Q_max( 0.0f, avg );
	}
	else avg = 0.0;

	// move rolling average
	framerate = FRAMERATE_AVG_FRAC * host.frametime + ( 1.0f - FRAMERATE_AVG_FRAC ) * framerate;

	ref.dllFuncs.GL_SetRenderMode( font->rendermode );

	if( framerate > 0.0f )
	{
		y -= net_graphheight.value;

		CL_DrawStringf( font, x, y, colors, FONT_DRAW_NORENDERMODE, "%.1f fps" , 1.0f / framerate);

		if( avg > 1.0f )
			CL_DrawStringf( font, x + 75, y, colors, FONT_DRAW_NORENDERMODE, "%i ms" , (int)avg );

		y += 15;

		out = netstat_cmdinfo[i].size;
		if( !out ) out = lastout;
		else lastout = out;

		CL_DrawStringf( font, x, y, colors, FONT_DRAW_NORENDERMODE,
			"in :  %i %.2f kb/s", netstat_graph[j].msgbytes, cls.netchan.flow[FLOW_INCOMING].avgkbytespersec );
		y += 15;

		CL_DrawStringf( font, x, y, colors, FONT_DRAW_NORENDERMODE,
			"out:  %i %.2f kb/s", out, cls.netchan.flow[FLOW_OUTGOING].avgkbytespersec );
		y += 15;

		if( graphtype > 2 )
		{
			int	loss = (int)(( packet_loss + PACKETLOSS_AVG_FRAC ) - 0.01f );
			int	choke = (int)(( packet_choke + PACKETCHOKE_AVG_FRAC ) - 0.01f );

			CL_DrawStringf( font, x, y, colors, FONT_DRAW_NORENDERMODE, "loss: %i choke: %i", loss, choke );
		}
	}

	if( graphtype < 3 )
		CL_DrawStringf( font, ptx, pty, colors, FONT_DRAW_NORENDERMODE, "%i/s", (int)cl_cmdrate.value );

	CL_DrawStringf( font, ptx, last_y, colors, FONT_DRAW_NORENDERMODE, "%i/s" , (int)cl_updaterate.value );
}

/*
===========
NetGraph_DrawDataSegment

===========
*/
static int NetGraph_DrawDataSegment( wrect_t *fill, int bytes, byte r, byte g, byte b, byte a )
{
	float	h = bytes / net_scale.value;
	byte	colors[4] = { r, g, b, a };

	fill->top -= (int)h;

	if( net_graphsolid.value )
		fill->bottom = (int)h;
	else fill->bottom = 1;

	if( fill->top > 1 )
	{
		NetGraph_DrawRect( fill, colors );
		return 1;
	}

	return 0;
}

/*
===========
NetGraph_ColorForHeight

color based on packet latency
===========
*/
static void NetGraph_ColorForHeight( struct packet_latency_t *packet, byte color[4], int *ping )
{
	switch( packet->latency )
	{
	case 9999:
		memcpy( color, netcolors[0], sizeof( byte ) * 4 ); // dropped
		*ping = 0;
		break;
	case 9998:
		memcpy( color, netcolors[1], sizeof( byte ) * 4 ); // invalid
		*ping = 0;
		break;
	case 9997:
		memcpy( color, netcolors[2], sizeof( byte ) * 4 ); // skipped
		*ping = 0;
		break;
	default:
		*ping = 1;
		if( packet->choked )
		{
			memcpy( color, netcolors[3], sizeof( byte ) * 4 );
		}
		else
		{
			memcpy( color, netcolors[4], sizeof( byte ) * 4 );
		}
	}
}

/*
===========
NetGraph_DrawDataUsage

===========
*/
static void NetGraph_DrawDataUsage( int x, int y, int w, int graphtype )
{
	int	a, i, h, lastvalidh = 0, ping;
	int	pingheight = net_graphheight.value - NETGRAPH_LERP_HEIGHT - 2;
	wrect_t	fill = { 0 };
	byte	color[4];

	for( a = 0; a < w; a++ )
	{
		i = (cls.netchan.incoming_sequence - a) & NET_TIMINGS_MASK;
		h = netstat_packet_latency[i].latency;

		NetGraph_ColorForHeight( &netstat_packet_latency[i], color, &ping );

		if( !ping ) h = lastvalidh;
		else lastvalidh = h;

		if( h > pingheight )
			h = pingheight;

		fill.left = x + w - a - 1;
		fill.top = y - h;
		fill.right = 1;
		fill.bottom = ping ? 1: h;

		if( !ping )
		{
			if( fill.bottom > 3 )
			{
				fill.bottom = 2;
				NetGraph_DrawRect( &fill, color );
				fill.top += fill.bottom - 2;
				NetGraph_DrawRect( &fill, color );
			}
			else
			{
				NetGraph_DrawRect( &fill, color );
			}
		}
		else
		{
			NetGraph_DrawRect( &fill, color );
		}

		fill.top = y;
		fill.bottom = 1;

		color[0] = 0;
		color[1] = 255;
		color[2] = 0;
		color[3] = 160;

		if( NetGraph_AtEdge( a, w ))
			NetGraph_DrawRect( &fill, color );

		if( graphtype < 2 )
			continue;

		color[0] = color[1] = color[2] = color[3] = 255;
		fill.top = y - net_graphheight.value - 1;
		fill.bottom = 1;

		if( NetGraph_AtEdge( a, w ))
			NetGraph_DrawRect( &fill, color );

		fill.top -= 1;

		if( netstat_packet_latency[i].latency > 9995 )
			continue; // skip invalid

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].client, 255, 0, 0, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].players, 255, 255, 0, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].entities, 255, 0, 255, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].tentities, 0, 0, 255, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].sound, 0, 255, 0, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].event, 0, 255, 255, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].usr, 200, 200, 200, 128 ))
			continue;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].voicebytes, 255, 255, 255, 255 ))
			continue;

		fill.top = y - net_graphheight.value - 1;
		fill.bottom = 1;
		fill.top -= 2;

		if( !NetGraph_DrawDataSegment( &fill, netstat_graph[i].msgbytes, 240, 240, 240, 128 ))
			continue;
	}

	if( graphtype >= 2 )
		NetGraph_DrawHatches( x, y - net_graphheight.value - 1 );
}

/*
===========
NetGraph_GetScreenPos

===========
*/
static void NetGraph_GetScreenPos( wrect_t *rect, int *w, int *x, int *y )
{
	rect->left = rect->top = 0;
	rect->right = refState.width;
	rect->bottom = refState.height;

	*w = Q_min( NET_TIMINGS, net_graphwidth.value );
	if( rect->right < *w + 10 )
		*w = rect->right - 10;

	// detect x and y position
	switch( (int)net_graphpos.value )
	{
	case 1: // right sided
		*x = rect->left + rect->right - 5 - *w;
		break;
	case 2: // center
		*x = ( rect->left + ( rect->right - 10 - *w )) / 2;
		break;
	default: // left sided
		*x = rect->left + 5;
		break;
	}

	*y = rect->bottom + rect->top - NETGRAPH_LERP_HEIGHT - 5;
}

/*
===========
SCR_DrawNetGraph

===========
*/
void SCR_DrawNetGraph( void )
{
	wrect_t	rect;
	float	avg_ping;
	int	ping_count;
	int	w, x, y;
	kbutton_t *in_graph;
	int   graphtype;

	if( !host.allow_console )
		return;

	if( cls.state != ca_active )
		return;

	in_graph = clgame.dllFuncs.KB_Find( "in_graph" );

	if( in_graph->state & 1 )
	{
		graphtype = 2;
	}
	else if( net_graph.value != 0.0f )
	{
		graphtype = (int)net_graph.value;
	}
	else
	{
		return;
	}

	if( net_scale.value <= 0 )
		Cvar_SetValue( "net_scale", 0.1f );

	NetGraph_GetScreenPos( &rect, &w, &x, &y );

	NetGraph_GetFrameData( &avg_ping, &ping_count );

	NetGraph_DrawTextFields( x, y, w, rect, ping_count, avg_ping, packet_loss, packet_choke, graphtype );

	if( graphtype < 3 )
	{
		ref.dllFuncs.GL_SetRenderMode( kRenderTransColor );
		ref.dllFuncs.GL_Bind( XASH_TEXTURE0, R_GetBuiltinTexture( REF_WHITE_TEXTURE ) );
		ref.dllFuncs.Begin( TRI_QUADS ); // draw all the fills as a long solid sequence of quads for speedup reasons

		// NOTE: fill colors without texture at this point
		NetGraph_DrawDataUsage( x, y, w, graphtype );
		NetGraph_DrawTimes( rect, x, w );

		ref.dllFuncs.End();
		ref.dllFuncs.Color4ub( 255, 255, 255, 255 );
		ref.dllFuncs.GL_SetRenderMode( kRenderNormal );
	}
}

void CL_InitNetgraph( void )
{
	Cvar_RegisterVariable( &net_graph );
	Cvar_RegisterVariable( &net_graphpos );
	Cvar_RegisterVariable( &net_scale );
	Cvar_RegisterVariable( &net_graphwidth );
	Cvar_RegisterVariable( &net_graphheight );
	Cvar_RegisterVariable( &net_graphsolid );
	packet_loss = packet_choke = 0.0;

	NetGraph_InitColors();
}
