#include "precompiled.h"

#include "scripting/DOMEvent.h"
#include "scripting/JSConversions.h"
#include "scripting/ScriptableObject.h"
#include "Client.h"
#include "JSEvents.h"
#include "ps/CStr.h"
#include "ps/CLogger.h"
#include "ps/CConsole.h"
#include "ps/Game.h"
#include "ps/GameAttributes.h"
#include "simulation/Simulation.h"

#define LOG_CAT_NET "net"

CNetClient *g_NetClient=NULL;
extern int fps;

CNetClient::CServerSession::CServerSession(int sessionID, const CStrW& name):
    m_SessionID(sessionID),
    m_Name(name)
{
    ONCE( ScriptingInit(); );
}

void CNetClient::CServerSession::ScriptingInit()
{
    AddProperty(L"id", &CNetClient::CServerSession::m_SessionID, true);
    AddProperty(L"name", &CNetClient::CServerSession::m_Name, true);

    CJSObject<CServerSession>::ScriptingInit("NetClient_ServerSession");
}

CNetClient::CNetClient(CGame *pGame, CGameAttributes *pGameAttribs):
    CNetSession(ConnectHandler),
    m_JSI_ServerSessions(&m_ServerSessions),
    m_pLocalPlayerSlot(NULL),
    m_pGame(pGame),
    m_pGameAttributes(pGameAttribs)
{
    ONCE( ScriptingInit(); );

    m_pGame->GetSimulation()->SetTurnManager(this);
    
    g_ScriptingHost.SetGlobal("g_NetClient", OBJECT_TO_JSVAL(GetScript()));
}

CNetClient::~CNetClient()
{
    g_ScriptingHost.SetGlobal("g_NetClient", JSVAL_NULL);
    
    SessionMap::iterator it=m_ServerSessions.begin();
    while (it != m_ServerSessions.end())
    {
        delete it->second;
    }
    // (Isn't this an infinite loop?)
}

void CNetClient::ScriptingInit()
{
    AddMethod<bool, &CNetClient::JSI_BeginConnect>("beginConnect", 1);

    AddProperty(L"onStartGame", &CNetClient::m_OnStartGame);
    AddProperty(L"onChat", &CNetClient::m_OnChat);
    AddProperty(L"onConnectComplete", &CNetClient::m_OnConnectComplete);
    AddProperty(L"onDisconnect", &CNetClient::m_OnDisconnect);
    AddProperty(L"onClientConnect", &CNetClient::m_OnClientConnect);
    AddProperty(L"onClientDisconnect", &CNetClient::m_OnClientDisconnect);

    AddProperty(L"password", &CNetClient::m_Password);
    AddProperty<CStrW>(L"playerName", &CNetClient::m_Name);
    AddProperty(L"sessionId", &CNetClient::m_SessionID);
    
    AddProperty(L"sessions", &CNetClient::m_JSI_ServerSessions);
    CJSMap<SessionMap>::ScriptingInit("NetClient_SessionMap");
    CJSObject<CNetClient>::ScriptingInit("NetClient");
}

bool CNetClient::JSI_BeginConnect(JSContext* UNUSED(cx), uintN argc, jsval *argv)
{
    CStr connectHostName;
    uint connectPort=PS_DEFAULT_PORT;
    if (argc >= 1)
    {
        connectHostName=g_ScriptingHost.ValueToString(argv[0]);
    }
    if (argc >= 2)
    {
        connectPort=ToPrimitive<int>(argv[1]);
    }
    
    PS_RESULT res=BeginConnect(connectHostName.c_str(), connectPort);
    if (res != PS_OK)
    {
        LOG(ERROR, LOG_CAT_NET, "CNetClient::JSI_Connect(): BeginConnect error: %s", res);
        return false;
    }
    else
        return true;
}

/* TEMPLATE FOR MESSAGE HANDLERS:

bool CNetClient::<X>Handler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    switch (pMsg->GetType())
    {
    case XXX:
        break;
    default:
        UNHANDLED(pMsg);
    }
    HANDLED(pMsg);
}
*/

#define UNHANDLED(_pMsg) return false;
#define HANDLED(_pMsg) delete _pMsg; return true;
#define TAKEN(_pMsg) return true;
// Uglily assumes the arguments are called pMsg and pSession (which they are
// all through this file)
#define CHAIN(_chainHandler) STMT(if (_chainHandler(pMsg, pSession)) return true;)

bool CNetClient::ConnectHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;

    switch (pMsg->GetType())
    {
    case NMT_CONNECT_COMPLETE:
        pClient->m_pMessageHandler=HandshakeHandler;
        if (pClient->m_OnConnectComplete.Defined())
        {
            CConnectCompleteEvent evt(CStr((char *)PS_OK), true);
            pClient->m_OnConnectComplete.DispatchEvent(pClient->GetScript(), &evt);
        }
        break;
    case NMT_ERROR:
    {
        CNetErrorMessage *msg=(CNetErrorMessage *)pMsg;
        LOG(ERROR, LOG_CAT_NET, "CNetClient::ConnectHandler(): Connect Failed: %s", msg->m_Error);
        if (pClient->m_OnConnectComplete.Defined())
        {
            CConnectCompleteEvent evt(CStr(msg->m_Error), false);
            pClient->m_OnConnectComplete.DispatchEvent(pClient->GetScript(), &evt);
        }
        break;
    }
    default:
        UNHANDLED(pMsg);
    }
    HANDLED(pMsg);
}

