/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2010-2014 QuakeSpasm developers

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// cl_main.c  -- client main loop

#include "quakedef.hpp"
#include "bgmusic.hpp"

// we need to declare some mouse variables here, because the menu system
// references them even when on a unix system.

// these two are not intended to be set directly
convar cl_name{"_cl_name", "player", {.archive = true}};
convar cl_color{"_cl_color", "0", {.archive = true}};

convar cl_shownet{"cl_shownet", "0"}; // can be 0, 1, or 2
convar cl_nolerp{"cl_nolerp", "0"};

convar cfg_unbindall{"cfg_unbindall", "1", {.archive = true}};

convar lookspring{"lookspring", "0", {.archive = true}};
convar lookstrafe{"lookstrafe", "0", {.archive = true}};
convar sensitivity{"sensitivity", "3", {.archive = true}};

convar m_pitch{"m_pitch", "0.022", {.archive = true}};
convar m_yaw{"m_yaw", "0.022", {.archive = true}};
convar m_forward{"m_forward", "1", {.archive = true}};
convar m_side{"m_side", "0.8", {.archive = true}};

convar cl_maxpitch{"cl_maxpitch", "90", {.archive = true}}; //johnfitz -- variable pitch clamping
convar cl_minpitch{"cl_minpitch", "-90", {.archive = true}}; //johnfitz -- variable pitch clamping

convar cl_startdemos{"cl_startdemos", "1", {.archive = true}};

client_static_t cls;
client_state_t cl;
// FIXME: put these on hunk?
entity_t cl_static_entities[MAX_STATIC_ENTITIES];
lightstyle_t cl_lightstyle[MAX_LIGHTSTYLES];
dlight_t cl_dlights[MAX_DLIGHTS];

entity_t *cl_entities; //johnfitz -- was a static array, now on hunk
int cl_max_edicts; //johnfitz -- only changes when new map loads

int cl_numvisedicts;
entity_t *cl_visedicts[MAX_VISEDICTS];

extern convar r_lerpmodels, r_lerpmove; //johnfitz

/*
=====================
CL_ClearState

=====================
*/
void CL_ClearState(void) {
    if (!sv.active)
        Host_ClearMemory();

    // wipe the entire cl structure
    memset(&cl, 0, sizeof(cl));

    cls.message = net::msg{};

    // clear other arrays
    memset(cl_dlights, 0, sizeof(cl_dlights));
    memset(cl_lightstyle, 0, sizeof(cl_lightstyle));
    memset(cl_temp_entities, 0, sizeof(cl_temp_entities));
    memset(cl_beams, 0, sizeof(cl_beams));

    //johnfitz -- cl_entities is now dynamically allocated
    cl_max_edicts = std::clamp((int) max_edicts.value,MIN_EDICTS, MAX_EDICTS);
    cl_entities = (entity_t *) Hunk_AllocName(cl_max_edicts * sizeof(entity_t), "cl_entities");
    //johnfitz
}

/*
=====================
CL_Disconnect

Sends a disconnect message to the server
This is also called on Host_Error, so it shouldn't cause any errors
=====================
*/
void CL_Disconnect(void) {
    if (key_dest == key_message)
        Key_EndChat(); // don't get stuck in chat mode

    // stop sounds (especially looping!)
    S_StopAllSounds(true);
    music::stop();
    CDAudio_Stop();

    // if running a local server, shut it down
    if (cls.demoplayback)
        CL_StopPlayback();
    else if (cls.state == ca_connected) {
        if (cls.demorecording)
            CL_Stop_f();

        console::debug("Sending clc_disconnect\n");
        cls.message.clear();
        cls.message.write_byte(clc_disconnect);
        NET_SendUnreliableMessage(cls.netcon, cls.message);
        cls.message.clear();
        NET_Close(cls.netcon);

        cls.state = ca_disconnected;
        if (sv.active)
            Host_ShutdownServer(false);
    }

    cls.demoplayback = cls.timedemo = false;
    cls.demopaused = false;
    cl.intermission = 0;
    CL_ClearSignons();
}

