// NeLNS - 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/>.

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif // HAVE_CONFIG_H

#ifndef NELNS_CONFIG
#define NELNS_CONFIG ""
#endif // NELNS_CONFIG

#ifndef NELNS_LOGS
#define NELNS_LOGS ""
#endif // NELNS_LOGS

#include "nel/misc/types_nl.h"

#include <stdio.h>
#include <ctype.h>
#include <math.h>

#include <list>

#include "nel/misc/debug.h"
#include "nel/misc/config_file.h"
#include "nel/misc/displayer.h"
#include "nel/misc/command.h"
#include "nel/misc/variable.h"
#include "nel/misc/log.h"
#include "nel/misc/file.h"
#include "nel/misc/path.h"

#include "nel/net/service.h"
#include "nel/net/unified_network.h"
#include "nel/net/login_cookie.h"

#include "welcome_service.h"

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


CVariable<sint> PlayerLimit(
	"ws","PlayerLimit", "Rough max number of players accepted on this shard (-1 for Unlimited)",
	5000,
	0, true );

// Forward declaration of callback cbShardOpen (see ShardOpen variable)
void	cbShardOpen(IVariable &var);

// Forward declaration of callback cbShardOpenStateFile (see ShardOpenStateFile variable)
void	cbShardOpenStateFile(IVariable &var);

// Forward declaration of callback cbUsePatchMode
void	cbUsePatchMode(IVariable &var);

// Types of open state
enum TShardOpenState
{
	ClosedForAll = 0,
	OpenOnlyForAllowed = 1,
	OpenForAll = 2
};

static bool	AllowDispatchMsgToLS = false;

/**
 * ShardOpen
 * true if shard is open to public
 * 0 means closed for all but :DEV:
 * 1 means open only for groups in config file (see OpenGroups variable) and :DEV:
 * 2 means open for all
 */
CVariable<uint>		ShardOpen("ws", "ShardOpen", "Indicates if shard is open to public (0 closed for all but :DEV:, 1 open only for groups in cfg, 2 open for all)", 2, 0, true, cbShardOpen);

/**
 * ShardOpenStateFile
 * true if shard is open to public
 */
CVariable<string>	ShardOpenStateFile("ws", "ShardOpenStateFile", "Name of the file that contains ShardOpen state", "", 0, true, cbShardOpenStateFile);

/**
 * OpenGroups
 */
CVariable<string>	OpenGroups("ws", "OpenGroups", "list of groups allowed at ShardOpen Level 1", "", 0, true);

/**
 * OpenFrontEndThreshold
 * The FS balance algorithm works like this:
 * - select the least loaded frontend
 * - if this frontend has more than the OpenFrontEndThreshold
 *   - try to open a new frontend
 *   - reselect least loaded frontend
 */
CVariable<uint>		OpenFrontEndThreshold("ws", "OpenFrontEndThreshold", "Limit number of players on all FS to decide to open a new FS", 800,	0, true );


/**
 * Use Patch mode
 */
CVariable<bool>		UsePatchMode("ws", "UsePatchMode", "Use Frontends as Patch servers (at FS startup)", true, 0, true, cbUsePatchMode );

/**
 * Use Patch mode
 */
CVariable<bool>		DontUseLS("ws", "DontUseLS", "Don't use the login service", false, 0, true);


// Shortcut to the module instance
//CWelcomeServiceMod	*CWelcomeServiceMod::_Instance = NULL;


/**
 * Using expected services and current running service instances, this class
 * reports a main "online status".
 */
class COnlineServices
{
public:

	/// Set expected instances. Ex: { "TICKS", "FS", "FS", "FS" }
	void		setExpectedInstances( CConfigFile::CVar& var )
	{
		// Reset "expected" counters (but don't clear the map, keep the running instances)
		CInstances::iterator ici;
		for ( ici=_Instances.begin(); ici!=_Instances.end(); ++ici )
		{
			(*ici).second.Expected = 0;
		}
		// Rebuild "expected" counters
		for ( uint i=0; i!=var.size(); ++i )
		{
			++_Instances[var.asString(i)].Expected;
		}
	}

	/// Add a service instance
	void		addInstance( const std::string& serviceName )
	{
		++_Instances[serviceName].Running;
	}

	/// Remove a service instance
	void		removeInstance( const std::string& serviceName )
	{
		CInstances::iterator ici = _Instances.find( serviceName );
		if ( ici != _Instances.end() )
		{
			--(*ici).second.Running;

			// Remove from the map only if not part of the expected list
			if ( ((*ici).second.Expected == 0) && ((*ici).second.Running == 0) )
			{
				_Instances.erase( ici );
			}
		}
		else
		{
			nlwarning( "Can't remove instance of %s", serviceName.c_str() );
		}
	}

	/// Check if all expected instances are online
	bool		getOnlineStatus() const
	{
		CInstances::const_iterator ici;
		for ( ici=_Instances.begin(); ici!=_Instances.end(); ++ici )
		{
			if ( ! ici->second.isOnlineAsExpected() )
				return false;
		}
		return true;
	}

	/// Display contents
	void		display( NLMISC::CLog& log = *NLMISC::DebugLog )
	{
		CInstances::const_iterator ici;
		for ( ici=_Instances.begin(); ici!=_Instances.end(); ++ici )
		{
			log.displayNL( "%s: %s (%u expected, %u running)",
				(*ici).first.c_str(),
				(*ici).second.Expected ? ((*ici).second.isOnlineAsExpected() ? "ONLINE" : "MISSING") : "OPTIONAL",
				(*ici).second.Expected, (*ici).second.Running );
		}
	}

private:

