// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// 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/>.

#include "stdpch.h"
#include "game_share/utils.h"
#include "session_browser.h"

#include "game_share/r2_share_itf.h"

#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif

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

/// The time the client stay connected to the session browser without requests
const uint32 KeepConnectedTime = 3;

// a callback adaptor that queue the sent message for later sending.
class CCallbackClientAdaptor : public CNelCallbackClientAdaptor
{
	friend class CSessionBrowser;

	// a queue of message to be sent by the comm thread
	std::queue<CMessage>	_SendQueue;

	// The mutex used by the session browser thread
	NLMISC::CUnfairMutex	_Mutex;

	// A flag to let some message passthrue without enqueuing (for authenticate)
	bool					_PassThrue;

	CCallbackClientAdaptor(void *containerClass)
		:	CNelCallbackClientAdaptor(containerClass),
			_PassThrue(false)
	{
		_CallbackClient.setPreDispatchCallback(&CCallbackClientAdaptor::cb_preDispatchMessage);
	}

	CCallbackClient &getCallback()
	{
		return _CallbackClient;
	}

	virtual void connect( const NLNET::CInetAddress& /* addr */ )
	{
		// do not connect now
	}


	virtual void send(const NLNET::CMessage &buffer, NLNET::TSockId hostid = NLNET::InvalidSockId, bool log = true)
	{
		CAutoMutex<CUnfairMutex> mutex(_Mutex);

		if (!_PassThrue)
		{
			// queue the message for later sending.
			nldebug("SB: Pushing a buffer into SendQueue (from %u elts)", _SendQueue.size());
			_SendQueue.push(buffer);
		}
		else
		{
			// protect socket because update() can be called in the main thread while send() can be called with passthru=true in the session browser thread (by authenticate() for example)
			_CallbackClient.send(buffer, hostid, log);
		}
	}

