/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2007-2008 Kristian Duske
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.

*/
// host.c -- coordinates spawning and killing of local servers

#include "quakedef.hpp"
#include "bgmusic.hpp"
#include <csetjmp>

#include "pr_wren.hpp"

/*

A server can allways be started, even if the system started out as a client
to a remote system.

A client can NOT be started if the system started as a dedicated server.

Memory is cleared / released when a server or client begins, not when they end.

*/

quakeparms_t *host_parms;

bool host_initialized; // true if into command execution

double host_frametime;
double realtime; // without any filtering or bounding
double oldrealtime; // last frame run

int host_framecount;

int host_hunklevel;

int minimum_memory;

client_t *host_client; // current client

jmp_buf host_abortserver;

byte *host_colormap;

convar host_framerate = {"host_framerate", "0"}; // set for slow motion
convar host_speeds = {"host_speeds", "0"}; // set for running times
convar host_maxfps = {"host_maxfps", "72", {.archive = true}}; //johnfitz
convar host_timescale = {"host_timescale", "0"}; //johnfitz
convar max_edicts = {"max_edicts", "8192"}; //johnfitz //ericw -- changed from 2048 to 8192, removed CVAR_ARCHIVE

convar sys_ticrate = {"sys_ticrate", "0.05"}; // dedicated server
convar serverprofile = {"serverprofile", "0"};

convar fraglimit = {"fraglimit", "0", {.notify = true, .server_info = true}};
convar timelimit = {"timelimit", "0", {.notify = true, .server_info = true}};
convar teamplay = {"teamplay", "0", {.notify = true, .server_info = true}};
convar samelevel = {"samelevel", "0"};
convar noexit = {"noexit", "0", {.notify = true, .server_info = true}};
convar skill = {"skill", "1"}; // 0 - 3
convar deathmatch = {"deathmatch", "0"}; // 0, 1, or 2
convar coop = {"coop", "0"}; // 0 or 1

convar pausable = {"pausable", "1"};

convar developer = {"developer", "0"};

convar temp1 = {"temp1", "0"};

convar devstats = {"devstats", "0"}; //johnfitz -- track developer statistics that vary every frame

convar campaign = {"campaign", "0"}; // for the 2021 rerelease
convar horde = {"horde", "0"}; // for the 2021 rerelease
convar sv_cheats = {"sv_cheats", "0"}; // for the 2021 rerelease

devstats_t dev_stats, dev_peakstats;
overflowtimes_t dev_overflows;
//this stores the last time overflow messages were displayed, not the last time overflows occured

/*
================
Max_Edicts_f -- johnfitz
================
*/
static void Max_Edicts_f(convar *var) {
    //TODO: clamp it here?
    if (cls.state == ca_connected || sv.active)
        console::info("Changes to max_edicts will not take effect until the next time a map is loaded.\n");
}

/*
================
Max_Fps_f -- ericw
================
*/
static void Max_Fps_f(convar *var) {
    if (var->value > 72)
        console::warn("host_maxfps above 72 breaks physics.\n");
}

/*
================
Host_EndGame
================
*/
void Host_EndGame(const char *message, ...) {
    va_list argptr;
    char string[1024];

    va_start(argptr, message);
    q_vsnprintf(string, sizeof(string), message, argptr);
    va_end(argptr);
    console::debug("Host_EndGame: %s\n", string);

    if (sv.active)
        Host_ShutdownServer(false);

    if (cls.state == ca_dedicated)
        Sys_Error("Host_EndGame: %s\n", string); // dedicated servers exit

    if (cls.demonum != -1 && !cls.timedemo)
        CL_NextDemo();
    else
        CL_Disconnect();

    longjmp(host_abortserver, 1);
}

