// client.cpp, mostly network related client game code

#include "cube.h"
#include "bot/bot.h"

VAR(connected, 1, 0, 0);

ENetHost *clienthost = NULL;
ENetPeer *curpeer = NULL, *connpeer = NULL;
int connmillis = 0, connattempts = 0, discmillis = 0;
bool c2sinit = false;       // whether we need to tell the other clients our stats

int getclientnum() { return player1 ? player1->clientnum : -1; }

bool multiplayer(bool msg)
{
    // check not correct on listen server?
    if(curpeer && msg) conoutf("operation not available in multiplayer");
    return curpeer!=NULL;
}

bool allowedittoggle()
{
    bool allow = !curpeer || gamemode==1;
    if(!allow) conoutf("editing in multiplayer requires coopedit mode (1)");
    return allow; 
}

void setrate(int rate)
{
   if(!curpeer) return;
   enet_host_bandwidth_limit(clienthost, rate, rate);
}

VARF(rate, 0, 0, 25000, setrate(rate));

void throttle();

VARF(throttle_interval, 0, 5, 30, throttle());
VARF(throttle_accel,    0, 2, 32, throttle());
VARF(throttle_decel,    0, 2, 32, throttle());

void throttle()
{
    if(!curpeer) return;
    ASSERT(ENET_PEER_PACKET_THROTTLE_SCALE==32);
    enet_peer_throttle_configure(curpeer, throttle_interval*1000, throttle_accel, throttle_decel);
}

string clientpassword = "";

void abortconnect()
{
    if(!connpeer) return;
    if(connpeer->state!=ENET_PEER_STATE_DISCONNECTED) enet_peer_reset(connpeer);
    connpeer = NULL;
    if(curpeer) return;
    enet_host_destroy(clienthost);
    clienthost = NULL;
}

void connects(char *servername, char *password)
{   
    if(connpeer)
    {
        conoutf("aborting connection attempt");
        abortconnect();
    }

    s_strcpy(clientpassword, password ? password : "");

    ENetAddress address;
    address.port = CUBE_SERVER_PORT;

    if(servername)
    {
        addserver(servername);
        conoutf("attempting to connect to %s", servername);
        if(!resolverwait(servername, &address))
        {
            conoutf("\f3could not resolve server %s", servername);
            return;
        }
    }
    else
    {
        conoutf("attempting to connect over LAN");
        address.host = ENET_HOST_BROADCAST;
    }

    if(!clienthost) clienthost = enet_host_create(NULL, 2, rate, rate);

    if(clienthost)
    {
        connpeer = enet_host_connect(clienthost, &address, 3); 
        enet_host_flush(clienthost);
        connmillis = lastmillis;
        connattempts = 0;
    }
    else conoutf("\f3could not connect to server");
}

void connectadmin(char *servername, char *password)
{
    if(!password) return;
    connects(servername, password);
    if(clienthost) addmsg(SV_SETADMIN, "ris", 1, password); // in case the server is not private locked or pwd protected
}

void lanconnect()
{
    connects(0);
}  

void disconnect(int onlyclean, int async)
{
    bool cleanup = onlyclean!=0;
    if(curpeer)
    {
        if(!discmillis)
        {
            enet_peer_disconnect(curpeer, DISC_NONE);
            enet_host_flush(clienthost);
            discmillis = lastmillis;
        }
        if(curpeer->state!=ENET_PEER_STATE_DISCONNECTED)
        {
            if(async) return;
            enet_peer_reset(curpeer);
        }
        curpeer = NULL;
        discmillis = 0;
        connected = 0;
        conoutf("disconnected");
        cleanup = true;
    }
    else if(demoplayback)
    {
        // todo: additional cleanups
        cleanup = true;
    }

    if(cleanup)
    {
        stop();
        c2sinit = false;
        player1->clientnum = -1;
        player1->lifesequence = 0;
        player1->clientrole = CR_DEFAULT;
        if(m_botmode) BotManager.EndMap();
        loopv(players) zapplayer(players[i]);
        localdisconnect();
    }
    if(!connpeer && clienthost)
    {
        enet_host_destroy(clienthost);
        clienthost = NULL;
    }
    if(!onlyclean) localconnect();
}

void trydisconnect()
{
    if(connpeer)
    {
        conoutf("aborting connection attempt");
        abortconnect();
        return;
    }
    if(!curpeer)
    {
        conoutf("not connected");
        return;
    }
    conoutf("attempting to disconnect...");
    disconnect(0, !discmillis);
}

void toserver(char *text) 
{ 
    bool toteam = text && text[0] == '%';
    if(toteam) text++;
    conoutf("%s:\f%d %s", colorname(player1), toteam ? 1 : 0, text);
    addmsg(toteam ? SV_TEAMTEXT : SV_TEXT, "rs", text);
}

void echo(char *text) { conoutf("%s", text); }