void CL_Disconnect_f(void) {
    CL_Disconnect();
    if (sv.active)
        Host_ShutdownServer(false);
}


/*
=====================
CL_EstablishConnection

Host should be either "local" or a net address to be passed on
=====================
*/
void CL_EstablishConnection(const char *host) {
    if (cls.state == ca_dedicated)
        return;

    if (cls.demoplayback)
        return;

    CL_Disconnect();

    cls.netcon = NET_Connect(host);
    if (!cls.netcon)
        Host_Error("CL_Connect: connect failed");
    console::debug("CL_EstablishConnection: connected to %s\n", host);

    cls.demonum = -1; // not in the demo loop now
    cls.state = ca_connected;
    CL_ClearSignons(); // need all the signon messages before playing
    cls.message.write_byte(clc_nop); // NAT Fix from ProQuake
}

/*
=====================
CL_SignonReply

An svc_signonnum has been received, perform a client side setup
=====================
*/
void CL_SignonReply(void) {
    char str[8192];

    console::debug("CL_SignonReply: %i\n", cls.signon);

    switch (cls.signon) {
        case 1:
            cls.message.write_byte(clc_stringcmd);
            cls.message.write_string("prespawn");
            break;

        case 2:
            cls.message.write_byte(clc_stringcmd);
            cls.message.write_string(va("name \"%s\"\n", cl_name.string));

            cls.message.write_byte(clc_stringcmd);
            cls.message.write_string(
                            va("color %i %i\n", ((int) cl_color.value) >> 4, ((int) cl_color.value) & 15));

            cls.message.write_byte(clc_stringcmd);
            sprintf(str, "spawn %s", cls.spawnparms);
            cls.message.write_string(str);
            break;

        case 3:
            cls.message.write_byte(clc_stringcmd);
            cls.message.write_string("begin");
            Cache_Report(); // print remaining memory
            break;

        case 4:
            SCR_EndLoadingPlaque(); // allow normal screen updates
            break;
    }
}

/*
=====================
CL_NextDemo

Called to play the next demo in the demo loop
=====================
*/
void CL_NextDemo(void) {
    char str[1024];

    if (cls.demonum == -1)
        return; // don't play demos

    if (!cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS) {
        cls.demonum = 0;
        if (!cls.demos[cls.demonum][0]) {
            console::info("No demos listed with startdemos\n");
            cls.demonum = -1;
            CL_Disconnect();
            return;
        }
    }

    SCR_BeginLoadingPlaque();

    sprintf(str, "playdemo %s\n", cls.demos[cls.demonum]);
    command::buffer::insert_text(str);
    cls.demonum++;
}

/*
==============
CL_PrintEntities_f
==============
*/
void CL_PrintEntities_f(void) {
    entity_t *ent;
    int i;

    if (cls.state != ca_connected)
        return;

    for (i = 0, ent = cl_entities; i < cl.num_entities; i++, ent++) {
        console::info("%3i:", i);
        if (!ent->model) {
            console::info("EMPTY\n");
            continue;
        }
        console::info("%s:%2i  (%5.1f,%5.1f,%5.1f) [%5.1f %5.1f %5.1f]\n"
                   , ent->model->name, ent->frame, ent->origin[0], ent->origin[1], ent->origin[2], ent->angles[0],
                   ent->angles[1], ent->angles[2]);
    }
}