/*
================
Host_Error

This shuts down both the client and server
================
*/
void Host_Error(const char *error, ...) {
    va_list argptr;
    char string[1024];
    static bool inerror = false;

    if (inerror)
        Sys_Error("Host_Error: recursively entered");
    inerror = true;

    SCR_EndLoadingPlaque(); // reenable screen updates

    va_start(argptr, error);
    q_vsnprintf(string, sizeof(string), error, argptr);
    va_end(argptr);
    console::info("Host_Error: %s\n", string);

    if (sv.active)
        Host_ShutdownServer(false);

    if (cls.state == ca_dedicated)
        Sys_Error("Host_Error: %s\n", string); // dedicated servers exit

    CL_Disconnect();
    cls.demonum = -1;
    cl.intermission = 0; //johnfitz -- for errors during intermissions (changelevel with no map found, etc.)

    inerror = false;

    longjmp(host_abortserver, 1);
}

/*
================
Host_FindMaxClients
================
*/
void Host_FindMaxClients(void) {
    svs.maxclients = 1;

    auto i = common::check_param("-dedicated");
    if (i.has_value()) {
        cls.state = ca_dedicated;
        if (i != (common::_argv.size() - 1)) {
            svs.maxclients = std::atoi(common::_argv[i.value() + 1].c_str());
        } else
            svs.maxclients = 8;
    } else
        cls.state = ca_disconnected;

    i = common::check_param("-listen");
    if (i.has_value()) {
        if (cls.state == ca_dedicated)
            Sys_Error("Only one of -dedicated or -listen can be specified");
        if (i != (common::_argv.size() - 1))
            svs.maxclients = std::atoi(common::_argv[i.value() + 1].c_str());
        else
            svs.maxclients = 8;
    }
    if (svs.maxclients < 1)
        svs.maxclients = 8;
    else if (svs.maxclients > MAX_SCOREBOARD)
        svs.maxclients = MAX_SCOREBOARD;

    svs.maxclientslimit = svs.maxclients;
    if (svs.maxclientslimit < 4)
        svs.maxclientslimit = 4;
    svs.clients = (struct client_s *) Hunk_AllocName(svs.maxclientslimit * sizeof(client_t), "clients");

    if (svs.maxclients > 1)
        deathmatch.set("1");
    else
        deathmatch.set("0");
}

void Host_Version_f(void) {
    console::info("Quake Version %1.2f\n", VERSION);
    console::info("QuakeSpasm Version " QUAKESPASM_VER_STRING "\n");
    console::info("Exe: " __TIME__ " " __DATE__ "\n");
}

/* cvar callback functions : */
void Host_Callback_Notify(convar *var) {
    if (sv.active)
        SV_BroadcastPrintf("\"%s\" changed to \"%s\"\n", var->name.c_str(), var->string);
}

/*
=======================
Host_InitLocal
======================
*/
void Host_InitLocal(void) {
    command::add("version", Host_Version_f);

    Host_InitCommands();

    host_framerate.inscribe();
    host_speeds.inscribe();
    host_maxfps.inscribe(); //johnfitz
    host_maxfps.set_callback(Max_Fps_f);
    host_timescale.inscribe(); //johnfitz

    max_edicts.inscribe(); //johnfitz
    max_edicts.set_callback(Max_Edicts_f);
    devstats.inscribe(); //johnfitz

    sys_ticrate.inscribe();
    sys_throttle.inscribe();
    serverprofile.inscribe();

    fraglimit.inscribe();
    timelimit.inscribe();
    teamplay.inscribe();
    fraglimit.set_callback(Host_Callback_Notify);
    timelimit.set_callback(Host_Callback_Notify);
    teamplay.set_callback(Host_Callback_Notify);
    samelevel.inscribe();
    noexit.inscribe();
    noexit.set_callback(Host_Callback_Notify);
    skill.inscribe();
    developer.inscribe();
    coop.inscribe();
    deathmatch.inscribe();

    campaign.inscribe();
    horde.inscribe();
    sv_cheats.inscribe();

    pausable.inscribe();

    temp1.inscribe();

    Host_FindMaxClients();
}


