// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.


//
// Includes
//

#include "nel/misc/types_nl.h"
#include "nel/misc/debug.h"
#include "nel/misc/mem_stream.h"
#include "nel/misc/time_nl.h"
#include "nel/misc/config_file.h"

#include "nel/net/udp_sock.h"
#include "nel/net/callback_client.h"
#include "nel/net/inet_address.h"
#include "nel/net/udp_sim_sock.h"

#include "graph.h"


#ifdef USE_3D

#include "nel/3d/u_driver.h"
#include "nel/3d/u_scene.h"
#include "nel/3d/u_camera.h"
#include "nel/3d/u_instance.h"
#include "nel/3d/u_animation_set.h"
#include "nel/3d/u_play_list.h"
#include "nel/3d/u_play_list_manager.h"
#include "nel/3d/u_text_context.h"
#include "nel/3d/u_texture.h"
#include "nel/3d/event_mouse_listener.h"

using namespace NL3D;
#endif

#ifndef UDP_DIR
#define UDP_DIR "."
#endif // UDP_DIR

//
// Namespaces
//

using namespace std;
using namespace NLMISC;
using namespace NLNET;

//
// Variables
//

// must be increase at each version and must be the same value as the server
uint32		Version = 2;

string		ServerAddr = "itsalive.nevrax.org";	// ldserver
uint16		UDPPort = 45455;
uint16		TCPPort = 45456;

uint32		MaxUDPPacketSize = 1000;

CUdpSimSock	*UdpSock = NULL;

uint8		Mode = 0;

uint64		Session = 0;

string		ConnectionName;

CConfigFile	ConfigFile;

#ifdef USE_3D

CGraph FpsGraph ("frame rate (fps)", 10.0f, 110.0f, 100.0f, 100.0f, CRGBA(128,0,0,128), 1000, 150.0f);
CGraph DownloadGraph ("download (bps)", 10.0f, 260.0f, 100.0f, 100.0f, CRGBA(0,0,128,128), 1000, 20000.0f);
CGraph UploadGraph ("upload (bps)", 10.0f, 360.0f, 100.0f, 100.0f, CRGBA(0,128,128,128), 1000, 20000.0f);
CGraph LagGraph ("lag (ms)", 150.0f, 110.0f, 100.0f, 100.0f, CRGBA(128,64,64,128), 100000, 2000.0f);

#endif

//
// Functions
//

void exit (const string &reason)
{
	if (!reason.empty())
		InfoLog->displayRawNL ("%s", reason.c_str());
	InfoLog->displayRawNL ("Press <enter> to exit");
	getchar ();
	exit(EXIT_FAILURE);
}

//
// Config file functions
//

void createConfigFile()
{
	FILE *fp = fopen ("client.cfg", "wt");
	if (fp == NULL)
	{
		InfoLog->displayRawNL ("Can't create client.cfg");
	}
	else
	{
		fprintf (fp, "ServerAddress = \"%s\";\n", ServerAddr.c_str());
		fprintf (fp, "SimInLag = 0;\n");
		fprintf (fp, "SimInPacketLost = 0;\n");
		fprintf (fp, "SimOutLag = 0;\n");
		fprintf (fp, "SimOutPacketLost = 0;\n");
		fprintf (fp, "SimOutPacketDuplication = 0;\n");
		fprintf (fp, "SimOutPacketDisordering = 0;\n");
		fprintf (fp, "ConnectionName = \"\";\n");
		fclose (fp);
	}
}

void checkConnectionName ()
{
	if (ConnectionName.size() > 30)
	{
		exit ("Bad connection name (must be <= 30 characters)");
	}

	if (ConnectionName.size() > 0 && ConnectionName[ConnectionName.size()-1] == '\n')
	{
		ConnectionName = ConnectionName.substr (0, ConnectionName.size()-1);
	}
	
	if (ConnectionName.size() <= 0)
	{
		exit ("Bad connection name (must be > 0 character)");
	}

	for (uint i = 0; i < ConnectionName.size(); i++)
	{
		if (!isalnum(ConnectionName[i]))
		{
			exit ("Bad connection name, only alpha numeric characters is allowed (char '%c' is not alphanum)");
		}
	}
}

void loadConfigFile ()
{
	FILE *fp = fopen ("client.cfg", "rt");
	if (fp == NULL)
	{
		createConfigFile();
	}
	else
	{
		fclose (fp);
	}

	ConfigFile.load ("client.cfg");

	// set internet simulation values
	CUdpSimSock::setSimValues (ConfigFile);

	ServerAddr = ConfigFile.getVar("ServerAddress").asString();

	ConnectionName = ConfigFile.getVar("ConnectionName").asString();

	if (ConnectionName.empty())
	{
		InfoLog->displayRawNL ("Please, enter a connection name");
		InfoLog->displayRawNL ("(only alphanumeric character limited to 30 character, no space)");
		InfoLog->displayRawNL ("For example enter your name and/or your location (ie: \"AceHome\"),");
		InfoLog->displayRawNL ("It'll be use to find your stat file easier:");
		char cn[128];
		if (fgets (cn, 127, stdin) == NULL)
		{
			exit ("Error during the keyboard scanning");
		}
		ConnectionName = cn;
		checkConnectionName ();
		ConfigFile.getVar ("ConnectionName").setAsString(ConnectionName);
		ConfigFile.save ();
	}
	else
	{
		checkConnectionName ();
	}
}


//
// Callbacks
//

