#include "precompiled.h"

#include <algorithm>

#include "Server.h"
#include "ServerSession.h"
#include "Network.h"
#include "JSEvents.h"

#include "scripting/ScriptableObject.h"

#include "ps/Game.h"
#include "simulation/Simulation.h"
#include "ps/Player.h"
#include "ps/CLogger.h"
#include "ps/CConsole.h"

#define LOG_CAT_NET "net"

extern CConsole *g_Console;

CNetServer *g_NetServer=NULL;

using namespace std;

// NOTE: Called in network thread
CNetServerSession *CNetServer::CreateSession(CSocketInternal *pInt)
{
	CNetServerSession *pRet=new CNetServerSession(this, pInt);

	CServerHandshake *pMsg=new CServerHandshake();
	pMsg->m_Magic=PS_PROTOCOL_MAGIC;
	pMsg->m_ProtocolVersion=PS_PROTOCOL_VERSION;
	pMsg->m_SoftwareVersion=PS_PROTOCOL_VERSION;
	pRet->Push(pMsg);

	return pRet;
}

// NOTE: Called in network thread
void CNetServer::OnAccept(const CSocketAddress &addr)
{
	LOG(NORMAL, LOG_CAT_NET, "CNetServer::OnAccept(): Accepted connection from %s port %d", addr.GetString().c_str(), addr.GetPort());

	CSocketInternal *pInt=Accept();
	CNetServerSession *pSession=CreateSession(pInt);
	UNUSED2(pSession);
}

CNetServer::CNetServer(CGame *pGame, CGameAttributes *pGameAttribs):
	m_JSI_Sessions(&m_Sessions),
	m_pGame(pGame),
	m_pGameAttributes(pGameAttribs),
	m_MaxObservers(5),
	m_LastSessionID(1),
	m_ServerPlayerName(L"Noname Server Player"),
	m_ServerName(L"Noname Server"),
	m_WelcomeMessage(L"Noname Server Welcome Message"),
	m_Port(-1)
{
	ONCE(
		ScriptingInit();
	);

	m_pGameAttributes->SetUpdateCallback(AttributeUpdate, this);
	m_pGameAttributes->SetPlayerUpdateCallback(PlayerAttributeUpdate, this);
	m_pGameAttributes->SetPlayerSlotAssignmentCallback(PlayerSlotAssignmentCallback, this);

	m_pGame->GetSimulation()->SetTurnManager(this);
	// Set an incredibly long turn length - less command batch spam that way
	for (int i=0;i<3;i++)
		CTurnManager::SetTurnLength(i, 3000);

	g_ScriptingHost.SetGlobal("g_NetServer", OBJECT_TO_JSVAL(GetScript()));
}

CNetServer::~CNetServer()
{
	g_ScriptingHost.SetGlobal("g_NetServer", JSVAL_NULL);
	
	while (m_Sessions.size() > 0)
	{
		SessionMap::iterator it=m_Sessions.begin();
		delete it->second;
		m_Sessions.erase(it);
	}
}

void CNetServer::ScriptingInit()
{
	CJSMap<SessionMap>::ScriptingInit("NetServer_SessionMap");

	AddMethod<bool, &CNetServer::JSI_Open>("open", 0);

	AddProperty(L"sessions", &CNetServer::m_JSI_Sessions);

	AddProperty(L"serverPlayerName", &CNetServer::m_ServerPlayerName);
	AddProperty(L"serverName", &CNetServer::m_ServerName);
	AddProperty(L"welcomeMessage", &CNetServer::m_WelcomeMessage);
	
	AddProperty(L"port", &CNetServer::m_Port);
	
	AddProperty(L"onChat", &CNetServer::m_OnChat);
	AddProperty(L"onClientConnect", &CNetServer::m_OnClientConnect);
	AddProperty(L"onClientDisconnect", &CNetServer::m_OnClientDisconnect);

	CJSObject<CNetServer>::ScriptingInit("NetServer");
}