bool CNetClient::BaseHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    switch (pMsg->GetType())
    {
    case NMT_ERROR:
    {
        CNetErrorMessage *msg=(CNetErrorMessage *)pMsg;
        if (msg->m_State == SS_UNCONNECTED)
        {
            CStr message=msg->m_Error;
            CDisconnectEvent evt(message);
            if (pClient->m_OnDisconnect.Defined())
                pClient->m_OnDisconnect.DispatchEvent(pClient->GetScript(), &evt);
        }
        break;
    }
    default:
        UNHANDLED(pMsg);
    }
    HANDLED(pMsg);
}

bool CNetClient::HandshakeHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    
    CHAIN(BaseHandler);
    
    switch (pMsg->GetType())
    {
    case NMT_ServerHandshake:
        {
            CClientHandshake *msg=new CClientHandshake();
            msg->m_MagicResponse=PS_PROTOCOL_MAGIC_RESPONSE;
            msg->m_ProtocolVersion=PS_PROTOCOL_VERSION;
            msg->m_SoftwareVersion=PS_PROTOCOL_VERSION;
            pClient->Push(msg);
            break;
        }
    case NMT_ServerHandshakeResponse:
        {
            CAuthenticate *msg=new CAuthenticate();
            msg->m_Name=pClient->m_Name;
            msg->m_Password=pClient->m_Password;

            pClient->m_pMessageHandler=AuthenticateHandler;
            pClient->Push(msg);

            break;
        }
    default:
        UNHANDLED(pMsg);
    }
    HANDLED(pMsg);
}

bool CNetClient::AuthenticateHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    
    CHAIN(BaseHandler);
    
    switch (pMsg->GetType())
    {
    case NMT_AuthenticationResult:
        {
            CAuthenticationResult *msg=(CAuthenticationResult *)pMsg;
            if (msg->m_Code != NRC_OK)
            {
                LOG(ERROR, LOG_CAT_NET, "CNetClient::AuthenticateHandler(): Authentication failed: %ls", msg->m_Message.c_str());
            }
            else
            {
                LOG(NORMAL, LOG_CAT_NET, "CNetClient::AuthenticateHandler(): Authenticated!");
                pClient->m_SessionID=msg->m_SessionID;
                pClient->m_pMessageHandler=PreGameHandler;
            }
            break;
        }
    default:
        UNHANDLED(pMsg);
    }
    HANDLED(pMsg);
}

bool CNetClient::PreGameHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    
    CHAIN(BaseHandler);
    CHAIN(ChatHandler);
    
    switch (pMsg->GetType())
    {
        case NMT_StartGame:
        {
            pClient->OnStartGameMessage();
            HANDLED(pMsg);
        }
        case NMT_ClientConnect:
        {
            CClientConnect *msg=(CClientConnect *)pMsg;
            for (uint i=0;i<msg->m_Clients.size();i++)
            {
                pClient->OnClientConnect(msg->m_Clients[i].m_SessionID,
                    msg->m_Clients[i].m_Name);
            }
            HANDLED(pMsg);
        }
        case NMT_ClientDisconnect:
        {
            CClientDisconnect *msg=(CClientDisconnect *)pMsg;
            pClient->OnClientDisconnect(msg->m_SessionID);
            HANDLED(pMsg);
        }
        case NMT_SetGameConfig:
        {
            CSetGameConfig *msg=(CSetGameConfig *)pMsg;
            for (uint i=0;i<msg->m_Values.size();i++)
            {
                pClient->m_pGameAttributes->SetValue(msg->m_Values[i].m_Name, msg->m_Values[i].m_Value);
            }
            HANDLED(pMsg);
        }
        case NMT_AssignPlayerSlot:
        {
            CAssignPlayerSlot *msg=(CAssignPlayerSlot *)pMsg;
            // FIXME Validate slot id to prevent us from going boom
            CPlayerSlot *pSlot=pClient->m_pGameAttributes->GetSlot(msg->m_SlotID);
            if (pSlot == pClient->m_pLocalPlayerSlot)
                pClient->m_pLocalPlayerSlot=NULL;
            switch (msg->m_Assignment)
            {
                case PS_ASSIGN_SESSION:
                    if ((int)msg->m_SessionID == (int)pClient->m_SessionID) // squelch bogus sign mismatch warning
                        pClient->m_pLocalPlayerSlot=pSlot;
                    pSlot->AssignToSessionID(msg->m_SessionID);
                    break;
                case PS_ASSIGN_CLOSED:
                    pSlot->AssignClosed();
                    break;
                case PS_ASSIGN_OPEN:
                    pSlot->AssignOpen();
                    break;
                default:
                    LOG(WARNING, LOG_CAT_NET, "CNetClient::PreGameHandler(): Received an invalid slot assignment %s", msg->GetString().c_str());
            }
            HANDLED(pMsg);
        }
        case NMT_SetPlayerConfig:
        {
            CSetPlayerConfig *msg=(CSetPlayerConfig *)pMsg;
            // FIXME Check player ID
            CPlayer *pPlayer=pClient->m_pGameAttributes->GetPlayer(msg->m_PlayerID);
            for (uint i=0;i<msg->m_Values.size();i++)
            {
                pPlayer->SetValue(msg->m_Values[i].m_Name, msg->m_Values[i].m_Value);
            }
            HANDLED(pMsg);
        }
        default:
        {
            UNHANDLED(pMsg);
        }
    }
}

