//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================

#include <stdio.h>

#include "GraphPanel.h"

#include <vgui/ISystem.h>
#include <vgui/ISurface.h>
#include <vgui/IVGui.h>
#include <vgui/IScheme.h>
#include <vgui/ILocalize.h>
#include <KeyValues.h>

#include <vgui_controls/Label.h>
#include <vgui_controls/TextEntry.h>
#include <vgui_controls/Button.h>
#include <vgui_controls/ToggleButton.h>
#include <vgui_controls/RadioButton.h>
#include <vgui_controls/ListPanel.h>
#include <vgui_controls/ComboBox.h>
#include <vgui_controls/PHandle.h>
#include <vgui_controls/PropertySheet.h>
#include <vgui_controls/CheckButton.h>

#define max(a,b)            (((a) > (b)) ? (a) : (b))

#define STATS_UPDATE_RATE	5.0f


// colors for the various graph lines+controls
Color CGraphPanel::CGraphsImage::CPUColor= Color(0,255,0,255); // green
Color CGraphPanel::CGraphsImage::FPSColor= Color(255,0,0,255); // red
Color CGraphPanel::CGraphsImage::NetInColor = Color(255,255,0,255); // yellow
Color CGraphPanel::CGraphsImage::NetOutColor = Color(0,255,255,255); // light blue
Color CGraphPanel::CGraphsImage::PlayersColor = Color(255,0,255,255); // purple
Color CGraphPanel::CGraphsImage::PingColor = Color(0,0,0,255); // black
//Color CGraphPanel::CGraphsImage::lineColor = Color(76,88,68,255);

using namespace vgui;

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CGraphPanel::CGraphPanel(vgui::Panel *parent, const char *name) : PropertyPage(parent, name)
{
	SetMinimumSize(300,200);

	m_pGraphsPanel = new ImagePanel(this,"Graphs");
	m_pGraphs = new CGraphsImage();
	m_pGraphsPanel->SetImage(m_pGraphs);

	m_pInButton = new CheckButton(this,"InCheck","#Graph_In");
	m_pOutButton = new CheckButton(this,"OutCheck","#Graph_Out");
	m_pFPSButton = new CheckButton(this,"FPSCheck","#Graph_FPS");
	m_pCPUButton = new CheckButton(this,"CPUCheck","#Graph_CPU");
	m_pPINGButton = new CheckButton(this,"PingCheck","#Graph_Ping");
	m_pPlayerButton = new CheckButton(this,"PlayersCheck","#Graph_Players");

	m_pTimeCombo = new ComboBox(this, "TimeCombo",3,false);
	m_pTimeCombo->AddItem("#Graph_Minutes", NULL);
	int defaultItem = m_pTimeCombo->AddItem("#Graph_Hours", NULL);
	m_pTimeCombo->AddItem("#Graph_Day", NULL);
	m_pTimeCombo->ActivateItem(defaultItem);

	m_pVertCombo = new ComboBox(this, "VertCombo",6,false);
	m_pVertCombo->AddItem("#Graph_In", NULL);
	m_pVertCombo->AddItem("#Graph_Out", NULL);
	m_pVertCombo->AddItem("#Graph_FPS", NULL);
	defaultItem = m_pVertCombo->AddItem("#Graph_CPU", NULL);
	m_pVertCombo->AddItem("#Graph_Ping", NULL);
	m_pVertCombo->AddItem("#Graph_Players", NULL);
	m_pVertCombo->ActivateItem(defaultItem);

	// now setup defaults
	m_pCPUButton->SetSelected(true);
	m_pInButton->SetSelected(false);
	m_pOutButton->SetSelected(false);
	m_pFPSButton->SetSelected(false);
	m_pPINGButton->SetSelected(false);

	LoadControlSettings("Admin/GraphPanel.res", "PLATFORM");

	int w,h;
	m_pGraphsPanel->GetSize(w,h);
	m_pGraphs->SaveSize(w,h);
	m_pGraphs->SetDraw(m_pCPUButton->IsSelected(),m_pFPSButton->IsSelected(),
	m_pInButton->IsSelected(),m_pOutButton->IsSelected(),m_pPINGButton->IsSelected(),m_pPlayerButton->IsSelected());

	m_pPINGButton->SetFgColor(m_pGraphs->GetPingColor());
	m_pCPUButton->SetFgColor(m_pGraphs->GetCPUColor());
	m_pFPSButton->SetFgColor(m_pGraphs->GetFPSColor());
	m_pInButton->SetFgColor(m_pGraphs->GetInColor());
	m_pOutButton->SetFgColor(m_pGraphs->GetOutColor());
	m_pPlayerButton->SetFgColor(m_pGraphs->GetPlayersColor());

	ivgui()->AddTickSignal(GetVPanel());
	m_flNextStatsUpdateTime = 0;
}

