/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2010-2014 QuakeSpasm developers
Copyright (C) 2020-2021 Vittorio Romeo

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.

*/

#include "quakedef.hpp"
#include "console.hpp"
#include "net.hpp"
#include "keys.hpp"
#include "protocol.hpp"
#include "byteorder.hpp"
#include "msg.hpp"
#include "client.hpp"
#include "sys.hpp"
#include "cmd.hpp"
#include "gl_texmgr.hpp"

static void CL_FinishTimeDemo();

/*
==============================================================================

DEMO CODE

When a demo is playing back, all NET_SendMessages are skipped, and
NET_GetMessages are read from the demo file.

Whenever cl.time gets past the last received message, another message is
read from the demo file.
==============================================================================
*/

// from ProQuake: space to fill out the demo header for record at any time
static byte demo_head[3][MAX_MSGLEN];
static int demo_head_size[2];

/*
==============
CL_StopPlayback

Called when a demo file runs out, or the user starts a game
==============
*/
void CL_StopPlayback()
{
    if(!cls.demoplayback)
    {
        return;
    }

    fclose(cls.demofile);
    cls.demoplayback = false;
    cls.demopaused = false;
    cls.demofile = nullptr;
    cls.state = ca_disconnected;

    if(cls.timedemo)
    {
        CL_FinishTimeDemo();
    }
}

/*
====================
CL_WriteDemoMessage

Dumps the current net message, prefixed by the length and view angles
====================
*/
static void CL_WriteDemoMessage()
{
    int len = LittleLong(net_message.cursize);
    fwrite(&len, 4, 1, cls.demofile);

    for(int i = 0; i < 3; i++)
    {
        float f = LittleFloat(cl.viewangles[i]);
        fwrite(&f, 4, 1, cls.demofile);
    }

    fwrite(net_message.data, net_message.cursize, 1, cls.demofile);
    fflush(cls.demofile);
}

static int CL_GetDemoMessage()
{
    int r;

    if(cls.demopaused)
    {
        return 0;
    }

    // decide if it is time to grab the next message
    if(cls.signon == SIGNONS) // always grab until fully connected
    {
        if(cls.timedemo)
        {
            if(host_framecount == cls.td_lastframe)
            {
                return 0; // already read this frame's message
            }
            cls.td_lastframe = host_framecount;
            // if this is the second frame, grab the real td_starttime
            // so the bogus time on the first frame doesn't count
            if(host_framecount == cls.td_startframe + 1)
            {
                cls.td_starttime = realtime;
            }
        }
        else if(/* cl.time > 0 && */ cl.time <= cl.mtime[0])
        {
            return 0; // don't need another message yet
        }
    }

    // get the next message
    fread(&net_message.cursize, 4, 1, cls.demofile);
    cl.mviewangles[1] = cl.mviewangles[0];
    for(int i = 0; i < 3; i++)
    {
        float f;
        r = fread(&f, 4, 1, cls.demofile);
        cl.mviewangles[0][i] = LittleFloat(f);
    }

    net_message.cursize = LittleLong(net_message.cursize);
    if(net_message.cursize > MAX_MSGLEN)
    {
        Sys_Error("Demo message > MAX_MSGLEN");
    }
    r = fread(net_message.data, net_message.cursize, 1, cls.demofile);
    if(r != 1)
    {
        CL_StopPlayback();
        return 0;
    }

    return 1;
}

/*
====================
CL_GetMessage

Handles recording and playback of demos, on top of NET_ code
====================
*/
int CL_GetMessage()
{
    int r;

    if(cls.demoplayback)
    {
        return CL_GetDemoMessage();
    }

    while(true)
    {
        r = NET_GetMessage(cls.netcon);

        if(r != 1 && r != 2)
        {
            return r;
        }

        // discard nop keepalive message
        if(net_message.cursize == 1 && net_message.data[0] == svc_nop)
        {
            Con_Printf("<-- server to client keepalive\n");
        }
        else
        {
            break;
        }
    }

    if(cls.demorecording)
    {
        CL_WriteDemoMessage();
    }

    if(cls.signon < 2)
    {
        // record messages before full connection, so that a
        // demo record can happen after connection is done
        memcpy(demo_head[cls.signon], net_message.data, net_message.cursize);
        demo_head_size[cls.signon] = net_message.cursize;
    }

    return r;
}