COMMAND(echo, ARG_VARI);
COMMANDN(say, toserver, ARG_VARI);
COMMANDN(connect, connects, ARG_2STR);
COMMAND(connectadmin, ARG_2STR);
COMMAND(lanconnect, ARG_NONE);
COMMANDN(disconnect, trydisconnect, ARG_NONE);

// collect c2s messages conveniently

vector<uchar> messages;

void addmsg(int type, const char *fmt, ...)
{
    if(demoplayback) return;
    static uchar buf[MAXTRANS];
    ucharbuf p(buf, MAXTRANS);
    putint(p, type);
    int numi = 1, nums = 0;
    bool reliable = false;
    if(fmt)
    {
        va_list args;
        va_start(args, fmt);
        while(*fmt) switch(*fmt++)
        {
            case 'r': reliable = true; break;
            case 'i':
            {
                int n = isdigit(*fmt) ? *fmt++-'0' : 1;
                loopi(n) putint(p, va_arg(args, int));
                numi += n;
                break;
            }
            case 's': sendstring(va_arg(args, const char *), p); nums++; break;
        }
        va_end(args);
    }
    int num = nums?0:numi;
    if(num!=msgsizelookup(type)) { s_sprintfd(s)("inconsistant msg size for %d (%d != %d)", type, num, msgsizelookup(type)); fatal(s); }
    int len = p.length();
    messages.add(len&0xFF);
    messages.add((len>>8)|(reliable ? 0x80 : 0));
    loopi(len) messages.add(buf[i]);
}

static int lastupdate = 0, lastping = 0, laststate = -1;
bool senditemstoserver = false;     // after a map change, since server doesn't have map data

void sendpackettoserv(int chan, ENetPacket *packet)
{
    if(curpeer) enet_peer_send(curpeer, chan, packet);
    else localclienttoserver(chan, packet);
}

void c2skeepalive()
{
    if(clienthost) enet_host_service(clienthost, NULL, 0);
}

extern string masterpwd;

void c2sinfo(playerent *d)                  // send update to the server
{
    if(d->clientnum<0) return;              // we haven't had a welcome message from the server yet
    if(lastmillis-lastupdate<40) return;    // don't update faster than 25fps
    
    bool hasmsg = gun_changed || senditemstoserver || !c2sinit || messages.length() || lastmillis-lastping>250;
    // limit updates for dead players to the ping rate of 4fps, as above
    if(!hasmsg && laststate==CS_DEAD && player1->state==CS_DEAD) return;
    
    laststate = player1->state;

    ENetPacket *packet = enet_packet_create(NULL, 100, 0);
    ucharbuf q(packet->data, packet->dataLength);

    putint(q, SV_POS);
    putint(q, d->clientnum);
    putuint(q, (int)(d->o.x*DMF));       // quantize coordinates to 1/16th of a cube, between 1 and 3 bytes
    putuint(q, (int)(d->o.y*DMF));
    putuint(q, (int)(d->o.z*DMF));
    putuint(q, (int)(d->yaw*DAF));
    putint(q, (int)(d->pitch*DAF));
    putint(q, (int)(d->roll*DAF));
    putint(q, (int)(d->vel.x*DVF));     // quantize to 1/100, almost always 1 byte
    putint(q, (int)(d->vel.y*DVF));
    putint(q, (int)(d->vel.z*DVF));
    // pack rest in 1 int: strafe:2, move:2, onfloor:1, state:3, onladder: 1
    putint(q, (d->strafe&3) | ((d->move&3)<<2) | (((int)d->onfloor)<<4) | ((editmode ? CS_EDITING : d->state)<<5) | (((int)d->onladder)<<8) );

    enet_packet_resize(packet, q.length());
    incomingdemodata(0, q.buf, q.length(), true);
    sendpackettoserv(0, packet);

    bool serveriteminitdone = false;
    if(hasmsg)
    {
        packet = enet_packet_create (NULL, MAXTRANS, 0);
        ucharbuf p(packet->data, packet->dataLength);
    
        if(!c2sinit)    // tell other clients who I am
        {
            packet->flags = ENET_PACKET_FLAG_RELIABLE;
            c2sinit = true;
            if(clientpassword[0])
            {
                putint(p, SV_PWD);
                sendstring(clientpassword, p);
                clientpassword[0] = 0;
            }
            putint(p, SV_INITC2S);
            sendstring(player1->name, p);
            sendstring(player1->team, p);
            putint(p, player1->skin);
            putint(p, player1->lifesequence);
        }
        if(gun_changed)
        {
            packet->flags = ENET_PACKET_FLAG_RELIABLE;
            putint(p, SV_WEAPCHANGE);
            putint(p, player1->gunselect);
            gun_changed = false;       
        }
        if(senditemstoserver)
        {
            packet->flags = ENET_PACKET_FLAG_RELIABLE;
            putint(p, SV_ITEMLIST);
            if(!m_noitems) putitems(p);
            putint(p, -1);
            senditemstoserver = false;
            serveriteminitdone = true;
        }
        int i = 0;
        while(i < messages.length()) // send messages collected during the previous frames
        {
            int len = messages[i] | ((messages[i+1]&0x7F)<<8);
            if(p.remaining() < len) break;
            if(messages[i+1]&0x80) packet->flags = ENET_PACKET_FLAG_RELIABLE;
            p.put(&messages[i+2], len);
            i += 2 + len;
        }
        messages.remove(0, i);
        if(lastmillis-lastping>250)
        {
            putint(p, SV_PING);
            putint(p, lastmillis);
            lastping = lastmillis;
        }
        if(!p.length()) enet_packet_destroy(packet);
        else
        {
            enet_packet_resize(packet, p.length());
            incomingdemodata(42, p.buf, p.length());
            sendpackettoserv(1, packet);
        }
    }
    if(clienthost) enet_host_flush(clienthost);
    lastupdate = lastmillis;
    if(serveriteminitdone) loadgamerest();  // hack
}