	struct TInstanceCounters
	{
		TInstanceCounters() : Expected(0), Running(0) {}

		// If not expected, count as online as well
		bool isOnlineAsExpected() const { return Running >= Expected; }

		uint	Expected;
		uint	Running;
	};

	typedef std::map< std::string, TInstanceCounters > CInstances;

	CInstances	_Instances;
};

/// Online services
COnlineServices OnlineServices;


/// Main online status
bool OnlineStatus;

/// Send changes of status to the LS
void reportOnlineStatus( bool newStatus )
{
	if ( newStatus != OnlineStatus && AllowDispatchMsgToLS )
	{
		if (!DontUseLS)
		{
			CMessage msgout( "OL_ST" );
			msgout.serial( newStatus );
			CUnifiedNetwork::getInstance()->send( "LS", msgout );
		}

		if (CWelcomeServiceMod::isInitialized())
		{
			// send a status report to welcome service client
			CWelcomeServiceMod::getInstance()->reportWSOpenState(newStatus);
		}

		OnlineStatus = newStatus;
	}
}



/// Set the version of the shard. you have to increase it each time the client-server protocol changes.
/// You have to increment the client too (the server and client version must be the same to run correctly)
static const uint32 ServerVersion = 1;

/// Contains the correspondance between userid and the FES connection where the userid is connected.
map<uint32, TServiceId> UserIdSockAssociations;

// ubi hack
string FrontEndAddress;



enum TFESState
{
	PatchOnly,
	AcceptClientOnly
};

struct CFES
{
	CFES (TServiceId sid) : SId(sid), NbPendingUsers(0), NbUser(0), State(PatchOnly) { }

	TServiceId	SId;				// Connection to the front end
	uint32		NbPendingUsers;		// Number of not yet connected users (but rooted to this frontend)
	uint32		NbUser;				// Number of user currently connected on this front end

	TFESState	State;				// State of frontend (patching/accepting clients)
	std::string	PatchAddress;		// Address of frontend patching server

	uint32		getUsersCountHeuristic() const
	{
		return NbUser + NbPendingUsers;
	}

	void		setToAcceptClients()
	{
		if (State == AcceptClientOnly)
			return;

		// tell FS to accept client
		State = AcceptClientOnly;
		CMessage	msgOpenFES("FS_ACCEPT");
		CUnifiedNetwork::getInstance()->send(SId, msgOpenFES);

		// report state to LS
		bool	dummy;
		reportStateToLS(dummy, true);
	}

	void		reportStateToLS(bool& reportPatching, bool alive = true)
	{
		// report to LS

		bool	patching = (State == PatchOnly);
		if (alive && patching)
			reportPatching = true;

		if ( AllowDispatchMsgToLS )
		{
			if (!DontUseLS)
			{
				CMessage	msgout("REPORT_FS_STATE");
				msgout.serial(SId);
				msgout.serial(alive);
				msgout.serial(patching);
				msgout.serial(PatchAddress);
				CUnifiedNetwork::getInstance()->send("LS", msgout);
			}
		}
	}
};

list<CFES> FESList;

/*
 * Find the best front end service for a new connecting user (return NULL if there is no suitable FES).
 * Additionally, calculate totalNbUsers.
 */
CFES *findBestFES ( uint& totalNbUsers )
{
	totalNbUsers = 0;

	CFES*	best = NULL;

	for (list<CFES>::iterator it=FESList.begin(); it!=FESList.end(); ++it)
	{
		CFES &fes = *it;
		if (fes.State == AcceptClientOnly)
		{
			if (best == NULL || best->getUsersCountHeuristic() > fes.getUsersCountHeuristic())
				best = &fes;

			totalNbUsers += fes.NbUser;
		}

	}

	return best;
}

/**
 * Select a frontend in patch mode to open
 * Returns true if a new FES was open, false if no FES could be open
 */
bool	openNewFES()
{
	for (list<CFES>::iterator it=FESList.begin(); it!=FESList.end(); ++it)
	{
		if ((*it).State == PatchOnly)
		{
			nlinfo("openNewFES: ask the FS %d to accept clients", it->SId.get());

			// switch FES to AcceptClientOnly
			(*it).setToAcceptClients();
			return true;
		}
	}

	return false;
}



void displayFES ()
{
	nlinfo ("There's %d FES in the list:", FESList.size());
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		nlinfo(" > %u NbUser:%d NbPendingUser:%d", it->SId.get(), it->NbUser, it->NbPendingUsers);
	}
	nlinfo ("End of the list");
}