//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CGraphPanel::~CGraphPanel()
{

}

void CGraphPanel::ApplySchemeSettings( IScheme *pScheme )
{
	BaseClass::ApplySchemeSettings( pScheme );
	m_pGraphsPanel->SetBorder(	pScheme->GetBorder("ButtonDepressedBorder"));
	m_pGraphs->SetBgColor(GetSchemeColor("WindowBG", pScheme));
	m_pGraphs->SetAxisColor(Color(76,88,68,255));

}

//-----------------------------------------------------------------------------
// Purpose: Activates the page
//-----------------------------------------------------------------------------
void CGraphPanel::OnPageShow()
{
	BaseClass::OnPageShow();
}

//-----------------------------------------------------------------------------
// Purpose: Hides the page
//-----------------------------------------------------------------------------
void CGraphPanel::OnPageHide()
{
	BaseClass::OnPageHide();
}

//-----------------------------------------------------------------------------
// Purpose: called every frame to update stats page
//-----------------------------------------------------------------------------
void CGraphPanel::OnTick()
{
	if (m_flNextStatsUpdateTime > system()->GetFrameTime())
		return;

	m_flNextStatsUpdateTime = (float)system()->GetFrameTime() + STATS_UPDATE_RATE;
	RemoteServer().RequestValue(this, "stats");	
}

//-----------------------------------------------------------------------------
// Purpose: tells the image about the new size
//-----------------------------------------------------------------------------
void CGraphPanel::PerformLayout()
{
	BaseClass::PerformLayout();
	int w,h,x,y;
	m_pGraphsPanel->GetBounds(x,y,w,h);

	m_pGraphs->SaveSize(w,h); // tell the image about the resize

	// push the mid axis label to the middle of the image
	Label *entry = dynamic_cast<Label *>(FindChildByName("AxisMid"));
	if (entry)
	{
		int entry_x,entry_y;
		entry->GetPos(entry_x,entry_y);
		entry->SetPos(entry_x,y+(h/2)-8);
	}
}

//-----------------------------------------------------------------------------
// Purpose: Handles stats command returns
//-----------------------------------------------------------------------------
void CGraphPanel::OnServerDataResponse(const char *value, const char *response)
{
	if (!stricmp(value, "stats"))
	{
		// parse the stats out of the response
		Points_t p;
		float uptime, users;
		sscanf(response, "%f %f %f %f %f %f %f", &p.cpu, &p.in, &p.out, &uptime, &users, &p.fps, &p.players);
		p.cpu = p.cpu / 100; // its given as a value between 0<x<100, we want 0<x<1
		p.ping = 0;
		p.time = (float)system()->GetCurrentTime();
		m_pGraphs->AddPoint(p);

		// days:hours:minutes:seconds
		char timeText[64];
		_snprintf(timeText, sizeof(timeText), "%i", (int)p.players);
		SetControlString("TotalUsersLabel", timeText);

		// mark the vert combo has changed to force it to update graph ranges
		m_pVertCombo->GetText(timeText, 64);
		OnTextChanged(m_pVertCombo, timeText);
	}
}