/*
===============
CL_AllocDlight

===============
*/
dlight_t *CL_AllocDlight(int key) {
    int i;
    dlight_t *dl;

    // first look for an exact key match
    if (key) {
        dl = cl_dlights;
        for (i = 0; i < MAX_DLIGHTS; i++, dl++) {
            if (dl->key == key) {
                memset(dl, 0, sizeof(*dl));
                dl->key = key;
                dl->color[0] = dl->color[1] = dl->color[2] = 1; //johnfitz -- lit support via lordhavoc
                return dl;
            }
        }
    }

    // then look for anything else
    dl = cl_dlights;
    for (i = 0; i < MAX_DLIGHTS; i++, dl++) {
        if (dl->die < cl.time) {
            memset(dl, 0, sizeof(*dl));
            dl->key = key;
            dl->color[0] = dl->color[1] = dl->color[2] = 1; //johnfitz -- lit support via lordhavoc
            return dl;
        }
    }

    dl = &cl_dlights[0];
    memset(dl, 0, sizeof(*dl));
    dl->key = key;
    dl->color[0] = dl->color[1] = dl->color[2] = 1; //johnfitz -- lit support via lordhavoc
    return dl;
}


/*
===============
CL_DecayLights

===============
*/
void CL_DecayLights(void) {
    int i;
    dlight_t *dl;
    float time;

    time = cl.time - cl.oldtime;

    dl = cl_dlights;
    for (i = 0; i < MAX_DLIGHTS; i++, dl++) {
        if (dl->die < cl.time || !dl->radius)
            continue;

        dl->radius -= time * dl->decay;
        if (dl->radius < 0)
            dl->radius = 0;
    }
}


/*
===============
CL_LerpPoint

Determines the fraction between the last two messages that the objects
should be put at.
===============
*/
float CL_LerpPoint(void) {
    float f, frac;

    f = cl.mtime[0] - cl.mtime[1];

    if (!f || cls.timedemo || sv.active) {
        cl.time = cl.mtime[0];
        return 1;
    }

    if (f > 0.1) // dropped packet, or start of demo
    {
        cl.mtime[1] = cl.mtime[0] - 0.1;
        f = 0.1;
    }

    frac = (cl.time - cl.mtime[1]) / f;

    if (frac < 0) {
        if (frac < -0.01)
            cl.time = cl.mtime[1];
        frac = 0;
    } else if (frac > 1) {
        if (frac > 1.01)
            cl.time = cl.mtime[0];
        frac = 1;
    }

    //johnfitz -- better nolerp behavior
    if (cl_nolerp.value)
        return 1;
    //johnfitz

    return frac;
}