////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// CONNECTION TO THE FRONT END SERVICE ///////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void cbFESShardChooseShard (CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// the WS answer a user authorize
	string reason;
	CLoginCookie cookie;
	string addr;

	//
	// S09: receive "SCS" message from FES and send the "SCS" message to the LS
	//

	CMessage msgout ("SCS");

	msgin.serial (reason);
	msgout.serial (reason);

	msgin.serial (cookie);
	msgout.serial (cookie);

	if (reason.empty())
	{
		msgin.serial (addr);

		// if we set the FontEndAddress in the welcome_service.cfg we use this address
		if (FrontEndAddress.empty())
		{
			msgout.serial (addr);
		}
		else
		{
			msgout.serial (FrontEndAddress);
		}

		uint32 nbPendingUser;
		msgin.serial(nbPendingUser);

		// update the pending user count for this shard
		for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
		{
			if (it->SId == sid)
			{
				it->NbPendingUsers = nbPendingUser;
				break;
			}
		}

		/*
		// OBSOLETE: LS doesn't read patching URLs
		// build patch server list
		std::string	PatchURLS;
		for (list<CFES>::iterator it=FESList.begin(); it!=FESList.end(); ++it)
		{
			if ((*it).State == PatchOnly && !(*it).PatchAddress.empty())
			{
				if (!PatchURLS.empty())
					PatchURLS += '|';
				PatchURLS += (*it).PatchAddress;
			}
		}


		msgout.serial(PatchURLS);
		*/
	}

	if (PendingFeResponse.find(cookie) != PendingFeResponse.end())
	{
		nldebug( "ERLOG: SCS recvd from %s-%hu => sending %s to SU", serviceName.c_str(), sid.get(), cookie.toString().c_str());

		// this response is not waited by LS
		TPendingFEResponseInfo &pfri = PendingFeResponse.find(cookie)->second;

		pfri.WSMod->frontendResponse(pfri.WaiterModule, pfri.UserId, reason, cookie, addr);
		// cleanup pending record
		PendingFeResponse.erase(cookie);
	}
	else
	{
		nldebug( "ERLOG: SCS recvd from %s-%hu, but pending %s not found", serviceName.c_str(), sid.get(), cookie.toString().c_str());

		// return the result to the LS
		if (!DontUseLS)
		{
			CUnifiedNetwork::getInstance()->send ("LS", msgout);
		}
	}

}

// This function is call when a FES accepted a new client or lost a connection to a client
void cbFESClientConnected (CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	//
	// S15: receive "CC" message from FES and send "CC" message to the "LS"
	//

	CMessage msgout ("CC");

	uint32 userid;
	msgin.serial (userid);
	msgout.serial (userid);

	uint8 con;
	msgin.serial (con);
	msgout.serial (con);

	if (!DontUseLS)
	{
		CUnifiedNetwork::getInstance()->send ("LS", msgout);
	}

	// add or remove the user number really connected on this shard
	uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		if (it->SId == sid)
		{
			if (con)
			{
				(*it).NbUser++;

				// the client connected, it's no longer pending
				if ((*it).NbPendingUsers > 0)
					(*it).NbPendingUsers--;
			}
			else
			{
				if ( (*it).NbUser != 0 )
					(*it).NbUser--;
			}
		}
		totalNbOnlineUsers += (*it).NbUser;
		totalNbPendingUsers += (*it).NbPendingUsers;
	}

	if (CWelcomeServiceMod::isInitialized())
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);

	if (con)
	{
		// we know that this user is on this FES
		UserIdSockAssociations.insert (make_pair (userid, sid));
	}
	else
	{
		// remove the user
		UserIdSockAssociations.erase (userid);
	}

}

// This function is called when a FES rejected a client' cookie
void	cbFESRemovedPendingCookie(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	CLoginCookie	cookie;
	msgin.serial(cookie);
	nldebug( "ERLOG: RPC recvd from %s-%hu => %s removed", serviceName.c_str(), sid.get(), cookie.toString().c_str(), cookie.toString().c_str());


	// client' cookie rejected, no longer pending
	uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		if ((*it).SId == sid)
		{
			if ((*it).NbPendingUsers > 0)
				--(*it).NbPendingUsers;
		}
		totalNbOnlineUsers += (*it).NbUser;
		totalNbPendingUsers += (*it).NbPendingUsers;
	}

	if (CWelcomeServiceMod::isInitialized())
	{
		CWelcomeServiceMod::getInstance()->pendingUserLost(cookie);
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);
	}
}

// This function is called by FES to setup its PatchAddress
void	cbFESPatchAddress(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	std::string	address;
	msgin.serial(address);

	bool		acceptClients;
	msgin.serial(acceptClients);

	nldebug("Received patch server address '%s' from service %s %d", address.c_str(), serviceName.c_str(), sid.get());

	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		if ((*it).SId == sid)
		{
			nldebug("Affected patch server address '%s' to frontend %s %d", address.c_str(), serviceName.c_str(), sid.get());

			if (!UsePatchMode.get() && !acceptClients)
			{
				// not in patch mode, force fs to accept clients
				acceptClients = true;
				(*it).setToAcceptClients();
			}

			(*it).PatchAddress = address;
			(*it).State = (acceptClients ? AcceptClientOnly : PatchOnly);
			if (acceptClients)
				nldebug("Frontend %s %d reported to accept client, patching unavailable for that server", address.c_str(), serviceName.c_str(), sid.get());
			else
				nldebug("Frontend %s %d reported to be in patching mode", address.c_str(), serviceName.c_str(), sid.get());

			bool	dummy;
			(*it).reportStateToLS(dummy);
			break;
		}
	}
}

// This function is called by FES to setup the right number of players (if FES was already present before WS launching)
void	cbFESNbPlayers(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// *********** WARNING *******************
	// This version of the callback is deprecated, the system
	// now use cbFESNbPlayers2 that report the pending user count
	// as well as the number of connected players.
	// It is kept for backward compatibility only.
	// ***************************************

	uint32	nbPlayers;
	msgin.serial(nbPlayers);

	uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		if ((*it).SId == sid)
		{
			nldebug("Frontend '%d' reported %d online users", sid.get(), nbPlayers);
			(*it).NbUser = nbPlayers;
			if (nbPlayers != 0 && (*it).State == PatchOnly)
			{
				nlwarning("Frontend %d is in state PatchOnly, yet reports to have online %d players, state AcceptClientOnly is forced (FS_ACCEPT message sent)");
				(*it).setToAcceptClients();
			}
		}
		totalNbOnlineUsers += (*it).NbUser;
		totalNbPendingUsers += (*it).NbPendingUsers;
	}

	if (CWelcomeServiceMod::isInitialized())
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);
}