//-----------------------------------------------------------------------------
// Purpose: the paint routine for the graph image. Handles the layout and drawing of the graph image
//-----------------------------------------------------------------------------
void CGraphPanel::CGraphsImage::Paint()
{
	int x,y;
	float distPoints; // needs to be a float, rounding errors cause problems with lots of points
	int bottom=5; // be 5 pixels above the bottom
	int left=2;

	int *pCpuX=NULL, *pCpuY=NULL;
	int *pInX=NULL, *pInY=NULL;
	int *pOutX=NULL, *pOutY=NULL;
	int *pFPSX=NULL, *pFPSY=NULL;
	int *pPingX=NULL, *pPingY=NULL;
	int *pPlayersX=NULL, *pPlayersY=NULL;

	GetSize(x,y);

	SetColor(bgColor);
	SetBkColor(bgColor);
	DrawFilledRect(0,0,x,y);

	y-=4; // borders
	x-=4;


	if(!cpu && !fps && !net_i && !net_o && !ping && !players) // no graphs selected
		return; 

	if(points.Count()<2)
		return; // not enough points yet...

	if(x<=200 || y<=100) 
		return; // to small


	distPoints= static_cast<float>(x)/static_cast<float>(points.Count()-1);
	if(distPoints<=0)
	{
		distPoints=1;
	}

	SetColor(lineColor);
	SetBkColor(lineColor);
	//DrawLine(4,5,x,5);
	DrawLine(4,y/2,x,y/2);
	//DrawLine(4,y,x,y);

	float RangePing=maxPing;
	float RangeFPS=maxFPS;
	float Range=0;
	float RangePlayers=maxPlayers;


	if(ping)
	{
		RangePing+=static_cast<float>(maxPing*0.1); // don't let the top of the range touch the top of the panel

		if(RangePing<=1) 
		{ // don't let the zero be at the top of the screen
			RangePing=1.0;
		}
		pPingX = new int[points.Count()];
		pPingY = new int[points.Count()];
	}

	if(cpu)
	{
		pCpuX = new int[points.Count()];
		pCpuY = new int[points.Count()];
	}
	
	if(fps)
	{			
		RangeFPS+=static_cast<float>(maxFPS*0.1); // don't let the top of the range touch the top of the panel

		if(RangeFPS<=1) 
		{ // don't let the zero be at the top of the screen
			RangeFPS=1.0;
		}
		pFPSX = new int[points.Count()];
		pFPSY = new int[points.Count()];
	}

	if(net_i)
	{
	
		// put them on a common scale, base it at zero 
		Range = max(maxIn,maxOut);
			
		Range+=static_cast<float>(Range*0.1); // don't let the top of the range touch the top of the panel

		if(Range<=1) 
		{ // don't let the zero be at the top of the screen
			Range=1.0;
		}

		pInX = new int[points.Count()];
		pInY = new int[points.Count()];
	}

	if(net_o)
	{
		// put them on a common scale, base it at zero 
		Range = max(maxIn,maxOut);
			
		Range+=static_cast<float>(Range*0.1); // don't let the top of the range touch the top of the panel

		if(Range<=1) 
		{ // don't let the zero be at the top of the screen
			Range=1.0;
		}

		pOutX = new int[points.Count()];
		pOutY = new int[points.Count()];
	}

	if(players)
	{
		RangePlayers+=static_cast<float>(maxPlayers*0.1); // don't let the top of the range touch the top of the panel

		pPlayersX = new int[points.Count()];
		pPlayersY = new int[points.Count()];
	}

	for(int i=0;i<points.Count();i++)
	// draw the graphs, left to right
	{
	
		if(cpu) 
		{
			pCpuX[i] = left+static_cast<int>(i*distPoints);
			pCpuY[i] = static_cast<int>((1-points[i].cpu)*y);
		}
	
		if(net_i)
		{
			pInX[i] = left+static_cast<int>(i*distPoints);
			pInY[i] = static_cast<int>(( (Range-points[i].in)/Range)*y-bottom);
		}
		
		if(net_o)
		{
			pOutX[i] = left+static_cast<int>(i*distPoints);
			pOutY[i] = static_cast<int>(((Range-points[i].out)/Range)*y-bottom);
		}

		if(fps)
		{
			pFPSX[i] = left+static_cast<int>(i*distPoints);
			pFPSY[i] = static_cast<int>(( (RangeFPS-points[i].fps)/RangeFPS)*y-bottom);
		}

		if(ping)
		{
			pPingX[i] = left+static_cast<int>(i*distPoints);
			pPingY[i] = static_cast<int>(( (RangePing-points[i].ping)/RangePing)*y-bottom);
		}

		if(players)
		{
			pPlayersX[i] = left+static_cast<int>(i*distPoints);
			pPlayersY[i] = static_cast<int>(( (RangePlayers-points[i].players)/RangePlayers)*y-bottom);
		}

	
	}
	// we use DrawPolyLine, its much, much, much more efficient than calling lots of DrawLine()'s

	if(cpu)
	{
		SetColor(CPUColor); // green
		DrawPolyLine(pCpuX, pCpuY, points.Count());	
		delete [] pCpuX;
		delete [] pCpuY;
	} 

	if(net_i) 
	{
		SetColor(NetInColor); // red
		DrawPolyLine(pInX, pInY, points.Count());	
		delete [] pInX;
		delete [] pInY;
	}
	if(net_o)
	{
		SetColor(NetOutColor); //yellow
		DrawPolyLine(pOutX, pOutY, points.Count());	
		delete [] pOutX;
		delete [] pOutY;
	}
	if(fps)
	{
		SetColor(FPSColor);
		DrawPolyLine(pFPSX, pFPSY, points.Count());	
		delete [] pFPSX;
		delete [] pFPSY;
	}

	if(ping)
	{
		SetColor(PingColor);
		DrawPolyLine(pPingX, pPingY, points.Count());	
		delete [] pPingX;
		delete [] pPingY;
	}

	if(players)
	{
		SetColor(PlayersColor);
		DrawPolyLine(pPlayersX, pPlayersY, points.Count());	
		delete [] pPlayersX;
		delete [] pPlayersY;
	}
}  