/*
===============
CL_RelinkEntities
===============
*/
void CL_RelinkEntities(void) {
    entity_t *ent;
    int i, j;
    float frac, f, d;
    vec3_t delta;
    float bobjrotate;
    vec3_t oldorg;
    dlight_t *dl;

    // determine partial update time
    frac = CL_LerpPoint();

    cl_numvisedicts = 0;

    //
    // interpolate player info
    //
    for (i = 0; i < 3; i++)
        cl.velocity[i] = cl.mvelocity[1][i] +
                         frac * (cl.mvelocity[0][i] - cl.mvelocity[1][i]);

    if (cls.demoplayback) {
        // interpolate the angles
        for (j = 0; j < 3; j++) {
            d = cl.mviewangles[0][j] - cl.mviewangles[1][j];
            if (d > 180)
                d -= 360;
            else if (d < -180)
                d += 360;
            cl.viewangles[j] = cl.mviewangles[1][j] + frac * d;
        }
    }

    bobjrotate = anglemod(100 * cl.time);

    // start on the entity after the world
    for (i = 1, ent = cl_entities + 1; i < cl.num_entities; i++, ent++) {
        if (!ent->model) {
            // empty slot

            // ericw -- efrags are only used for static entities in GLQuake
            // ent can't be static, so this is a no-op.
            //if (ent->forcelink)
            //	R_RemoveEfrags (ent);	// just became empty
            continue;
        }

        // if the object wasn't included in the last packet, remove it
        if (ent->msgtime != cl.mtime[0]) {
            ent->model = NULL;
            ent->lerpflags |= LERP_RESETMOVE | LERP_RESETANIM;
            //johnfitz -- next time this entity slot is reused, the lerp will need to be reset
            continue;
        }

        VectorCopy(ent->origin, oldorg);

        if (ent->forcelink) {
            // the entity was not updated in the last message
            // so move to the final spot
            VectorCopy(ent->msg_origins[0], ent->origin);
            VectorCopy(ent->msg_angles[0], ent->angles);
        } else {
            // if the delta is large, assume a teleport and don't lerp
            f = frac;
            for (j = 0; j < 3; j++) {
                delta[j] = ent->msg_origins[0][j] - ent->msg_origins[1][j];
                if (delta[j] > 100 || delta[j] < -100) {
                    f = 1; // assume a teleportation, not a motion
                    ent->lerpflags |= LERP_RESETMOVE; //johnfitz -- don't lerp teleports
                }
            }

            //johnfitz -- don't cl_lerp entities that will be r_lerped
            if (r_lerpmove.value && (ent->lerpflags & LERP_MOVESTEP))
                f = 1;
            //johnfitz

            // interpolate the origin and angles
            for (j = 0; j < 3; j++) {
                ent->origin[j] = ent->msg_origins[1][j] + f * delta[j];

                d = ent->msg_angles[0][j] - ent->msg_angles[1][j];
                if (d > 180)
                    d -= 360;
                else if (d < -180)
                    d += 360;
                ent->angles[j] = ent->msg_angles[1][j] + f * d;
            }
        }

        // rotate binary objects locally
        if (ent->model->flags & EF_ROTATE)
            ent->angles[1] = bobjrotate;

        if (ent->effects & EF_BRIGHTFIELD)
            R_EntityParticles(ent);

        if (ent->effects & EF_MUZZLEFLASH) {
            vec3_t fv, rv, uv;

            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->origin[2] += 16;
            AngleVectors(ent->angles, fv, rv, uv);

            VectorMA(dl->origin, 18, fv, dl->origin);
            dl->radius = 200 + (rand() & 31);
            dl->minlight = 32;
            dl->die = cl.time + 0.1;

            //johnfitz -- assume muzzle flash accompanied by muzzle flare, which looks bad when lerped
            if (r_lerpmodels.value != 2) {
                if (ent == &cl_entities[cl.viewentity])
                    cl.viewent.lerpflags |= LERP_RESETANIM | LERP_RESETANIM2; //no lerping for two frames
                else
                    ent->lerpflags |= LERP_RESETANIM | LERP_RESETANIM2; //no lerping for two frames
            }
            //johnfitz
        }
        if (ent->effects & EF_BRIGHTLIGHT) {
            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->origin[2] += 16;
            dl->radius = 400 + (rand() & 31);
            dl->die = cl.time + 0.001;
        }
        if (ent->effects & EF_DIMLIGHT) {
            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->radius = 200 + (rand() & 31);
            dl->die = cl.time + 0.001;
        }
        if (ent->effects & EF_QEX_QUADLIGHT) {
            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->radius = 200 + (rand() & 31);
            dl->die = cl.time + 0.001;
            dl->color[0] = 0.25f;
            dl->color[1] = 0.25f;
            dl->color[2] = 1.0f;
        }
        if (ent->effects & EF_QEX_PENTALIGHT) {
            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->radius = 200 + (rand() & 31);
            dl->die = cl.time + 0.001;
            dl->color[0] = 1.0f;
            dl->color[1] = 0.25f;
            dl->color[2] = 0.25f;
        }

        if (ent->model->flags & EF_GIB)
            R_RocketTrail(oldorg, ent->origin, 2);
        else if (ent->model->flags & EF_ZOMGIB)
            R_RocketTrail(oldorg, ent->origin, 4);
        else if (ent->model->flags & EF_TRACER)
            R_RocketTrail(oldorg, ent->origin, 3);
        else if (ent->model->flags & EF_TRACER2)
            R_RocketTrail(oldorg, ent->origin, 5);
        else if (ent->model->flags & EF_ROCKET) {
            R_RocketTrail(oldorg, ent->origin, 0);
            dl = CL_AllocDlight(i);
            VectorCopy(ent->origin, dl->origin);
            dl->radius = 200;
            dl->die = cl.time + 0.01;
        } else if (ent->model->flags & EF_GRENADE)
            R_RocketTrail(oldorg, ent->origin, 1);
        else if (ent->model->flags & EF_TRACER3)
            R_RocketTrail(oldorg, ent->origin, 6);

        ent->forcelink = false;

        if (i == cl.viewentity && !chase_active.value)
            continue;

        if (cl_numvisedicts < MAX_VISEDICTS) {
            cl_visedicts[cl_numvisedicts] = ent;
            cl_numvisedicts++;
        }
    }
}


