/*
Copyright (C) 1996-2001 Id Software, Inc.
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.

*/

#include <cstring>
#include <format>

#include "quakedef.hpp"

static void CL_FinishTimeDemo(void);

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

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;
// static int *demo_head_sizes;

std::vector<std::vector<byte>> demo_head{};

/*
==============
CL_ClearSignons
==============
*/
void CL_ClearSignons(void) {
    demo_head.clear();
    cls.signon = 0;
}

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

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

    fclose(cls.demofile);
    cls.demoplayback = false;
    cls.demopaused = false;
    cls.demofile = NULL;
    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(void) {
    int len;
    int i;
    float f;

    len = LittleLong(net_message.size());
    fwrite(&len, 4, 1, cls.demofile);
    for (i = 0; i < 3; i++) {
        f = LittleFloat(cl.viewangles[i]);
        fwrite(&f, 4, 1, cls.demofile);
    }
    fwrite(net_message.data(), net_message.size(), 1, cls.demofile);
    fflush(cls.demofile);
}

static int CL_GetDemoMessage(void) {
    int r, i;
    float f;

    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
    int msgsize;
    if (!fread(&msgsize, 4, 1, cls.demofile))
        Sys_Error("Demo read error");
    msgsize = LittleLong(msgsize);
    VectorCopy(cl.mviewangles[0], cl.mviewangles[1]);
    for (i = 0; i < 3; i++) {
        r = fread(&f, 4, 1, cls.demofile);
        cl.mviewangles[0][i] = LittleFloat(f);
    }

    if (msgsize > MAX_MSGLEN)
        Sys_Error("Demo message > MAX_MSGLEN");
    auto buf = new std::uint8_t[msgsize];
    r = fread(&buf[0], msgsize, 1, cls.demofile);
    net_message.clear();
    net_message.write(&buf[0], msgsize);
    delete []buf;
    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(void) {
    int r;

    if (cls.demoplayback)
        return CL_GetDemoMessage();

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

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

        // discard nop keepalive message
        if (net_message.size() == 1 && net_message.data()[0] == svc_nop)
            console::info("<-- 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
        std::vector<byte> new_msg{};
        new_msg.resize(net_message.size());
        std::memcpy(new_msg.data(), net_message.data(), net_message.size());
        demo_head.push_back(new_msg);
    }

    return r;
}


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

stop recording a demo
====================
*/
void CL_Stop_f(void) {
    if (command::last_source != command::source::command)
        return;

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

    // write a disconnect message to the demo file
    net_message.clear();
    net_message.write_byte(svc_disconnect);
    CL_WriteDemoMessage();

    // finish up
    fclose(cls.demofile);
    cls.demofile = NULL;
    cls.demorecording = false;
    console::info("Completed demo\n");

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

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

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

    if (command::last_source != command::source::command)
        return;

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

    if (cls.demorecording)
        CL_Stop_f();

    c = command::argc();
    if (c != 2 && c != 3 && c != 4) {
        console::info("record <demoname> [<map> [cd track]]\n");
        return;
    }

    if (strstr(command::argv(1).value_or("").c_str(), "..")) {
        console::info("Relative pathnames are not allowed.\n");
        return;
    }

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

    // write the forced cd track number, or -1
    if (c == 4) {
        track = atoi(command::argv(3)->c_str());
        console::info("Forcing CD track to %i\n", cls.forcetrack);
    } else {
        track = -1;
    }

    q_snprintf(name, sizeof(name), "%s/%s", com_gamedir, command::argv(1).value_or("").c_str());

    // start the map up
    if (c > 2) {
        command::execute_string(std::format("map {:s}", *command::argv(2)), command::source::command);
        if (cls.state != ca_connected)
            return;
    }

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

    console::info("recording to %s.\n", name);
    cls.demofile = fopen(name, "wb");
    if (!cls.demofile) {
        console::info("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) {
        static byte tmpbuf[NET_MAXMESSAGE];
        net::msg old = net_message;

        for (auto msg : demo_head) {
            net_message = net::msg{};
            net_message.write(msg.data(), msg.size());
            CL_WriteDemoMessage();
        }

        net_message.clear();

        // current names, colors, and frag counts
        for (auto i = 0; i < cl.maxclients; i++) {
            net_message.write_byte(svc_updatename);
            net_message.write_byte(i);
            net_message.write_string(cl.scores[i].name);
            net_message.write_byte(svc_updatefrags);
            net_message.write_byte(i);
            net_message.write_short(cl.scores[i].frags);
            net_message.write_byte(svc_updatecolors);
            net_message.write_byte(i);
            net_message.write_byte(cl.scores[i].colors);
        }

        // send all current light styles
        for (auto i = 0; i < MAX_LIGHTSTYLES; i++) {
            net_message.write_byte(svc_lightstyle);
            net_message.write_byte(i);
            net_message.write_string(cl_lightstyle[i].map);
        }

        // what about the CD track or SVC fog... future consideration.
        net_message.write_byte(svc_updatestat);
        net_message.write_byte(STAT_TOTALSECRETS);
        net_message.write_long(cl.stats[STAT_TOTALSECRETS]);

        net_message.write_byte(svc_updatestat);
        net_message.write_byte(STAT_TOTALMONSTERS);
        net_message.write_long(cl.stats[STAT_TOTALMONSTERS]);

        net_message.write_byte(svc_updatestat);
        net_message.write_byte(STAT_SECRETS);
        net_message.write_long(cl.stats[STAT_SECRETS]);

        net_message.write_byte(svc_updatestat);
        net_message.write_byte(STAT_MONSTERS);
        net_message.write_long(cl.stats[STAT_MONSTERS]);

        // view entity
        net_message.write_byte(svc_setview);
        net_message.write_short(cl.viewentity);

        // signon
        net_message.write_byte(svc_signonnum);
        net_message.write_byte(3);

        CL_WriteDemoMessage();

        // restore net_message
        net_message = old;
    }
}


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

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

    if (command::last_source != command::source::command)
        return;

    if (command::argc() != 2) {
        console::info("playdemo <demoname> : plays a demo\n");
        return;
    }

    // disconnect from server
    CL_Disconnect();

    // open the demo file
    std::strncpy(name, command::argv(1).value_or("").c_str(), sizeof(name));
    COM_AddExtension(name, ".dem", sizeof(name));

    console::info("Playing demo from %s.\n", name);

    COM_FOpenFile(name, &cls.demofile, NULL);
    if (!cls.demofile) {
        console::info("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);
    if (fscanf(cls.demofile, "%i", &cls.forcetrack) != 1 || fgetc(cls.demofile) != '\n') {
        fclose(cls.demofile);
        cls.demofile = NULL;
        cls.demonum = -1; // stop demo loop
        console::info("ERROR: demo \"%s\" is invalid\n", name);
        return;
    }

    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(void) {
    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;
    console::info("%i frames %5.1f seconds %5.1f fps\n", frames, time, frames / time);
}

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

timedemo [demoname]
====================
*/
void CL_TimeDemo_f(void) {
    if (command::last_source != command::source::command)
        return;

    if (command::argc() != 2) {
        console::info("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
}