bool CNetClient::InGameHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    ENetMessageType msgType=pMsg->GetType();

    CHAIN(BaseHandler);
    CHAIN(ChatHandler);

    if (msgType >= NMT_COMMAND_FIRST && msgType < NMT_COMMAND_LAST)
    {
        pClient->QueueMessage(1, pMsg);
        TAKEN(pMsg);
    }

    switch (msgType)
    {
    case NMT_EndCommandBatch:
    {
        CEndCommandBatch *msg=(CEndCommandBatch *)pMsg;
        pClient->SetTurnLength(1, msg->m_TurnLength);
    
        // FIXME When the command batch has ended, we should start accepting
        // commands for the next turn. This will be accomplished by calling
        // NewTurn. *BUT* we shouldn't prematurely proceed game simulation
        // since this will produce jerky playback (everything expects a sim
        // turn to have a certain duration).

        // We should make sure that any commands received after this message
        // are queued in the next batch (#2 instead of #1). If we're already
        // putting everything new in batch 2 - we should fast-forward a bit to
        // catch up with the server.

        HANDLED(pMsg);
    }
        
    default:
        UNHANDLED(pMsg);
    }
}

bool CNetClient::ChatHandler(CNetMessage *pMsg, CNetSession *pSession)
{
    CNetClient *pClient=(CNetClient *)pSession;
    switch (pMsg->GetType())
    {
    case NMT_ChatMessage:
    {
        CChatMessage *msg=(CChatMessage *)pMsg;
        g_Console->ReceivedChatMessage(msg->m_Sender, msg->m_Message);
        if (pClient->m_OnChat.Defined())
        {
            CChatEvent evt(msg->m_Sender, msg->m_Message);
            pClient->m_OnChat.DispatchEvent(pClient->GetScript(), &evt);
        }
        HANDLED(pMsg);
    }
    default:
        UNHANDLED(pMsg);
    }
}

void CNetClient::OnClientConnect(int sessionID, const CStrW& name)
{
    // Find existing server session, if any, and delete it
    SessionMap::iterator it=m_ServerSessions.find(sessionID);
    if (it != m_ServerSessions.end())
    {
        delete it->second;
        m_ServerSessions.erase(it);
    }
    
    // Insert new serversession
    m_ServerSessions[sessionID]=new CServerSession(sessionID, name);
    
    // Call JS Callback
    if (m_OnClientConnect.Defined())
    {
        CClientConnectEvent evt(sessionID, name);
        m_OnClientConnect.DispatchEvent(GetScript(), &evt);
    }
}

void CNetClient::OnClientDisconnect(int sessionID)
{
    SessionMap::iterator it=m_ServerSessions.find(sessionID);
    if (it == m_ServerSessions.end())
    {
        LOG(WARNING, LOG_CAT_NET, "Server said session %d disconnected. I don't know of such a session.");
    }
    
    // Call JS Callback
    if (m_OnClientConnect.Defined())
    {
        CClientDisconnectEvent evt(it->second->m_SessionID, it->second->m_Name);
        m_OnClientConnect.DispatchEvent(GetScript(), &evt);
    }
    
    // Delete server session and remove from map
    delete it->second;
    m_ServerSessions.erase(it);
}

void CNetClient::OnStartGameMessage()
{
    m_pMessageHandler=InGameHandler;
    debug_assert( m_OnStartGame.Defined() );
    CStartGameEvent evt;
    m_OnStartGame.DispatchEvent(GetScript(), &evt);
}

int CNetClient::StartGame()
{
    if (m_pGame->StartGame(m_pGameAttributes) != PSRETURN_OK)
    {
        return -1;
    }
    else
    {
        return 0;
    }
}

CPlayer* CNetClient::GetLocalPlayer()
{
    return m_pLocalPlayerSlot->GetPlayer();
}

void CNetClient::NewTurn()
{
    RotateBatches();
    ClearBatch(2);

    CEndCommandBatch *pMsg=new CEndCommandBatch();
    pMsg->m_TurnLength=1000/fps;
    Push(pMsg);
}

void CNetClient::QueueLocalCommand(CNetMessage *pMsg)
{
    // Don't save these locally, since they'll be bounced by the server anyway
    Push(pMsg);
}