//-----------------------------------------------------------------------------
// Purpose: constructor for the graphs image
//-----------------------------------------------------------------------------
CGraphPanel::CGraphsImage::CGraphsImage(): vgui::Image(), points()
{
	maxIn=maxOut=minIn=minOut=minFPS=maxFPS=minPing=maxPing=0;
	net_i=net_o=fps=cpu=ping=players=false;
	numAvgs=0;
	memset(&avgPoint,0x0,sizeof(Points_t));
}

//-----------------------------------------------------------------------------
// Purpose: sets which graph to draw, true means draw it
//-----------------------------------------------------------------------------
void CGraphPanel::CGraphsImage::SetDraw(bool cpu_in,bool fps_in,bool net_in,bool net_out,bool ping_in,bool players_in)
{
	cpu=cpu_in;
	fps=fps_in;
	net_i=net_in;
	net_o=net_out;
	ping=ping_in;
	players=players_in;
}

//-----------------------------------------------------------------------------
// Purpose: used to average points over a period of time
//-----------------------------------------------------------------------------
void CGraphPanel::CGraphsImage::AvgPoint(Points_t p)
{
	avgPoint.cpu += p.cpu;
	avgPoint.fps += p.fps;
	avgPoint.in += p.in;
	avgPoint.out += p.out;
	avgPoint.ping += p.ping;
	avgPoint.players += p.players;
	numAvgs++;
}