void cbInfo (CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
	string line;
	msgin.serial (line);
	InfoLog->displayRawNL ("%s", line.c_str());

#ifdef USE_3D
	string token = "MeanPongTime ";
	string::size_type pos=line.find (token);
	string::size_type pos2=line.find (" ", pos+token.size());
	float val;
	NLMISC::fromString(line.substr (pos+token.size(), pos2-pos-token.size()), val);	
	LagGraph.addOneValue (val);
#endif
}

void cbInit (CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
	msgin.serial (Session);

	// create the UDP connection
	nlassert (UdpSock == NULL);
	UdpSock = new CUdpSimSock( false );
	try
	{
		UdpSock->connect( CInetAddress (ServerAddr, UDPPort) );
	}
	catch ( Exception& e )
	{
		InfoLog->displayRawNL ("Cannot connect to remote UDP host '%s': %s", ServerAddr.c_str(), e.what() );
		exit ("");
	}
}

void cbStart (CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
	InfoLog->displayRawNL ("Bench is starting..");

	Mode = 1;
}

void cbDisconnect (TSockId from, void *arg)
{
	exit ("Lost the server connection. You should not have the last client version\nGet it here: http://www.nevrax.org/download/bench.zip");
}

TCallbackItem CallbackArray[] =
{
	{ "INIT", cbInit },
	{ "INFO", cbInfo },
	{ "START", cbStart },
};



//
// Main
//
int main( int argc, char **argv )
{
	createDebug ();
	DebugLog->addNegativeFilter(" ");
	
	InfoLog->displayRawNL ("\nNevrax UDP benchmark client\n\nPress <CTRL-C> to exit");

	CPath::addSearchPath(UDP_DIR);

	loadConfigFile ();

	CCallbackClient *cc = new CCallbackClient;
	
	cc->addCallbackArray (CallbackArray, sizeof(CallbackArray)/sizeof(CallbackArray[0]));
	cc->setDisconnectionCallback (cbDisconnect, NULL);

	try
	{
		InfoLog->displayRawNL ("Try to connect to %s:%d", ServerAddr.c_str(), TCPPort);
		cc->connect(CInetAddress (ServerAddr, TCPPort));

		CMessage msgout ("INIT");
		msgout.serial (ConnectionName);
		msgout.serial (Version);
		cc->send (msgout);

		InfoLog->displayRawNL ("Waiting the server answer...");
	}
	catch(Exception &e)
	{
		InfoLog->displayRawNL ("Can't connect to %s:%d (%s)\n", ServerAddr.c_str(), TCPPort, e.what());
		exit ("");
	}

	uint8 *packet = new uint8[MaxUDPPacketSize];
	uint32 psize;

#ifdef USE_3D

	UDriver *Driver = UDriver::createDriver();
	Driver->setDisplay(UDriver::CMode(800, 600, 32, true));
	UScene *Scene= Driver->createScene(false);
	UCamera Camera= Scene->getCam();
	Camera.setTransformMode(UTransform::DirectMatrix);
	UTextContext *TextContext= Driver->createTextContext(CPath::lookup("n019003l.pfb"));
	TextContext->setFontSize(18);

	Camera.setPerspective(80*(float)Pi/180, 1.33f, 0.15f, 1000);

	CEvent3dMouseListener MouseListener;
	MouseListener.addToServer(Driver->EventServer);
	MouseListener.setFrustrum(Camera.getFrustum());
	MouseListener.setHotSpot(CVector(0,0,0));
	CMatrix		initMat;
	initMat.setPos(CVector(0,-5,0));
	MouseListener.setMatrix(initMat);

#endif



	while (cc->connected ())
	{
#ifdef USE_3D

		// Manip.
		Camera.setMatrix(MouseListener.getViewMatrix());

		Driver->EventServer.pump();
		if(Driver->AsyncListener.isKeyPushed(KeyESCAPE))
			return EXIT_SUCCESS;

		Driver->clearBuffers(CRGBA(255,255,255,0));

		Scene->render();

		CGraph::render (*Driver, *TextContext);

		Driver->swapBuffers();

		FpsGraph.addValue (1);

#endif


		CConfigFile::checkConfigFiles ();

		// update TCP connection
		cc->update ();

		// update UDP connection
		if (UdpSock != NULL)
		{
			if (Mode == 0)
			{
				// init the UDP connection
				CMemStream msgout;
				msgout.serial (Mode);
				msgout.serial (Session);
				uint32 size = msgout.length();
#ifdef USE_3D
				UploadGraph.addValue ((float)size);
#endif
				UdpSock->send (msgout.buffer(), size);
				nldebug ("Sent init udp connection");
				nlSleep (100);
			}

			while (UdpSock->dataAvailable())
			{
				psize = MaxUDPPacketSize;
				UdpSock->receive (packet, psize);
#ifdef USE_3D
				DownloadGraph.addValue ((float)psize);
#endif
				CMemStream msgin( true );
				memcpy (msgin.bufferToFill (psize), packet, psize);

				sint64 t = 0;
				msgin.serial (t);

				uint32 p = 0;
				msgin.serial (p);

				uint32 b = 0;
				msgin.serial (b);
	
				// I received a ping, send a pong

				CMemStream msgout;
				msgout.serial (Mode);
				msgout.serial (t);
				msgout.serial (p);
				msgout.serial (b);
				uint8 dummy=0;
				while (msgout.length() < 200)
					msgout.serial (dummy);

				uint32 size =  msgout.length();
				nlassert (size == 200);

#ifdef USE_3D
				UploadGraph.addValue ((float)size);
#endif
				UdpSock->send (msgout.buffer(), size);
			}
		}

		nlSleep (1);
	}
	
	exit ("");
	return EXIT_SUCCESS;
}