bool CNetServer::JSI_Open(JSContext* UNUSED(cx), uintN UNUSED(argc), jsval* UNUSED(argv))
{
	CSocketAddress addr;
	if (m_Port == -1)
		GetDefaultListenAddress(addr);
	else
		addr=CSocketAddress(m_Port, /* m_UseIPv6 ? IPv6 : */ IPv4);
	
	PS_RESULT res=Bind(addr);
	if (res != PS_OK)
	{
		LOG(ERROR, LOG_CAT_NET, "CNetServer::JSI_Open(): Bind error: %s", res);
		return false;
	}

	return true;
}

PS_RESULT CNetServer::Bind(const CSocketAddress &address)
{
	PS_RESULT res=CServerSocket::Bind(address);
	if (res==PS_OK)
		m_ServerState=NSS_PreGame;
	return res;
}

void FillSetGameConfigCB(CStrW name, ISynchedJSProperty *prop, void *userdata)
{
	CSetGameConfig *pMsg=(CSetGameConfig *)userdata;
	size_t size=pMsg->m_Values.size();
	pMsg->m_Values.resize(size+1);
	pMsg->m_Values[size].m_Name=name;
	pMsg->m_Values[size].m_Value=prop->ToString();
}

void CNetServer::FillSetGameConfig(CSetGameConfig *pMsg)
{
	m_pGameAttributes->IterateSynchedProperties(FillSetGameConfigCB, pMsg);
}

void FillSetPlayerConfigCB(CStrW name, ISynchedJSProperty *prop, void *userdata)
{
	CSetPlayerConfig *pMsg=(CSetPlayerConfig *)userdata;
	size_t size=pMsg->m_Values.size();
	pMsg->m_Values.resize(size+1);
	pMsg->m_Values[size].m_Name=name;
	pMsg->m_Values[size].m_Value=prop->ToString();
}

void CNetServer::FillSetPlayerConfig(CSetPlayerConfig *pMsg, CPlayer *pPlayer)
{
	pMsg->m_PlayerID=pPlayer->GetPlayerID();
	pPlayer->IterateSynchedProperties(FillSetPlayerConfigCB, pMsg);
}

void CNetServer::AssignSessionID(CNetServerSession *pSession)
{
	int newID=++m_LastSessionID;
	pSession->SetID(newID);
	m_Sessions[newID]=pSession;
}

void CNetServer::AddSession(CNetServerSession *pSession)
{
	{
		CSetGameConfig *pMsg=new CSetGameConfig();
		FillSetGameConfig(pMsg);
		pSession->Push(pMsg);
	}

	// Broadcast a message for the newly added player session
	CClientConnect *pMsg=new CClientConnect();
	pMsg->m_Clients.resize(1);
	pMsg->m_Clients[0].m_SessionID=pSession->GetID();
	pMsg->m_Clients[0].m_Name=pSession->GetName();
	Broadcast(pMsg);

	pMsg=new CClientConnect();

	// Server "client"
	pMsg->m_Clients.resize(1);
	pMsg->m_Clients.back().m_SessionID=1; // Server is always 1
	pMsg->m_Clients.back().m_Name=m_ServerPlayerName;

	// All the other clients
	SessionMap::iterator it=m_Sessions.begin();
	for (;it!=m_Sessions.end();++it)
	{
		pMsg->m_Clients.push_back(CClientConnect::S_m_Clients());
		pMsg->m_Clients.back().m_SessionID=it->second->GetID();
		pMsg->m_Clients.back().m_Name=it->second->GetName();
	}
	pSession->Push(pMsg);

	// Sync player slot assignments and player attributes
	for (uint i=0;i<m_pGameAttributes->GetSlotCount();i++)
	{
		CPlayerSlot *pSlot=m_pGameAttributes->GetSlot(i);
	
		pSession->Push(CreatePlayerSlotAssignmentMessage(pSlot));
	
		if (pSlot->GetAssignment() == SLOT_SESSION)
		{
			CSetPlayerConfig *pMsg=new CSetPlayerConfig();
			FillSetPlayerConfig(pMsg, pSlot->GetPlayer());
			pSession->Push(pMsg);
		}
	}
	
	OnClientConnect(pSession);
}