/*
====================
CL_Stop_f

stop recording a demo
====================
*/
void CL_Stop_f()
{
    if(cmd_source != src_command)
    {
        return;
    }

    if(!cls.demorecording)
    {
        Con_Printf("Not recording a demo.\n");
        return;
    }

    // write a disconnect message to the demo file
    SZ_Clear(&net_message);
    MSG_WriteByte(&net_message, svc_disconnect);
    CL_WriteDemoMessage();

    // finish up
    fclose(cls.demofile);
    cls.demofile = nullptr;
    cls.demorecording = false;
    Con_Printf("Completed demo\n");

    // ericw -- update demo tab-completion list
    DemoList_Rebuild();
}

/*
====================
CL_Record_f

record <demoname> <map> [cd track]
====================
*/
void CL_Record_f()
{
    int c;
    char name[MAX_OSPATH];
    int track;

    if(cmd_source != src_command)
    {
        return;
    }

    if(cls.demoplayback)
    {
        Con_Printf("Can't record during demo playback\n");
        return;
    }

    if(cls.demorecording)
    {
        CL_Stop_f();
    }

    c = Cmd_Argc();
    if(c != 2 && c != 3 && c != 4)
    {
        Con_Printf("record <demoname> [<map> [cd track]]\n");
        return;
    }

    if(strstr(Cmd_Argv(1), ".."))
    {
        Con_Printf("Relative pathnames are not allowed.\n");
        return;
    }

    if(c == 2 && cls.state == ca_connected)
    {
#if 0
		Con_Printf("Can not record - already connected to server\nClient demo recording must be started before connecting\n");
		return;
#endif
        if(cls.signon < 2)
        {
            Con_Printf("Can't record - try again when connected\n");
            return;
        }
    }

    // write the forced cd track number, or -1
    if(c == 4)
    {
        track = atoi(Cmd_Argv(3));
        Con_Printf("Forcing CD track to %i\n", cls.forcetrack);
    }
    else
    {
        track = -1;
    }

    q_snprintf(name, sizeof(name), "%s/%s", com_gamedir, Cmd_Argv(1));

    // start the map up
    if(c > 2)
    {
        Cmd_ExecuteString(va("map %s", Cmd_Argv(2)), src_command);
        if(cls.state != ca_connected)
        {
            return;
        }
    }

    // open the demo file
    COM_AddExtension(name, ".dem", sizeof(name));

    Con_Printf("recording to %s.\n", name);
    cls.demofile = fopen(name, "wb");
    if(!cls.demofile)
    {
        Con_Printf("ERROR: couldn't create %s\n", name);
        return;
    }

    cls.forcetrack = track;
    fprintf(cls.demofile, "%i\n", cls.forcetrack);

    cls.demorecording = true;

    // from ProQuake: initialize the demo file if we're already connected
    if(c == 2 && cls.state == ca_connected)
    {
        byte* data = net_message.data;
        int cursize = net_message.cursize;
        int i;

        for(i = 0; i < 2; i++)
        {
            net_message.data = demo_head[i];
            net_message.cursize = demo_head_size[i];
            CL_WriteDemoMessage();
        }

        net_message.data = demo_head[2];
        SZ_Clear(&net_message);

        // current names, colors, and frag counts
        for(i = 0; i < cl.maxclients; i++)
        {
            MSG_WriteByte(&net_message, svc_updatename);
            MSG_WriteByte(&net_message, i);
            MSG_WriteString(&net_message, cl.scores[i].name);
            MSG_WriteByte(&net_message, svc_updatefrags);
            MSG_WriteByte(&net_message, i);
            MSG_WriteShort(&net_message, cl.scores[i].frags);
            MSG_WriteByte(&net_message, svc_updatecolors);
            MSG_WriteByte(&net_message, i);
            MSG_WriteByte(&net_message, cl.scores[i].colors);
        }

        // send all current light styles
        for(i = 0; i < MAX_LIGHTSTYLES; i++)
        {
            MSG_WriteByte(&net_message, svc_lightstyle);
            MSG_WriteByte(&net_message, i);
            MSG_WriteString(&net_message, cl_lightstyle[i].map);
        }

        // what about the CD track or SVC fog... future consideration.
        MSG_WriteByte(&net_message, svc_updatestat);
        MSG_WriteByte(&net_message, STAT_TOTALSECRETS);
        MSG_WriteLong(&net_message, cl.stats[STAT_TOTALSECRETS]);

        MSG_WriteByte(&net_message, svc_updatestat);
        MSG_WriteByte(&net_message, STAT_TOTALMONSTERS);
        MSG_WriteLong(&net_message, cl.stats[STAT_TOTALMONSTERS]);

        MSG_WriteByte(&net_message, svc_updatestat);
        MSG_WriteByte(&net_message, STAT_SECRETS);
        MSG_WriteLong(&net_message, cl.stats[STAT_SECRETS]);

        MSG_WriteByte(&net_message, svc_updatestat);
        MSG_WriteByte(&net_message, STAT_MONSTERS);
        MSG_WriteLong(&net_message, cl.stats[STAT_MONSTERS]);

        // view entity
        MSG_WriteByte(&net_message, svc_setview);
        MSG_WriteShort(&net_message, cl.viewentity);

        // signon
        MSG_WriteByte(&net_message, svc_signonnum);
        MSG_WriteByte(&net_message, 3);

        CL_WriteDemoMessage();

        // restore net_message
        net_message.data = data;
        net_message.cursize = cursize;
    }
}