void gets2c()           // get updates from the server
{
    ENetEvent event;
    if(!clienthost) return;
    if(connpeer && lastmillis/3000 > connmillis/3000)
    {
        conoutf("attempting to connect...");
        connmillis = lastmillis;
        ++connattempts;
        if(connattempts > 3)
        {
            conoutf("\f3could not connect to server");
            abortconnect();
            return;
        }
    }
    while(clienthost!=NULL && enet_host_service(clienthost, &event, 0)>0)
    switch(event.type)
    {
        case ENET_EVENT_TYPE_CONNECT:
            disconnect(1);
            curpeer = connpeer;
            connpeer = NULL;
            connected = 1;
            conoutf("connected to server");
            throttle();
            if(rate) setrate(rate);
            if(editmode) toggleedit();
            break;
         
        case ENET_EVENT_TYPE_RECEIVE:
            if(discmillis) conoutf("attempting to disconnect...");
            else localservertoclient(event.channelID, event.packet->data, (int)event.packet->dataLength);
            enet_packet_destroy(event.packet);
            break;

        case ENET_EVENT_TYPE_DISCONNECT:
            extern const char *disc_reason(int reason);
            if(event.data>=DISC_NUM) event.data = DISC_NONE;
            if(event.peer==connpeer)
            {
                conoutf("\f3could not connect to server");
                abortconnect();
            }
            else
            {
                if(!discmillis || event.data) conoutf("\f3server network error, disconnecting (%s) ...", disc_reason(event.data));
                disconnect();
            }
            return;

        default:
            break;
    }
}

// sendmap/getmap commands, should be replaced by more intuitive map downloading

cvector securemaps;

void clearsecuremaps() { securemaps.deletecontentsa(); }
void securemap(char *map) { if(map) securemaps.add(newstring(map)); }
bool securemapcheck(char *map)
{
    if(strstr(map, "maps/")==map || strstr(map, "maps\\")==map) map += strlen("maps/");
    loopv(securemaps) if(!strcmp(securemaps[i], map))
    {
        conoutf("\f3map %s is secured, you can not send or overwrite it", map);
        return true;
    }
    return false;
}


void sendmap(char *mapname)
{
    if(*mapname)
    {
        save_world(mapname);
        changemap(mapname);
    }    
    mapname = getclientmap();
    if(securemapcheck(mapname)) return;
    int mapsize;
    uchar *mapdata = readmap(mapname, &mapsize); 
    if(!mapdata) return;
    ENetPacket *packet = enet_packet_create(NULL, MAXTRANS + mapsize, ENET_PACKET_FLAG_RELIABLE);
    ucharbuf p(packet->data, packet->dataLength);
    putint(p, SV_SENDMAP);
    sendstring(mapname, p);
    putint(p, mapsize);
    if(65535 - p.length() < mapsize)
    {
        conoutf("map %s is too large to send", mapname);
        delete[] mapdata;
        enet_packet_destroy(packet);
        return;
    }
    p.put(mapdata, mapsize);
    delete[] mapdata; 
    enet_packet_resize(packet, p.length());
    sendpackettoserv(2, packet);
    conoutf("sending map %s to server...", mapname);
    s_sprintfd(msg)("[map %s uploaded to server, \"getmap\" to receive it]", mapname);
    toserver(msg);
}

void getmap()
{
    if(securemapcheck(getclientmap())) return;
    ENetPacket *packet = enet_packet_create(NULL, MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
    ucharbuf p(packet->data, packet->dataLength);
    putint(p, SV_RECVMAP);
    enet_packet_resize(packet, p.length());
    sendpackettoserv(2, packet);
    conoutf("requesting map from server...");
}

COMMAND(sendmap, ARG_1STR);
COMMAND(getmap, ARG_NONE);
COMMAND(clearsecuremaps, ARG_NONE);
COMMAND(securemap, ARG_1STR);