	// called by the callback client before each message dispatching
	static void cb_preDispatchMessage(CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
	{
		CNelCallbackClientAdaptor *adaptor = static_cast<CNelCallbackClientAdaptor*>(netbase.getUserData());
		if (adaptor != NULL)
		{
			CSessionBrowser *sb = static_cast<CSessionBrowser*>(adaptor->getContainerClass());

			sb->on_preDispatchMessage(msgin, from, netbase);
		}
	}


};



///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

CSessionBrowser::CSessionBrowser()
	:	CSessionBrowserServerWebClientItf(new CCallbackClientAdaptor(this)),
		_TerminateComm(false),
		_SignalConnFail(false),
		_NBReceivedMessage(0),
		_WaitingForMessage(false)
{
	// start the comm thread
	_CommThread = IThread::create(this);
	_CommThread->start();
}

CSessionBrowser::~CSessionBrowser()
{
	// signal the comm thread to terminate
	_TerminateComm = true;
	// wait the termination of the thread
	_CommThread->wait();

	// ok, we can leave
	delete _CommThread;
}

/** Set auth info */
void CSessionBrowser::setAuthInfo(const NLNET::CLoginCookie &cookie)
{
	_LoginCookie = cookie;
}

CCallbackClientAdaptor *CSessionBrowser::getCallbackAdaptor()
{
	return static_cast<CCallbackClientAdaptor *>(_CallbackClient.get());
}

void CSessionBrowser::connectItf(NLNET::CInetAddress address)
{
	// call the interface connectItf
	CSessionBrowserServerWebClientItf::connectItf(address);
	_ServerAddr = address;
}

void CSessionBrowser::clearSendQueue()
{
	CCallbackClientAdaptor	*adaptor = getCallbackAdaptor();
	nldebug("SB: Clearing SendQueue");
	CAutoMutex<CUnfairMutex> mutex(adaptor->_Mutex);
	while (!adaptor->_SendQueue.empty())
		adaptor->_SendQueue.pop();
}

// the comm thread entry point
void CSessionBrowser::run()
{
	CCallbackClientAdaptor	*adaptor = getCallbackAdaptor();
	while(!_TerminateComm)
	{
		// no server address available
		while (!_ServerAddr.isValid())
		{
			// wait a little
			nlSleep(100);

			if (_TerminateComm)
				break;
		}

disconnected:
		// disconnected, waiting for message to send
		while (adaptor->_SendQueue.empty())
		{
			nlSleep(100);

			if (_TerminateComm)
				break;
		}

retry_connection:
		while (!adaptor->getCallback().connected())
		{
			// connecting...
			try
			{
				nldebug("SB: Connecting...");
				adaptor->getCallback().connect(_ServerAddr);
			}
			catch(...)
			{
				// connection failed !
				// erase all queued messages
				clearSendQueue();
				// put an event in for the main loop
				_SignalConnFail = true;
			}
			if (_TerminateComm)
				break;

		}

		{
			CAutoMutex<CUnfairMutex> mutex(adaptor->_Mutex);
			// authenticate the client
			 adaptor->_PassThrue = true;
			nldebug("SB: Authenticating");
			// nico patch
			{
				if (!connected()) // oddly sometimes it gets disconnected here
				{
					goto retry_connection;
				}
			}
			authenticate(_LoginCookie.getUserId(), _LoginCookie);
			adaptor->_PassThrue = false;
		}
sendMessages:
		// connected, sending messages
		while (!adaptor->_SendQueue.empty())
		{
			bool popped = false;
			{
				CAutoMutex<CUnfairMutex> mutex(adaptor->_Mutex);
				if (!adaptor->getCallback().connected())
				{
					nldebug("SB: Disconnecting when authenticating");
					clearSendQueue(); // TODO: REMOVE?
					goto disconnected;
				}

				nldebug("SB: Sending next buffer of SendQueue");
				adaptor->getCallback().send(adaptor->_SendQueue.front());
				//if (adaptor->getCallback().connected()) // don't loose msg if it has not been sent (TODO: ADD?)
				{
					adaptor->_SendQueue.pop();
					popped = true;
				}
			}
			if (popped)
				nldebug("SB: Popping from SendQueue (now %u elts)", adaptor->_SendQueue.size());
			else
				nldebug("SB: Disconnected when sending");

			if (_TerminateComm)
				break;
		}

		// idle, waiting new message to send/receive or time out to disconnect
		time_t startWait = CTime::getSecondsSince1970();
		while (_WaitingForMessage || (adaptor->_SendQueue.empty() && CTime::getSecondsSince1970() - startWait < KeepConnectedTime))
		{
			nlSleep(100);

			if (_TerminateComm)
				break;

			{
				CAutoMutex<CUnfairMutex> mutex(adaptor->_Mutex);
				if (!adaptor->_SendQueue.empty())
					goto sendMessages;

				if (!adaptor->getCallback().connected())
					goto disconnected;
			}
		}

		{
			nldebug("SB: Disconnecting");
			CAutoMutex<CUnfairMutex> mutex(adaptor->_Mutex);
//			if (!adaptor->_SendQueue.empty())
				// timeout, disconnect
				adaptor->getCallback().disconnect();
		}

		if (_TerminateComm)
			break;

		goto disconnected;
	}
}

void CSessionBrowser::on_preDispatchMessage(CMessage &msgin, TSockId /* from */, CCallbackNetBase &/* netbase */)
{
	++_NBReceivedMessage;
	_LastReceivedMessageNames.push_back(msgin.getName());
}


void CSessionBrowser::update()
{
	// check flag to callback a connection failure event
	if (_SignalConnFail)
	{
		on_connectionFailed();
		_SignalConnFail = false;
	}

	//nldebug("SB: Updating module");
	CAutoMutex<CUnfairMutex> mutex(getCallbackAdaptor()->_Mutex);
	// call the underlying interface update
	CSessionBrowserServerWebClientItf::update();
}

bool CSessionBrowser::waitOneMessage(const std::string &msgName)
{
	nldebug("SB: Waiting for message");
	_WaitingForMessage = true;
	// cleanup the list of received messages
	_LastReceivedMessageNames.clear();

	CCallbackClientAdaptor	*adaptor = getCallbackAdaptor();


	// loop while no message received, still connected or having message to send
	while (_LastReceivedMessageNames.empty()
		&& (adaptor->getCallback().connected() || !adaptor->_SendQueue.empty()))
	{
		update();

		// give up the time slice
		nlSleep(0);

	}

	_WaitingForMessage = false;

	if (!_LastReceivedMessageNames.empty())
	{
		// check the first received message match the requested one
		bool receivedExpected =
				(_LastReceivedMessageNames.front() == msgName) ||
				(_LastReceivedMessageNames.front() == "JSSRE" && (msgName == "JSSR")); // harcoded: JSSRE may replace JSSR in some responses
		STOP_IF(!receivedExpected, "BIG BAD BUG - Expected SBS message '"+msgName+"' but received '"+_LastReceivedMessageNames.front()+"'");
		return receivedExpected;
	}

	// sorry, wait failed
	return false;
}

bool CSessionBrowser::connected()
{
	return getCallbackAdaptor()->connected();
}

void CSessionBrowser::send(const NLNET::CMessage& msg)
{
	getCallbackAdaptor()->send(msg);
}