/*
===============
CL_ReadFromServer

Read all incoming data from the server
===============
*/
int CL_ReadFromServer(void) {
    int ret;
    extern int num_temp_entities; //johnfitz
    int num_beams = 0; //johnfitz
    int num_dlights = 0; //johnfitz
    beam_t *b; //johnfitz
    dlight_t *l; //johnfitz
    int i; //johnfitz


    cl.oldtime = cl.time;
    cl.time += host_frametime;

    do {
        ret = CL_GetMessage();
        if (ret == -1)
            Host_Error("CL_ReadFromServer: lost server connection");
        if (!ret)
            break;

        cl.last_received_message = realtime;
        CL_ParseServerMessage();
    } while (ret && cls.state == ca_connected);

    if (cl_shownet.value)
        console::info("\n");

    CL_RelinkEntities();
    CL_UpdateTEnts();

    //johnfitz -- devstats

    //visedicts
    if (cl_numvisedicts > 256 && dev_peakstats.visedicts <= 256)
        console::dev_warn("%i visedicts exceeds standard limit of 256 (max = %d).\n", cl_numvisedicts, MAX_VISEDICTS);
    dev_stats.visedicts = cl_numvisedicts;
    dev_peakstats.visedicts = std::max(cl_numvisedicts, dev_peakstats.visedicts);

    //temp entities
    if (num_temp_entities > 64 && dev_peakstats.tempents <= 64)
        console::dev_warn("%i tempentities exceeds standard limit of 64 (max = %d).\n", num_temp_entities,
                     MAX_TEMP_ENTITIES);
    dev_stats.tempents = num_temp_entities;
    dev_peakstats.tempents = std::max(num_temp_entities, dev_peakstats.tempents);

    //beams
    for (i = 0, b = cl_beams; i < MAX_BEAMS; i++, b++)
        if (b->model && b->endtime >= cl.time)
            num_beams++;
    if (num_beams > 24 && dev_peakstats.beams <= 24)
        console::dev_warn("%i beams exceeded standard limit of 24 (max = %d).\n", num_beams, MAX_BEAMS);
    dev_stats.beams = num_beams;
    dev_peakstats.beams = std::max(num_beams, dev_peakstats.beams);

    //dlights
    for (i = 0, l = cl_dlights; i < MAX_DLIGHTS; i++, l++)
        if (l->die >= cl.time && l->radius)
            num_dlights++;
    if (num_dlights > 32 && dev_peakstats.dlights <= 32)
        console::dev_warn("%i dlights exceeded standard limit of 32 (max = %d).\n", num_dlights, MAX_DLIGHTS);
    dev_stats.dlights = num_dlights;
    dev_peakstats.dlights = std::max(num_dlights, dev_peakstats.dlights);

    //johnfitz

    //
    // bring the links up to date
    //
    return 0;
}