//-----------------------------------------------------------------------------
// Purpose: updates the current bounds of the points based on this new point
//-----------------------------------------------------------------------------
void CGraphPanel::CGraphsImage::CheckBounds(Points_t p)
{
	if(p.in>maxIn)
	{
		maxIn=avgPoint.in;
	}
	if(p.out>maxOut)
	{
		maxOut=avgPoint.out;
	}

	if(p.in<minIn)
	{
		minIn=avgPoint.in;
	}
	if(p.out<minOut)
	{
		minOut=avgPoint.out;
	}

	if(p.fps>maxFPS)
	{
		maxFPS=avgPoint.fps;
	}
	if(p.fps<minFPS)
	{
		minFPS=avgPoint.fps;
	}

	if(p.ping>maxPing)
	{
		maxPing=avgPoint.ping;
	}
	if(p.ping<minPing)
	{
		minPing=avgPoint.ping;
	}

	if(p.players>maxPlayers)
	{
		maxPlayers=avgPoint.players;
	}
	if(p.players<minPlayers)
	{
		minPlayers=avgPoint.players;
	}
	
}

//-----------------------------------------------------------------------------
// Purpose: adds a point to the graph image. 
//-----------------------------------------------------------------------------
bool CGraphPanel::CGraphsImage::AddPoint(Points_t p)
{
	int x,y;
	bool recalcBounds=false;

	GetSize(x,y);
	
	if(avgPoint.cpu>1)  // cpu is a percent !
	{
		return false;
	}

	if(timeBetween==SECONDS) // most recent minute
	{
		while(points.Count() && (p.time-points[0].time)>60)
		{
			points.Remove(0);
		}
	}
	else if(timeBetween==HOURS) // most recent hour
	{
		while(points.Count() && (p.time-points[0].time)>60*60)
		{
			points.Remove(0);
		}
	} 
	else if ( timeBetween==MINUTES) // most recent day
	{
			while(points.Count() && (p.time-points[0].time)>60*60*24)
		{
			points.Remove(0);
		}
	}

	AvgPoint(p);
	// now work out the average of all the values
	avgPoint.cpu /= numAvgs;
	avgPoint.fps /= numAvgs;
	avgPoint.in  /= numAvgs;
	avgPoint.out /= numAvgs;
	avgPoint.ping /= numAvgs;
	avgPoint.players /= numAvgs;
	avgPoint.time = p.time;
	numAvgs=0;

	int k=0;

	if(x!=0 && points.Count()> x/2) 
	// there are more points than pixels so thin them out
	{
		while(points.Count()> x/2)
		{
			// check that the bounds don't move
			if(points[0].in==maxIn ||
				points[0].out==maxOut ||
				points[0].fps==maxFPS ||
				points[0].ping==maxPing ||
				points[0].players==maxPlayers)
			{
				recalcBounds=true;
			}
			points.Remove(k); // remove the head node
			k+=2;
			if(k>points.Count()) 
			{
				k=0;
			}
		}
	}

	if(recalcBounds) 
	{
		for(int i=0;i<points.Count();i++)
		{
			CheckBounds(points[i]);
		}
	}


	CheckBounds(avgPoint);

	points.AddToTail(avgPoint);
	
	memset(&avgPoint,0x0,sizeof(Points_t));

	return true;
}

void CGraphPanel::CGraphsImage::SetScale(intervals time)
{
	timeBetween=time;

	// scale is reset so remove all the points
	points.RemoveAll();

	// and reset the maxes
	maxIn=maxOut=minIn=minOut=minFPS=maxFPS=minPing=maxPing=maxPlayers=minPlayers=0;

}


//-----------------------------------------------------------------------------
// Purpose: clear button handler, clears the current points
//-----------------------------------------------------------------------------
void CGraphPanel::OnClearButton()
{
	m_pGraphs->RemovePoints();
}