void CNetServer::AttributeUpdate(CStrW name, CStrW newValue, void *userdata)
{
	CNetServer *pServer=(CNetServer *)userdata;
	g_Console->InsertMessage(L"AttributeUpdate: %ls = \"%ls\"", name.c_str(), newValue.c_str());

	CSetGameConfig *pMsg=new CSetGameConfig;
	pMsg->m_Values.resize(1);
	pMsg->m_Values[0].m_Name=name;
	pMsg->m_Values[0].m_Value=newValue;

	pServer->Broadcast(pMsg);
}

void CNetServer::PlayerAttributeUpdate(CStrW name, CStrW newValue, CPlayer *pPlayer, void *userdata)
{
	CNetServer *pServer=(CNetServer *)userdata;
	g_Console->InsertMessage(L"PlayerAttributeUpdate(%d): %ls = \"%ls\"", pPlayer->GetPlayerID(), name.c_str(), newValue.c_str());

	CSetPlayerConfig *pMsg=new CSetPlayerConfig;
	pMsg->m_PlayerID=pPlayer->GetPlayerID();
	pMsg->m_Values.resize(1);
	pMsg->m_Values[0].m_Name=name;
	pMsg->m_Values[0].m_Value=newValue;

	pServer->Broadcast(pMsg);
}

CNetMessage *CNetServer::CreatePlayerSlotAssignmentMessage(CPlayerSlot *pSlot)
{
	CAssignPlayerSlot *pMsg=new CAssignPlayerSlot();
	pMsg->m_SlotID=pSlot->GetSlotID();
	pMsg->m_SessionID=pSlot->GetSessionID();
	switch (pSlot->GetAssignment())
	{
#define CASE(_a, _b) case _a: pMsg->m_Assignment=_b; break;
		CASE(SLOT_CLOSED, PS_ASSIGN_CLOSED)
		CASE(SLOT_OPEN, PS_ASSIGN_OPEN)
		CASE(SLOT_SESSION, PS_ASSIGN_SESSION)
		//CASE(SLOT_AI, PS_ASSIGN_AI)
	}
	return pMsg;
}

void CNetServer::PlayerSlotAssignmentCallback(void *userdata, CPlayerSlot *pSlot)
{
	CNetServer *pInstance=(CNetServer *)userdata;
	if (pSlot->GetAssignment() == SLOT_SESSION)
		pSlot->GetSession()->SetPlayerSlot(pSlot);
	CNetMessage *pMsg=CreatePlayerSlotAssignmentMessage(pSlot);
	g_Console->InsertMessage(L"Player Slot Assignment: %hs\n", pMsg->GetString().c_str());
	pInstance->Broadcast(pMsg);
}

bool CNetServer::AllowObserver(CNetServerSession* UNUSED(pSession))
{
	return m_Observers.size() < m_MaxObservers;
}

void CNetServer::RemoveSession(CNetServerSession *pSession)
{
	SessionMap::iterator it=m_Sessions.find(pSession->GetID());
	if (it != m_Sessions.end())
		m_Sessions.erase(it);

	/*
	 * Player sessions require some extra care:
	 *
	 * Pre-Game: dissociate the slot that was used by the session and
	 * synchronize the disconnection of the client.
	 *
	 * In-Game: Revert all player's entities to Gaia control, awaiting the
	 * client's reconnect attempts [if/when we implement that]
	 *
	 * Post-Game: Just sync disconnection - we don't have any players anymore
	 * and all is fine.
	 *
	 * After this is done, call the JS callback if it's been set.
	 */
	if (pSession->GetPlayer())
	{
		if (m_ServerState == NSS_PreGame)
		{
			pSession->GetPlayerSlot()->AssignClosed();
		}
		else if (m_ServerState == NSS_InGame)
		{
			// TODO Reassign entities to Gaia control
			// TODO Set everything up for re-connect and resume
			SetClientPipe(pSession->GetPlayerSlot()->GetSlotID(), NULL);
			pSession->GetPlayerSlot()->AssignClosed();
		}
	}
		
	CClientDisconnect *pMsg=new CClientDisconnect();
	pMsg->m_SessionID=pSession->GetID();
	Broadcast(pMsg);

	OnClientDisconnect(pSession);

	// TODO Correct handling of observers
}