/*
=================
CL_SendCmd
=================
*/
void CL_SendCmd(void) {
    usercmd_t cmd;

    if (cls.state != ca_connected)
        return;

    if (cls.signon == SIGNONS) {
        // get basic movement from keyboard
        CL_BaseMove(&cmd);

        // allow mice or other external controllers to add to the move
        IN_Move(&cmd);

        // send the unreliable message
        CL_SendMove(&cmd);
    }

    if (cls.demoplayback) {
        cls.message.clear();
        return;
    }

    // send the reliable message
    if (cls.message.empty())
        return; // no message at all

    if (!NET_CanSendMessage(cls.netcon)) {
        console::debug("CL_SendCmd: can't send\n");
        return;
    }

    if (NET_SendMessage(cls.netcon, cls.message) == -1)
        Host_Error("CL_SendCmd: lost server connection");

    cls.message.clear();
}

/*
=============
CL_Tracepos_f -- johnfitz

display impact point of trace along VPN
=============
*/
void CL_Tracepos_f(void) {
    vec3_t v, w;

    if (cls.state != ca_connected)
        return;

    VectorMA(r_refdef.vieworg, 8192.0, vpn, v);
    TraceLine(r_refdef.vieworg, v, w);

    if (VectorLength(w) == 0)
        console::info("Tracepos: trace didn't hit anything\n");
    else
        console::info("Tracepos: (%i %i %i)\n", (int) w[0], (int) w[1], (int) w[2]);
}

/*
=============
CL_Viewpos_f -- johnfitz

display client's position and angles
=============
*/
void CL_Viewpos_f(void) {
    if (cls.state != ca_connected)
        return;
#if 0
    //camera position
    console::info("Viewpos: (%i %i %i) %i %i %i\n",
               (int) r_refdef.vieworg[0],
               (int) r_refdef.vieworg[1],
               (int) r_refdef.vieworg[2],
               (int) r_refdef.viewangles[PITCH],
               (int) r_refdef.viewangles[YAW],
               (int) r_refdef.viewangles[ROLL]);
#else
    //player position
    console::info("Viewpos: (%i %i %i) %i %i %i\n",
               (int) cl_entities[cl.viewentity].origin[0],
               (int) cl_entities[cl.viewentity].origin[1],
               (int) cl_entities[cl.viewentity].origin[2],
               (int) cl.viewangles[PITCH],
               (int) cl.viewangles[YAW],
               (int) cl.viewangles[ROLL]);
#endif
}

/*
=================
CL_Init
=================
*/
void CL_Init(void) {
    cls.message = net::msg{};

    CL_InitInput();
    CL_InitTEnts();

    cl_name.inscribe();
    cl_color.inscribe();
    cl_upspeed.inscribe();
    cl_forwardspeed.inscribe();
    cl_backspeed.inscribe();
    cl_sidespeed.inscribe();
    cl_movespeedkey.inscribe();
    cl_yawspeed.inscribe();
    cl_pitchspeed.inscribe();
    cl_anglespeedkey.inscribe();
    cl_shownet.inscribe();
    cl_nolerp.inscribe();
    lookspring.inscribe();
    lookstrafe.inscribe();
    sensitivity.inscribe();

    cl_alwaysrun.inscribe();

    m_pitch.inscribe();
    m_yaw.inscribe();
    m_forward.inscribe();
    m_side.inscribe();

    cfg_unbindall.inscribe();

    cl_maxpitch.inscribe(); //johnfitz -- variable pitch clamping
    cl_minpitch.inscribe(); //johnfitz -- variable pitch clamping

    cl_startdemos.inscribe();

    command::add("entities", CL_PrintEntities_f);
    command::add("disconnect", CL_Disconnect_f);
    command::add("record", CL_Record_f);
    command::add("stop", CL_Stop_f);
    command::add("playdemo", CL_PlayDemo_f);
    command::add("timedemo", CL_TimeDemo_f);

    command::add("tracepos", CL_Tracepos_f); //johnfitz
    command::add("viewpos", CL_Viewpos_f); //johnfitz
}