/*
===============
Host_WriteConfiguration

Writes key bindings and archived cvars to config.cfg
===============
*/
void Host_WriteConfiguration(void) {
    FILE *f;

    // dedicated servers initialize the host but don't parse and set the
    // config.cfg cvars
    if (host_initialized && !isDedicated && !host_parms->errstate) {
        f = fopen(va("%s/config.cfg", com_gamedir), "w");
        if (!f) {
            console::info("Couldn't write config.cfg.\n");
            return;
        }

        //VID_SyncCvars (); //johnfitz -- write actual current mode to config file, in case cvars were messed with

        Key_WriteBindings(f);
        Cvar_WriteVariables(f);

        //johnfitz -- extra commands to preserve state
        fprintf(f, "vid_restart\n");
        if (in_mlook.state & 1) fprintf(f, "+mlook\n");
        //johnfitz

        fclose(f);
    }
}


/*
=================
SV_ClientPrintf

Sends text across to be displayed
FIXME: make this just a stuffed echo?
=================
*/
void SV_ClientPrintf(const char *fmt, ...) {
    va_list argptr;
    char string[1024];

    va_start(argptr, fmt);
    q_vsnprintf(string, sizeof(string), fmt, argptr);
    va_end(argptr);

    host_client->message.write_byte(svc_print);
    host_client->message.write_string(string);
}

/*
=================
SV_BroadcastPrintf

Sends text to all active clients
=================
*/
void SV_BroadcastPrintf(const char *fmt, ...) {
    va_list argptr;
    char string[1024];
    int i;

    va_start(argptr, fmt);
    q_vsnprintf(string, sizeof(string), fmt, argptr);
    va_end(argptr);

    for (i = 0; i < svs.maxclients; i++) {
        if (svs.clients[i].active && svs.clients[i].spawned) {
            svs.clients[i].message.write_byte(svc_print);
            svs.clients[i].message.write_string(string);
        }
    }
}

/*
=================
Host_ClientCommands

Send text over to the client to be executed
=================
*/
void Host_ClientCommands(const char *fmt, ...) {
    va_list argptr;
    char string[1024];

    va_start(argptr, fmt);
    q_vsnprintf(string, sizeof(string), fmt, argptr);
    va_end(argptr);

    host_client->message.write_byte(svc_stufftext);
    host_client->message.write_string(string);
}

/*
=====================
SV_DropClient

Called when the player is getting totally kicked off the host
if (crash = true), don't bother sending signofs
=====================
*/
void SV_DropClient(bool crash) {
    int saveSelf;
    int i;
    client_t *client;

    if (!crash) {
        // send any final messages (don't check for errors)
        if (NET_CanSendMessage(host_client->netconnection)) {
            host_client->message.write_byte(svc_disconnect);
            NET_SendMessage(host_client->netconnection, host_client->message);
        }

        if (host_client->edict && host_client->spawned) {
            // call the prog function for removing a client
            // this will set the body to a dead frame, among other things
            saveSelf = pr_global_struct->self;
            pr_global_struct->self = EDICT_TO_PROG(host_client->edict);
            PR_ExecuteProgram(pr_global_struct->ClientDisconnect);
            pr_global_struct->self = saveSelf;
        }

        Sys_Printf("Client %s removed\n", host_client->name);
    }

    // break the net connection
    NET_Close(host_client->netconnection);
    host_client->netconnection = NULL;

    // free the client (the body stays around)
    host_client->active = false;
    host_client->name[0] = 0;
    host_client->old_frags = -999999;
    net_activeconnections--;

    // send notification to all clients
    for (i = 0, client = svs.clients; i < svs.maxclients; i++, client++) {
        if (!client->active)
            continue;
        client->message.write_byte(svc_updatename);
        client->message.write_byte(host_client - svs.clients);
        client->message.write_string("");
        client->message.write_byte(svc_updatefrags);
        client->message.write_byte(host_client - svs.clients);
        client->message.write_short(0);
        client->message.write_byte(svc_updatecolors);
        client->message.write_byte(host_client - svs.clients);
        client->message.write_byte(0);
    }
}