//-----------------------------------------------------------------------------
// Purpose: passes the state of the check buttons (for graph line display) through to the graph image
//-----------------------------------------------------------------------------
void CGraphPanel::OnCheckButton()
{
	m_pGraphs->SetDraw(m_pCPUButton->IsSelected(), m_pFPSButton->IsSelected(), m_pInButton->IsSelected(), m_pOutButton->IsSelected(), m_pPINGButton->IsSelected(), m_pPlayerButton->IsSelected());
}

//-----------------------------------------------------------------------------
// Purpose:Handles the scale radio buttons, passes the scale to use through to the graph image
//-----------------------------------------------------------------------------
void CGraphPanel::OnTextChanged(Panel *panel, const char *text)
{
	if (panel == m_pTimeCombo)
	{
		if (strstr(text, "Hour"))
		{
			m_pGraphs->SetScale(MINUTES);
		}
		else if (strstr(text, "Day"))
		{
			m_pGraphs->SetScale(HOURS);
		}
		else
		{
			m_pGraphs->SetScale(SECONDS);
		}
	}
	else if (panel == m_pVertCombo)
	{
		float maxVal, minVal;
		char minText[20], midText[20], maxText[20];

		if (strstr(text, "CPU"))
		{
			SetAxisLabels(m_pGraphs->GetCPUColor(), "100%", "50%", "0%");
		}
		else if (strstr(text, "FPS"))
		{
			m_pGraphs->GetFPSLimits(maxVal, minVal);
			sprintf(maxText,"%0.2f", maxVal);
			sprintf(midText,"%0.2f", (maxVal - minVal) / 2);
			sprintf(minText,"%0.2f", minVal);
			SetAxisLabels(m_pGraphs->GetFPSColor(), maxText, midText, minText);
		}
		else if (strstr(text, "In"))
		{
			m_pGraphs->GetInLimits(maxVal, minVal);
			sprintf(maxText,"%0.2f", maxVal);
			sprintf(midText,"%0.2f", (maxVal - minVal) / 2);
			sprintf(minText,"%0.2f", minVal);

			SetAxisLabels(m_pGraphs->GetInColor(), maxText, midText, minText);
		}
		else if (strstr(text, "Out"))
		{
			m_pGraphs->GetOutLimits(maxVal, minVal);
			sprintf(maxText,"%0.2f", maxVal);
			sprintf(midText,"%0.2f", (maxVal - minVal) / 2);
			sprintf(minText,"%0.2f", minVal);
			SetAxisLabels(m_pGraphs->GetOutColor(), maxText, midText, minText);
		}
		else if (strstr(text, "Ping"))
		{
			m_pGraphs->GetPingLimits(maxVal, minVal);
			sprintf(maxText,"%0.2f", maxVal);
			sprintf(midText,"%0.2f", (maxVal - minVal) / 2);
			sprintf(minText,"%0.2f", minVal);
			SetAxisLabels(m_pGraphs->GetPingColor(), maxText, midText, minText);
		}
		else if (strstr(text, "Players"))
		{
			m_pGraphs->GetPlayerLimits(maxVal, minVal);
			sprintf(maxText,"%0.2f", maxVal);
			sprintf(midText,"%0.2f", (maxVal - minVal) / 2);
			sprintf(minText,"%0.2f", minVal);

			SetAxisLabels(m_pGraphs->GetPlayersColor(), maxText, midText, minText);
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CGraphPanel::SetAxisLabels(Color c, char *max, char *mid, char *min)
{
	Label *lab;
	lab= GetLabel("AxisMax");
	if(lab)
	{
		lab->SetFgColor(c);
		lab->SetText(max);
	}
	lab = GetLabel("AxisMid");
	if(lab)
	{
		lab->SetFgColor(c);
		lab->SetText(mid);
	}
	lab = GetLabel("AxisMin");
	if(lab)
	{
		lab->SetFgColor(c);
		lab->SetText(min);
	}
}

Label *CGraphPanel::GetLabel(const char *name)
{
	Label *lab = dynamic_cast<Label *>(FindChildByName(name));
	return lab;
}