// This function is called by FES to setup the right number of players (if FES was already present before WS launching)
void	cbFESNbPlayers2(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	uint32	nbPlayers;
	uint32	nbPendingPlayers;
	msgin.serial(nbPlayers);
	msgin.serial(nbPendingPlayers);

	uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		CFES &fes = *it;
		if (fes.SId == sid)
		{
			nldebug("Frontend '%d' reported %d online users", sid.get(), nbPlayers);
			fes.NbUser = nbPlayers;
			fes.NbPendingUsers = nbPendingPlayers;
			if (nbPlayers != 0 && fes.State == PatchOnly)
			{
				nlwarning("Frontend %d is in state PatchOnly, yet reports to have online %d players, state AcceptClientOnly is forced (FS_ACCEPT message sent)");
				(*it).setToAcceptClients();
			}
		}
		totalNbOnlineUsers += fes.NbUser;
		totalNbPendingUsers += fes.NbPendingUsers;
	}

	if (CWelcomeServiceMod::isInitialized())
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);
}

/*
 * Set Shard open state
 */
void	setShardOpenState(TShardOpenState state, bool writeInVar = true)
{
	if (writeInVar)
		ShardOpen = state;

	if ( AllowDispatchMsgToLS )
	{
		if (!DontUseLS)
		{
			// send to LS current shard state
			CMessage	msgout ("SET_SHARD_OPEN");
			uint8		shardOpenState = (uint8)state;

			msgout.serial (shardOpenState);
			CUnifiedNetwork::getInstance()->send ("LS", msgout);
		}
	}
}


/*
 * Set Shard Open State
 * uint8	Open State (0 closed for all, 1 open for groups in cfg, 2 open for all)
 */
void cbSetShardOpen(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	uint8 shardOpenState;
	msgin.serial (shardOpenState);

	if (shardOpenState > OpenForAll)
	{
		shardOpenState = OpenForAll;
	}

	setShardOpenState((TShardOpenState)shardOpenState);
}

// forward declaration to callback
void	cbShardOpenStateFile(IVariable &var);

/*
 * Restore Shard Open state from config file or from file if found
 */
void cbRestoreShardOpen(CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// first restore state from config file
	CConfigFile::CVar*	var = IService::getInstance()->ConfigFile.getVarPtr("ShardOpen");
	if (var != NULL)
	{
		setShardOpenState((TShardOpenState)var->asInt());
	}

	// then restore state from state file, if it exists
	cbShardOpenStateFile(ShardOpenStateFile);
}





// a new front end connecting to me, add it
void cbFESConnection (const std::string &serviceName, TServiceId  sid, void *arg)
{
	FESList.push_back (CFES ((TServiceId)sid));
	nldebug("new FES connection: sid %u", sid.get());
	displayFES ();

	bool	dummy;
	FESList.back().reportStateToLS(dummy);

	if (!UsePatchMode.get())
	{
		FESList.back().setToAcceptClients();
	}
}


// a front end closes the connection, deconnect him
void cbFESDisconnection (const std::string &serviceName, TServiceId  sid, void *arg)
{
	nldebug("new FES disconnection: sid %u", sid.get());

	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		if ((*it).SId == sid)
		{
			// send a message to the LS to say that all players from this FES are offline
			map<uint32, TServiceId>::iterator itc = UserIdSockAssociations.begin();
			map<uint32, TServiceId>::iterator nitc = itc;
			while (itc != UserIdSockAssociations.end())
			{
				nitc++;
				if ((*itc).second == sid)
				{
					// bye bye little player
					uint32 userid = (*itc).first;
					nlinfo ("Due to a frontend crash, removed the player %d", userid);
					if (!DontUseLS)
					{
						CMessage msgout ("CC");
						msgout.serial (userid);
						uint8 con = 0;
						msgout.serial (con);
						CUnifiedNetwork::getInstance()->send ("LS", msgout);
					}
					UserIdSockAssociations.erase (itc);
				}
				itc = nitc;
			}

			bool	dummy;
			(*it).reportStateToLS(dummy, false);

			// remove the FES
			FESList.erase (it);

			break;
		}
	}

	// Update the welcome service client with the new count of connection

	uint32 totalNbOnlineUsers =0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end(); it++)
	{
		const CFES &fes = *it;
		totalNbOnlineUsers += fes.NbUser;
		totalNbPendingUsers += fes.NbPendingUsers;
	}

	if (CWelcomeServiceMod::isInitialized())
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);

	displayFES ();
}


//
void cbServiceUp (const std::string &serviceName, TServiceId  sid, void *arg)
{
	OnlineServices.addInstance( serviceName );
	bool online = OnlineServices.getOnlineStatus();
	reportOnlineStatus( online );

	// send shard id to service
	sint32 shardId;
	if (IService::getInstance()->haveArg('S'))
	{
		// use the command line param if set
		shardId = atoi(IService::getInstance()->getArg('S').c_str());
	}
	else if (IService::getInstance()->ConfigFile.exists ("ShardId"))
	{
		// use the config file param if set
		shardId = IService::getInstance()->ConfigFile.getVar ("ShardId").asInt();
	}
	else
	{
		shardId = -1;
	}

	if (shardId == -1)
	{
		nlerror ("ShardId variable must be valid (>0)");
	}

	CMessage	msgout("R_SH_ID");
	msgout.serial(shardId);
	CUnifiedNetwork::getInstance()->send (sid, msgout);
}


//
void cbServiceDown (const std::string &serviceName, TServiceId  sid, void *arg)
{
	OnlineServices.removeInstance( serviceName );
	bool online = OnlineServices.getOnlineStatus();
	reportOnlineStatus( online );
}