/*
==================
Host_ShutdownServer

This only happens at the end of a game, not between levels
==================
*/
void Host_ShutdownServer(bool crash) {
    int i;
    int count;
    net::msg msg{};
    double start;

    if (!sv.active)
        return;

    sv.active = false;

    // stop all client sounds immediately
    if (cls.state == ca_connected)
        CL_Disconnect();

    // flush any pending messages - like the score!!!
    start = Sys_DoubleTime();
    do {
        count = 0;
        for (i = 0, host_client = svs.clients; i < svs.maxclients; i++, host_client++) {
            if (host_client->active && !host_client->message.empty()) {
                if (NET_CanSendMessage(host_client->netconnection)) {
                    NET_SendMessage(host_client->netconnection, host_client->message);
                    host_client->message.clear();
                } else {
                    NET_GetMessage(host_client->netconnection);
                    count++;
                }
            }
        }
        if ((Sys_DoubleTime() - start) > 3.0)
            break;
    } while (count);

    // make sure all the clients know we're disconnecting

    msg.write_byte(svc_disconnect);
    count = NET_SendToAll(msg, 5.0);
    if (count)
        console::info("Host_ShutdownServer: NET_SendToAll failed for %u clients\n", count);

    for (i = 0, host_client = svs.clients; i < svs.maxclients; i++, host_client++)
        if (host_client->active)
            SV_DropClient(crash);

    //
    // clear structures
    //
    //	memset (&sv, 0, sizeof(sv)); // ServerSpawn already do this by Host_ClearMemory
    memset(svs.clients, 0, svs.maxclientslimit * sizeof(client_t));
}


/*
================
Host_ClearMemory

This clears all the memory used by both the client and server, but does
not reinitialize anything.
================
*/
void Host_ClearMemory(void) {
    console::debug("Clearing memory\n");
    D_FlushCaches();
    Mod_ClearAll();
    Sky_ClearAll();
    /* host_hunklevel MUST be set at this point */
    Hunk_FreeToLowMark(host_hunklevel);
    cls.signon = 0; // not CL_ClearSignons()
    free(sv.edicts); // ericw -- sv.edicts switched to use malloc()
    memset(&sv, 0, sizeof(sv));
    memset(&cl, 0, sizeof(cl));
}


//==============================================================================
//
// Host Frame
//
//==============================================================================

/*
===================
Host_FilterTime

Returns false if the time is too short to run a frame
===================
*/
bool Host_FilterTime(float time) {
    float maxfps; //johnfitz

    realtime += time;

    //johnfitz -- max fps cvar
    maxfps = std::clamp(host_maxfps.value, 10.f, 1000.f);
    if (!cls.timedemo && realtime - oldrealtime < 1.0 / maxfps)
        return false; // framerate is too high
    //johnfitz

    host_frametime = realtime - oldrealtime;
    oldrealtime = realtime;

    //johnfitz -- host_timescale is more intuitive than host_framerate
    if (host_timescale.value > 0)
        host_frametime *= host_timescale.value;
        //johnfitz
    else if (host_framerate.value > 0)
        host_frametime = host_framerate.value;
    else // don't allow really long or short frames
        host_frametime = std::clamp(host_frametime, 0.001, 0.1); //johnfitz -- use CLAMP

    return true;
}

/*
===================
Host_GetConsoleCommands

Add them exactly as if they had been typed at the console
===================
*/
void Host_GetConsoleCommands(void) {
    const char *cmd;

    if (!isDedicated)
        return; // no stdin necessary in graphical mode

    while (1) {
        cmd = Sys_ConsoleInput();
        if (!cmd)
            break;
        command::buffer::add_text(cmd);
    }
}