// Unfortunately, the message queueing model is made so that each message has
// to be copied once for each socket its sent over, messages are deleted when
// sent by CMessageSocket. We could ref-count, but that requires a lot of
// thread safety stuff => hard work
void CNetServer::Broadcast(CNetMessage *pMsg)
{
	if (m_Sessions.empty())
	{
		delete pMsg;
		return;
	}

	SessionMap::iterator it=m_Sessions.begin();
	// Skip one session
	++it;
	// Send a *copy* to all remaining sessions
	for (;it != m_Sessions.end();++it)
	{
		it->second->Push(pMsg->Copy());
	}
	// Now send to the first session, *not* copying the message
	m_Sessions.begin()->second->Push(pMsg);
}

int CNetServer::StartGame()
{
	Broadcast(new CStartGame());
	
	if (m_pGame->StartGame(m_pGameAttributes) != PSRETURN_OK)
		return -1;
	else
	{
		CTurnManager::Initialize(m_pGameAttributes->GetSlotCount());
		for (uint i=0;i<m_pGameAttributes->GetSlotCount();i++)
		{
			CPlayerSlot *pSlot=m_pGameAttributes->GetSlot(i);
			if (pSlot->GetAssignment() == SLOT_SESSION)
				CTurnManager::SetClientPipe(i, pSlot->GetSession());
		}
		m_ServerState=NSS_InGame;

		SessionMap::iterator it=m_Sessions.begin();
		while (it != m_Sessions.end())
		{
			it->second->StartGame();
			++it;
		}
		
		// This is the signal for everyone to start their simulations.
		SendBatch(1);

		return 0;
	}
}

void CNetServer::GetDefaultListenAddress(CSocketAddress &address)
{
	address=CSocketAddress(PS_DEFAULT_PORT, IPv4);
}

void CNetServer::NewTurn()
{
	RecordBatch(2);

	RotateBatches();
	ClearBatch(2);

	IterateBatch(1, CSimulation::GetMessageMask, m_pGame->GetSimulation());
	SendBatch(1);
	//IterateBatch(1, SendToObservers, this);
}

void CNetServer::QueueLocalCommand(CNetMessage *pMsg)
{
	QueueIncomingCommand(pMsg);
}

void CNetServer::QueueIncomingCommand(CNetMessage *pMsg)
{
	LOG(NORMAL, LOG_CAT_NET, "CNetServer::QueueIncomingCommand(): %s.", pMsg->GetString().c_str());
	QueueMessage(2, pMsg);
}

void CNetServer::OnChat(CStrW from, CStrW message)
{
	if (m_OnChat.Defined())
	{
		CChatEvent evt(from, message);
		m_OnChat.DispatchEvent(GetScript(), &evt);
	}
}

void CNetServer::OnClientConnect(CNetServerSession *pSession)
{
	if (m_OnClientConnect.Defined())
	{
		CClientConnectEvent evt(pSession);
		m_OnClientConnect.DispatchEvent(GetScript(), &evt);
	}
}

void CNetServer::OnClientDisconnect(CNetServerSession *pSession)
{
	if (m_OnClientDisconnect.Defined())
	{
		CClientDisconnectEvent evt(pSession->GetID(), pSession->GetName());
		m_OnClientDisconnect.DispatchEvent(GetScript(), &evt);
	}
}