/*
====================
CL_PlayDemo_f

play [demoname]
====================
*/
void CL_PlayDemo_f()
{
    char name[MAX_OSPATH];
    int i;

    int c;
    bool neg;

    if(cmd_source != src_command)
    {
        return;
    }

    if(Cmd_Argc() != 2)
    {
        Con_Printf("playdemo <demoname> : plays a demo\n");
        return;
    }

    // disconnect from server
    CL_Disconnect();

    // open the demo file
    q_strlcpy(name, Cmd_Argv(1), sizeof(name));
    COM_AddExtension(name, ".dem", sizeof(name));

    Con_Printf("Playing demo from %s.\n", name);

    COM_FOpenFile(name, &cls.demofile, nullptr);
    if(!cls.demofile)
    {
        Con_Printf("ERROR: couldn't open %s\n", name);
        cls.demonum = -1; // stop demo loop
        return;
    }

    // ZOID, fscanf is evil
    // O.S.: if a space character e.g. 0x20 (' ') follows '\n',
    // fscanf skips that byte too and screws up further reads.
    //	fscanf (cls.demofile, "%i\n", &cls.forcetrack);
    cls.forcetrack = 0;
    c = 0; /* silence pesky compiler warnings */
    neg = false;
    // read a decimal integer possibly with a leading '-',
    // followed by a '\n':
    for(i = 0; i < 13; i++)
    {
        c = getc(cls.demofile);
        if(c == '\n')
        {
            break;
        }
        if(c == '-')
        {
            neg = true;
            continue;
        }
        // check for multiple '-' or legal digits? meh...
        cls.forcetrack = cls.forcetrack * 10 + (c - '0');
    }
    if(c != '\n')
    {
        fclose(cls.demofile);
        cls.demofile = nullptr;
        cls.demonum = -1; // stop demo loop
        Con_Printf("ERROR: demo \"%s\" is invalid\n", name);
        return;
    }
    if(neg)
    {
        cls.forcetrack = -cls.forcetrack;
    }

    cls.demoplayback = true;
    cls.demopaused = false;
    cls.state = ca_connected;

    // get rid of the menu and/or console
    key_dest = key_game;
}

/*
====================
CL_FinishTimeDemo

====================
*/
static void CL_FinishTimeDemo()
{
    int frames;
    float time;

    cls.timedemo = false;

    // the first frame didn't count
    frames = (host_framecount - cls.td_startframe) - 1;
    time = realtime - cls.td_starttime;
    if(!time)
    {
        time = 1;
    }
    Con_Printf(
        "%i frames %5.1f seconds %5.1f fps\n", frames, time, frames / time);
}

/*
====================
CL_TimeDemo_f

timedemo [demoname]
====================
*/
void CL_TimeDemo_f()
{
    if(cmd_source != src_command)
    {
        return;
    }

    if(Cmd_Argc() != 2)
    {
        Con_Printf("timedemo <demoname> : gets demo speeds\n");
        return;
    }

    CL_PlayDemo_f();
    if(!cls.demofile)
    {
        return;
    }

    // cls.td_starttime will be grabbed at the second frame of the demo, so
    // all the loading time doesn't get counted

    cls.timedemo = true;
    cls.td_startframe = host_framecount;
    cls.td_lastframe = -1; // get a new message this frame
}