/*
==================
Host_ServerFrame
==================
*/
void Host_ServerFrame(void) {
    int i, active; //johnfitz
    edict_t *ent; //johnfitz

    // run the world state
    pr_global_struct->frametime = host_frametime;

    // set the time and clear the general datagram
    SV_ClearDatagram();

    // check for new clients
    SV_CheckForNewClients();

    // read client messages
    SV_RunClients();

    // move things around and think
    // always pause in single player if in console or menus
    if (!sv.paused && (svs.maxclients > 1 || key_dest == key_game))
        SV_Physics();

    //johnfitz -- devstats
    if (cls.signon == SIGNONS) {
        for (i = 0, active = 0; i < sv.num_edicts; i++) {
            ent = EDICT_NUM(i);
            if (!ent->free)
                active++;
        }
        if (active > 600 && dev_peakstats.edicts <= 600)
            console::dev_warn("%i edicts exceeds standard limit of 600 (max = %d).\n", active, sv.max_edicts);
        dev_stats.edicts = active;
        dev_peakstats.edicts = std::max(active, dev_peakstats.edicts);
    }
    //johnfitz

    // send all messages to the clients
    SV_SendClientMessages();
}

/*
==================
Host_Frame

Runs all active servers
==================
*/
void _Host_Frame(float time) {
    static double time1 = 0;
    static double time2 = 0;
    static double time3 = 0;
    int pass1, pass2, pass3;

    if (setjmp(host_abortserver))
        return; // something bad happened, or the server disconnected

    // keep the random time dependent
    rand();

    // decide the simulation time
    if (!Host_FilterTime(time))
        return; // don't run too fast, or packets will flood out

    // get new key events
    Key_UpdateForDest();
    IN_UpdateInputMode();
    Sys_SendKeyEvents();

    // allow mice or other external controllers to add commands
    IN_Commands();

    // process console commands
    command::buffer::execute();

    NET_Poll();

    // if running the server locally, make intentions now
    if (sv.active)
        CL_SendCmd();

    //-------------------
    //
    // server operations
    //
    //-------------------

    // check for commands typed to the host
    Host_GetConsoleCommands();

    if (sv.active)
        Host_ServerFrame();

    //-------------------
    //
    // client operations
    //
    //-------------------

    // if running the server remotely, send intentions now after
    // the incoming messages have been read
    if (!sv.active)
        CL_SendCmd();

    // fetch results from server
    if (cls.state == ca_connected)
        CL_ReadFromServer();

    // update video
    if (host_speeds.value)
        time1 = Sys_DoubleTime();

    SCR_UpdateScreen();

    CL_RunParticles(); //johnfitz -- seperated from rendering

    if (host_speeds.value)
        time2 = Sys_DoubleTime();

    // update audio
    music::update(); // adds music raw samples and/or advances midi driver
    if (cls.signon == SIGNONS) {
        S_Update(r_origin, vpn, vright, vup);
        CL_DecayLights();
    } else
        S_Update(vec3_origin, vec3_origin, vec3_origin, vec3_origin);

    CDAudio_Update();

    if (host_speeds.value) {
        pass1 = (time1 - time3) * 1000;
        time3 = Sys_DoubleTime();
        pass2 = (time2 - time1) * 1000;
        pass3 = (time3 - time2) * 1000;
        console::info("%3i tot %3i server %3i gfx %3i snd\n",
                   pass1 + pass2 + pass3, pass1, pass2, pass3);
    }

    host_framecount++;
}

void Host_Frame(float time) {
    double time1, time2;
    static double timetotal;
    static int timecount;
    int i, c, m;

    if (!serverprofile.value) {
        _Host_Frame(time);
        return;
    }

    time1 = Sys_DoubleTime();
    _Host_Frame(time);
    time2 = Sys_DoubleTime();

    timetotal += time2 - time1;
    timecount++;

    if (timecount < 1000)
        return;

    m = timetotal * 1000 / timecount;
    timecount = 0;
    timetotal = 0;
    c = 0;
    for (i = 0; i < svs.maxclients; i++) {
        if (svs.clients[i].active)
            c++;
    }

    console::info("serverprofile: %2i clients %2i msec\n", c, m);
}