// Callback Array for message from FES
TUnifiedCallbackItem FESCallbackArray[] =
{
	{ "SCS",				cbFESShardChooseShard },
	{ "CC",					cbFESClientConnected },
	{ "RPC",				cbFESRemovedPendingCookie },
	{ "FEPA",				cbFESPatchAddress },
	{ "NBPLAYERS",			cbFESNbPlayers },
	{ "NBPLAYERS2",			cbFESNbPlayers2 },

	{ "SET_SHARD_OPEN",		cbSetShardOpen },
	{ "RESTORE_SHARD_OPEN",	cbRestoreShardOpen },
};


////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// CONNECTION TO THE LOGIN SERVICE ///////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void cbLSChooseShard (CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// the LS warns me that a new client want to come in my shard

	nldebug( "ERLOG: CS recvd from %s-%hu", serviceName.c_str(), sid.get());

	//
	// S07: receive the "CS" message from LS and send the "CS" message to the selected FES
	//

	CLoginCookie cookie;
	msgin.serial (cookie);
	string userName, userPriv, userExtended;
	msgin.serial (userName);

	try
	{
		msgin.serial (userPriv);
	}
	catch (Exception &)
	{
		nlwarning ("LS didn't give me the user privilege for user '%s', set to empty", userName.c_str());
	}

	try
	{
		msgin.serial (userExtended);
	}
	catch (Exception &)
	{
		nlwarning ("LS didn't give me the extended data for user '%s', set to empty", userName.c_str());
	}


	string ret = lsChooseShard(userName, cookie, userPriv, userExtended, WS::TUserRole::ur_player, 0xffffffff, ~0);

	if (!ret.empty())
	{
		// send back an error message to LS
		CMessage msgout ("SCS");
		msgout.serial (ret);
		msgout.serial (cookie);
		CUnifiedNetwork::getInstance()->send(sid, msgout);
	}
}

//void cbLSChooseShard (CMessage &msgin, const std::string &serviceName, uint16 sid)
std::string lsChooseShard (const std::string &userName,
					const CLoginCookie &cookie,
					const std::string &userPriv,
					const std::string &userExtended,
					WS::TUserRole userRole,
					uint32 instanceId,
					uint32 charSlot)
{
	// the LS warns me that a new client want to come in my shard

	//
	// S07: receive the "CS" message from LS and send the "CS" message to the selected FES
	//

/*
	uint totalNbUsers;
	CFES *best = findBestFES( totalNbUsers );
	if (best == NULL)
	{
		// answer the LS that we can't accept the user
		CMessage msgout ("SCS");
		string reason = "No front-end server available";
		msgout.serial (reason);
		msgout.serial (cookie);
		CUnifiedNetwork::getInstance()->send(sid, msgout);
		return;
	}
*/

	uint	totalNbUsers;
	CFES*	best = findBestFES( totalNbUsers );

	// could not find a good FES or best FES has more players than balance limit
	if (best == NULL || best->getUsersCountHeuristic() >= OpenFrontEndThreshold)
	{
		// open a new frontend
		openNewFES();

		// reselect best FES (will return newly open FES, or previous if no more FES available)
		best = findBestFES(totalNbUsers);

		// check there is a FES available
		if (best == NULL)
		{
			// answer the LS that we can't accept the user
			return "No front-end server available";
		}
	}


	bool	authorizeUser = false;
	bool	forceAuthorize = false;

	if (userPriv == ":DEV:")
	{
		// devs have all privileges
		authorizeUser = true;
		forceAuthorize = true;
	}
	else if (ShardOpen != ClosedForAll)
	{
		const std::string&	allowedGroups = OpenGroups;
		bool				userInOpenGroups = (!userPriv.empty() && !allowedGroups.empty() && allowedGroups.find(userPriv) != std::string::npos);

		// open for all or user is privileged
		authorizeUser = (ShardOpen == OpenForAll || userInOpenGroups);
		// let authorized users to force access even if limit is reached
		forceAuthorize = userInOpenGroups;
	}

	bool	shardLimitReached = ( (PlayerLimit.get() != -1) && (totalNbUsers >= (uint)PlayerLimit.get()) );

	if (!forceAuthorize && (!authorizeUser || shardLimitReached))
	{
		// answer the LS that we can't accept the user
		CMessage msgout ("SCS");
		string reason;
		if (shardLimitReached)
			return "The shard is currently full, please try again in 5 minutes.";
		else
			return "The shard is closed.";
	}


	CMessage msgout ("CS");
	msgout.serial (const_cast<CLoginCookie&>(cookie));
	msgout.serial (const_cast<string&>(userName), const_cast<string&>(userPriv), const_cast<string&>(userExtended));
	msgout.serial (instanceId);
	msgout.serial (charSlot);

	CUnifiedNetwork::getInstance()->send (best->SId, msgout);
	best->NbPendingUsers++;

	// Update counts
	uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
	for (list<CFES>::iterator it=FESList.begin(); it!=FESList.end(); ++it)
	{
		totalNbOnlineUsers += (*it).NbUser;
		totalNbPendingUsers += (*it).NbPendingUsers;
	}
	if (CWelcomeServiceMod::isInitialized())
		CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);

	return "";
}

void cbFailed (CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// I can't connect to the Login Service, just nlerror ();
	string reason;
	msgin.serial (reason);
	nlerror (reason.c_str());
}