/*
====================
Host_Init
====================
*/
void Host_Init(void) {
    if (standard_quake)
        minimum_memory = MINIMUM_MEMORY;
    else minimum_memory = MINIMUM_MEMORY_LEVELPAK;

    if (common::check_param("-minmemory").has_value())
        host_parms->memsize = minimum_memory;

    if (host_parms->memsize < minimum_memory)
        Sys_Error("Only %4.1f megs of memory available, can't execute game", host_parms->memsize / (float) 0x100000);

    common::_argv.clear();
    for (auto i = 0;i < host_parms->argc;i++) {
        common::_argv.emplace_back(host_parms->argv[i]);
    }

    Memory_Init(host_parms->membase, host_parms->memsize);
    command::buffer::init();
    command::init();
    LOG_Init(host_parms);
    Cvar_Init(); //johnfitz
    common::init();
    COM_InitFilesystem();
    Host_InitLocal();
    W_LoadWadFile(); //johnfitz -- filename is now hard-coded for honesty
    if (cls.state != ca_dedicated) {
        Key_Init();
        console::init();
    }
    PR_Init();
    wren::init();
    Mod_Init();
    NET_Init();
    SV_Init();

    console::info("Exe: " __TIME__ " " __DATE__ "\n");
    console::info("%4.1f megabyte heap\n", host_parms->memsize / (1024 * 1024.0));

    if (cls.state != ca_dedicated) {
        host_colormap = (byte *) COM_LoadHunkFile("gfx/colormap.lmp", NULL);
        if (!host_colormap)
            Sys_Error("Couldn't load gfx/colormap.lmp");

        V_Init();
        Chase_Init();
        M_Init();
        ExtraMaps_Init(); //johnfitz
        Modlist_Init(); //johnfitz
        DemoList_Init(); //ericw
        VID_Init();
        IN_Init();
        TexMgr_Init(); //johnfitz
        Draw_Init();
        SCR_Init();
        R_Init();
        S_Init();
        CDAudio_Init();
        music::init();
        Sbar_Init();
        CL_Init();
    }

    LOC_Init(); // for 2021 rerelease support.

    Hunk_AllocName(0, "-HOST_HUNKLEVEL-");
    host_hunklevel = Hunk_LowMark();

    host_initialized = true;
    console::info("\n========= Quake Initialized =========\n\n");

    if (cls.state != ca_dedicated) {
        command::buffer::insert_text("exec quake.rc\n");
        // johnfitz -- in case the vid mode was locked during vid_init, we can unlock it now.
        // note: two leading newlines because the command buffer swallows one of them.
        command::buffer::add_text("\n\nvid_unlock\n");
    }

    if (cls.state == ca_dedicated) {
        command::buffer::add_text("exec autoexec.cfg\n");
        command::buffer::add_text("stuffcmds");
        command::buffer::execute();
        if (!sv.active)
            command::buffer::add_text("map start\n");
    }
}


/*
===============
Host_Shutdown

FIXME: this is a callback from Sys_Quit and Sys_Error.  It would be better
to run quit through here before the final handoff to the sys code.
===============
*/
void Host_Shutdown(void) {
    static bool isdown = false;

    if (isdown) {
        printf("recursive shutdown\n");
        return;
    }
    isdown = true;

    // keep console::info from trying to update the screen
    scr_disabled_for_loading = true;

    Host_WriteConfiguration();

    NET_Shutdown();

    if (cls.state != ca_dedicated) {
        if (console::initialized)
            History_Shutdown();
        music::shutdown();
        CDAudio_Shutdown();
        S_Shutdown();
        IN_Shutdown();
        VID_Shutdown();
    }

    LOG_Close();

    LOC_Shutdown();
}