bool disconnectClient(uint32 userId)
{
	map<uint32, TServiceId>::iterator it = UserIdSockAssociations.find (userId);
	if (it == UserIdSockAssociations.end ())
	{
		nlinfo ("Login service ask to disconnect user %d, he is not connected here, so ignoring", userId);
		return false;
	}
	else
	{
		CMessage msgout ("DC");
		msgout.serial (userId);
		CUnifiedNetwork::getInstance()->send (it->second, msgout);

		return true;
	}
}

void cbLSDisconnectClient (CMessage &msgin, const std::string &serviceName, TServiceId  sid)
{
	// the LS tells me that i have to disconnect a client

	uint32 userid;
	msgin.serial (userid);

	disconnectClient(userid);

}

// connection to the LS, send the identification message
void cbLSConnection (const std::string &serviceName, TServiceId  sid, void *arg)
{
	sint32 shardId;

	if (IService::getInstance()->haveArg('S'))
	{
		// use the command line param if set
		shardId = atoi(IService::getInstance()->getArg('S').c_str());
	}
	else if (IService::getInstance()->ConfigFile.exists ("ShardId"))
	{
		// use the config file param if set
		shardId = IService::getInstance()->ConfigFile.getVar ("ShardId").asInt();
	}
	else
	{
		shardId = -1;
	}

	if (shardId == -1)
	{
		nlerror ("ShardId variable must be valid (>0)");
	}

	CMessage	msgout ("WS_IDENT");
	msgout.serial (shardId);
	CUnifiedNetwork::getInstance()->send (sid, msgout);

	nlinfo ("Connected to %s-%hu and sent identification with shardId '%d'", serviceName.c_str(), sid.get(), shardId);

	// send state to LS
	setShardOpenState((TShardOpenState)(ShardOpen.get()), false);

	//
	if (!DontUseLS)
	{
		CMessage	msgrpn("REPORT_NO_PATCH");
		CUnifiedNetwork::getInstance()->send("LS", msgrpn);
	}

	bool	reportPatching = false;
	list<CFES>::iterator	itfs;
	for (itfs=FESList.begin(); itfs!=FESList.end(); ++itfs)
		(*itfs).reportStateToLS(reportPatching);
}


// Callback for detection of config file change about "ExpectedServices"
void cbUpdateExpectedServices( CConfigFile::CVar& var )
{
	OnlineServices.setExpectedInstances( var );
}


/*
 * ShardOpen update functions/callbacks etc.
 */

/**
 * updateShardOpenFromFile()
 * Update ShardOpen from a file.
 * Read a line of text in the file, converts it to int (atoi), then casts into bool for ShardOpen.
 */
void	updateShardOpenFromFile(const std::string& filename)
{
	CIFile	f;

	if (!f.open(filename))
	{
		nlwarning("Failed to update ShardOpen from file '%s', couldn't open file", filename.c_str());
		return;
	}

	try
	{
		char	readBuffer[256];
		f.getline(readBuffer, 256);
		setShardOpenState((TShardOpenState)atoi(readBuffer));

		nlinfo("Updated ShardOpen state to '%u' from file '%s'", ShardOpen.get(), filename.c_str());
	}
	catch (Exception& e)
	{
		nlwarning("Failed to update ShardOpen from file '%s', exception raised while getline() '%s'", filename.c_str(), e.what());
	}
}

std::string	ShardOpenStateFileName;

/**
 * cbShardOpen()
 * Callback for ShardOpen
 */
void	cbShardOpen(IVariable &var)
{
	setShardOpenState((TShardOpenState)(ShardOpen.get()), false);
}


/**
 * cbShardOpenStateFile()
 * Callback for ShardOpenStateFile
 */
void	cbShardOpenStateFile(IVariable &var)
{
	// remove previous file change callback
	if (!ShardOpenStateFileName.empty())
	{
		CFile::removeFileChangeCallback(ShardOpenStateFileName);
		nlinfo("Removed callback for ShardOpenStateFileName file '%s'", ShardOpenStateFileName.c_str());
	}

	ShardOpenStateFileName = var.toString();

	if (!ShardOpenStateFileName.empty())
	{
		// set new callback for the file
		CFile::addFileChangeCallback(ShardOpenStateFileName, updateShardOpenFromFile);
		nlinfo("Set callback for ShardOpenStateFileName file '%s'", ShardOpenStateFileName.c_str());

		// and update state from file...
		updateShardOpenFromFile(ShardOpenStateFileName);
	}
}

/**
 * cbUsePatchMode()
 * Callback for UsePatchMode
 */
void	cbUsePatchMode(IVariable &var)
{
	// if patch mode not set, set all fs in patching mode to accept clients now
	if (!UsePatchMode.get())
	{
		nlinfo("UsePatchMode disabled, switch all patching servers to actual frontends");

		list<CFES>::iterator	it;

		for (it=FESList.begin(); it!=FESList.end(); ++it)
		{
			if ((*it).State == PatchOnly)
			{
				(*it).setToAcceptClients();
			}
		}
	}
}


// Callback Array for message from LS
TUnifiedCallbackItem LSCallbackArray[] =
{
	{ "CS", cbLSChooseShard },
	{ "DC", cbLSDisconnectClient },
	{ "FAILED", cbFailed },
};

class CWelcomeService : public IService
{

public:

	/// Init the service, load the universal time.
	void init ()
	{
		string FrontendServiceName = ConfigFile.getVar ("FrontendServiceName").asString();

		try { FrontEndAddress = ConfigFile.getVar ("FrontEndAddress").asString(); } catch(Exception &) { }

		nlinfo ("Waiting frontend services named '%s'", FrontendServiceName.c_str());

		CUnifiedNetwork::getInstance()->setServiceUpCallback(FrontendServiceName, cbFESConnection, NULL);
		CUnifiedNetwork::getInstance()->setServiceDownCallback(FrontendServiceName, cbFESDisconnection, NULL);
		CUnifiedNetwork::getInstance()->setServiceUpCallback("*", cbServiceUp, NULL);
		CUnifiedNetwork::getInstance()->setServiceDownCallback("*", cbServiceDown, NULL);

		// add a connection to the LS
		string LSAddr;
		if (haveArg('T'))
		{
			// use the command line param if set
			LSAddr = getArg('T');
		}
		else if (ConfigFile.exists ("LSHost"))
		{
			// use the config file param if set
			LSAddr = ConfigFile.getVar("LSHost").asString();
		}

		if (haveArg('S'))
		{
			// use the command line param if set
			uint shardId = atoi(IService::getInstance()->getArg('S').c_str());

			nlinfo("Using shard id %u from command line '%s'", shardId, IService::getInstance()->getArg('S').c_str());
			anticipateShardId(shardId);
		}
		else if (ConfigFile.exists ("ShardId"))
		{
			// use the config file param if set
			uint shardId = IService::getInstance()->ConfigFile.getVar ("ShardId").asInt();

			nlinfo("Using shard id %u from config file '%s'", shardId, IService::getInstance()->ConfigFile.getVar ("ShardId").asString().c_str());
			anticipateShardId(shardId);
		}

		// the config file must have a valid address where the login service is
		nlassert(!LSAddr.empty());

		// add default port if not set by the config file
		if (LSAddr.find (":") == string::npos)
			LSAddr += ":49999";

		AllowDispatchMsgToLS = true;

		if (ConfigFile.getVarPtr("DontUseLSService") == NULL
			|| !ConfigFile.getVar("DontUseLSService").asBool())
		{
			// We are using NeL Login Service
			CUnifiedNetwork::getInstance()->addCallbackArray(LSCallbackArray, sizeof(LSCallbackArray)/sizeof(LSCallbackArray[0]));
			if (!DontUseLS)
			{
				CUnifiedNetwork::getInstance()->setServiceUpCallback("LS", cbLSConnection, NULL);
				CUnifiedNetwork::getInstance()->addService("LS", LSAddr);
			}
		}
		// List of expected service instances
		ConfigFile.setCallback( "ExpectedServices", cbUpdateExpectedServices );
		cbUpdateExpectedServices( ConfigFile.getVar( "ExpectedServices" ) );


		/*
		 * read config variable ShardOpenStateFile to update
		 *
		 */
		cbShardOpenStateFile(ShardOpenStateFile);

//		// create a welcome service module (for SU comm)
//		IModuleManager::getInstance().createModule("WelcomeService", "ws", "");
//		// plug the module in the default gateway
//		NLMISC::CCommandRegistry::getInstance().execute("ws.plug wg", InfoLog());
	}

	bool			update ()
	{
		// update the service status

		removeStatusTag("DEV_ONLY");
		removeStatusTag("RESTRICTED");
		removeStatusTag("Open");

		if (ShardOpen == 0)
			addStatusTag("DEV_ONLY");
		else if (ShardOpen == 1)
			addStatusTag("RESTRICTED");
		else if (ShardOpen == 2)
			addStatusTag("Open");

		return true;
	}

};


static const char* getCompleteServiceName(const IService* theService)
{
	static std::string s;
	s= "welcome_service";

	if (theService->haveLongArg("wsname"))
	{
		s+= "_"+theService->getLongArg("wsname");
	}

	if (theService->haveLongArg("fullwsname"))
	{
		s= theService->getLongArg("fullwsname");
	}

	return s.c_str();
}

static const char* getShortServiceName(const IService* theService)
{
	static std::string s;
	s= "WS";

	if (theService->haveLongArg("shortwsname"))
	{
		s= theService->getLongArg("shortwsname");
	}

	return s.c_str();
}

// Service instantiation
NLNET_SERVICE_MAIN( CWelcomeService, getShortServiceName(scn), getCompleteServiceName(scn), 0, FESCallbackArray, NELNS_CONFIG, NELNS_LOGS);


// welcome service module
//class CWelcomeServiceMod :
//	public CEmptyModuleCommBehav<CEmptyModuleServiceBehav<CEmptySocketBehav<CModuleBase> > >,
//	public WS::CWelcomeServiceSkel
//{
//	void onProcessModuleMessage(IModuleProxy *sender, const CMessage &message)
//	{
//		if (CWelcomeServiceSkel::onDispatchMessage(sender, message))
//			return;
//
//		nlwarning("Unknown message '%s' received by '%s'",
//			message.getName().c_str(),
//			getModuleName().c_str());
//	}
//
//
//	////// CWelcomeServiceSkel implementation
//
//	// ask the welcome service to welcome a user
//	virtual void welcomeUser(NLNET::IModuleProxy *sender, uint32 userId, const std::string &userName, const CLoginCookie &cookie, const std::string &priviledge, const std::string &exPriviledge, WS::TUserRole mode, uint32 instanceId)
//	{
//		string ret = lsChooseShard(userName,
//			cookie,
//			priviledge,
//			exPriviledge,
//			mode,
//			instanceId);
//
//		if (!ret.empty())
//		{
//			// TODO : correct this
//			string fsAddr;
//			CWelcomeServiceClientProxy wsc(sender);
//			wsc.welcomeUserResult(this, userId, ret.empty(), fsAddr);
//		}
//	}
//
//	// ask the welcome service to disconnect a user
//	virtual void disconnectUser(NLNET::IModuleProxy *sender, uint32 userId)
//	{
//		nlstop;
//	}
//
//};

namespace WS
{

	void CWelcomeServiceMod::onModuleUp(IModuleProxy *proxy)
	{
		if (proxy->getModuleClassName() == "RingSessionManager")
		{
			if (_RingSessionManager != NULL)
			{
				nlwarning("WelcomeServiceMod::onModuleUp : receiving module up for RingSessionManager '%s', but already have it as '%s', replacing it",
					proxy->getModuleName().c_str(),
					_RingSessionManager->getModuleName().c_str());
			}
			// store this module as the ring session manager
			_RingSessionManager = proxy;

			// say hello to our new friend (transmit fixed session id if set in config file)
			nlinfo("Registering welcome service module into session manager '%s'", proxy->getModuleName().c_str());
			uint32 sessionId = 0;
			CConfigFile::CVar *varFixedSessionId = IService::getInstance()->ConfigFile.getVarPtr( "FixedSessionId" );
			if ( varFixedSessionId )
				sessionId = varFixedSessionId->asInt();
			CWelcomeServiceClientProxy wscp(proxy);
			wscp.registerWS(this, IService::getInstance()->getShardId(), sessionId, OnlineServices.getOnlineStatus());

			// Send counts
			uint32 totalNbOnlineUsers = 0, totalNbPendingUsers = 0;
			for (list<CFES>::iterator it=FESList.begin(); it!=FESList.end(); ++it)
			{
				totalNbOnlineUsers += (*it).NbUser;
				totalNbPendingUsers += (*it).NbPendingUsers;
			}
			CWelcomeServiceMod::getInstance()->updateConnectedPlayerCount(totalNbOnlineUsers, totalNbPendingUsers);
		}
		else if (proxy->getModuleClassName() == "LoginService")
		{
			_LoginService = proxy;
		}
	}

	void CWelcomeServiceMod::onModuleDown(IModuleProxy *proxy)
	{
		if (_RingSessionManager == proxy)
		{
			// remove this module as the ring session manager
			_RingSessionManager = NULL;
		}
		else if (_LoginService == proxy)
			_LoginService = NULL;
	}


	void CWelcomeServiceMod::welcomeUser(NLNET::IModuleProxy *sender, uint32 charId, const std::string &userName, const CLoginCookie &cookie, const std::string &priviledge, const std::string &exPriviledge, WS::TUserRole mode, uint32 instanceId)
	{
		nldebug( "ERLOG: welcomeUser(%u,%s,%s,%s,%s,%u,%u)", charId, userName.c_str(), cookie.toString().c_str(), priviledge.c_str(), exPriviledge.c_str(), (uint)mode.getValue(), instanceId );
		string ret = lsChooseShard(userName,
									cookie,
									priviledge,
									exPriviledge,
									mode,
									instanceId,
									charId & 0xF);

		uint32 userId = charId >> 4;
		if (!ret.empty())
		{
			nldebug( "ERLOG: lsChooseShard returned an error => welcomeUserResult");
			// TODO : correct this
			string fsAddr;
			CWelcomeServiceClientProxy wsc(sender);
			wsc.welcomeUserResult(this, userId, false, fsAddr, ret);
		}
		else
		{
			nldebug( "ERLOG: lsChooseShard OK => adding to pending");
			TPendingFEResponseInfo pfri;
			pfri.WSMod = this;
			pfri.UserId = userId;
			pfri.WaiterModule = sender;
			PendingFeResponse.insert(make_pair(cookie, pfri));
		}
	}

	void CWelcomeServiceMod::pendingUserLost(const NLNET::CLoginCookie &cookie)
	{
		if (!_LoginService)
			return;

		CLoginServiceProxy ls(_LoginService);

		ls.pendingUserLost(this, cookie);
	}


	// register the module
	NLNET_REGISTER_MODULE_FACTORY(CWelcomeServiceMod, "WelcomeService");

} // namespace WS


//
// Variables
//

NLMISC_DYNVARIABLE(uint32, OnlineUsersNumber, "number of connected users on this shard")
{
	// we can only read the value
	if (get)
	{
		uint32 nbusers = 0;
		for (list<CFES>::iterator it = FESList.begin(); it != FESList.end (); it++)
		{
			nbusers += (*it).NbUser;
		}
		*pointer = nbusers;
	}
}


//
// Commands
//


NLMISC_COMMAND (frontends, "displays the list of all registered front ends", "")
{
	if(args.size() != 0) return false;

	log.displayNL ("Display the %d registered front end :", FESList.size());
	for (list<CFES>::iterator it = FESList.begin(); it != FESList.end (); it++)
	{
//		log.displayNL ("> FE %u: nb estimated users: %u nb users: %u, nb pending users : %u",
		log.displayNL ("> FE %u: nb users: %u, nb pending users : %u",
			it->SId.get(),
			it->NbUser,
			it->NbPendingUsers);
	}
	log.displayNL ("End ot the list");

	return true;
}

NLMISC_COMMAND (users, "displays the list of all registered users", "")
{
	if(args.size() != 0) return false;

	log.displayNL ("Display the %d registered users :", UserIdSockAssociations.size());
	for (map<uint32, TServiceId>::iterator it = UserIdSockAssociations.begin(); it != UserIdSockAssociations.end (); it++)
	{
		log.displayNL ("> %u SId=%u", (*it).first, (*it).second.get());
	}
	log.displayNL ("End ot the list");

	return true;
}

NLMISC_COMMAND( displayOnlineServices, "Display the online service instances", "" )
{
	OnlineServices.display( log );
	return true;
}

NLMISC_VARIABLE( bool, OnlineStatus, "Main online status of the shard" );