/*
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
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.

*/
// cmd.c -- Quake script command processing module

#include "quakedef.hpp"
#include "cmd.hpp"
#include "console.hpp"
#include "zone.hpp"
#include "msg.hpp"
#include "common.hpp"
#include "client.hpp"
#include "server.hpp"
#include "sys.hpp"
#include "developer.hpp"
#include "qcvm.hpp"

// QSS
cvar_t cl_nopext = {"cl_nopext", "0",
    CVAR_NONE}; // Spike -- prevent autodetection of protocol extensions, so
                // that servers fall back to only their base protocol (without
                // needing to reconfigure the server. Requires reconnect.

// QSS
cvar_t cmd_warncmd = {"cl_warncmd", "1",
    CVAR_NONE}; // Spike -- prevent autodetection of protocol extensions, so
                // that servers fall back to only their base protocol (without
                // needing to reconfigure the server. Requires reconnect.

#define MAX_ALIAS_NAME 32

#define CMDLINE_LENGTH 256 // johnfitz -- mirrored in common.c

typedef struct cmdalias_s
{
    struct cmdalias_s* next;
    char name[MAX_ALIAS_NAME];
    char* value;
} cmdalias_t;

cmdalias_t* cmd_alias;

bool cmd_wait;

//=============================================================================

/*
============
Cmd_Wait_f

Causes execution of the remainder of the command buffer to be delayed until
next frame.  This allows commands like:
bind g "impulse 5 ; +attack ; wait ; -attack ; impulse 2"
============
*/
void Cmd_Wait_f()
{
    cmd_wait = true;
}

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

                        COMMAND BUFFER

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

sizebuf_t cmd_text;

/*
============
Cbuf_Init
============
*/
void Cbuf_Init()
{
    SZ_Alloc(&cmd_text,
        1 << 18); // space for commands and script files. spike -- was 8192, but
                  // modern configs can be _HUGE_, at least if they contain lots
                  // of comments/docs for things.
}

// QSS
void Cbuf_AddTextLen(const char* text, int l)
{
    if(cmd_text.cursize + l >= cmd_text.maxsize)
    {
        Con_Printf("Cbuf_AddText: overflow\n");
        return;
    }

    SZ_Write(&cmd_text, text, l);
}

/*
============
Cbuf_AddText

Adds command text at the end of the buffer
============
*/
void Cbuf_AddText(const char* text)
{
    Cbuf_AddTextLen(text, Q_strlen(text));
}



/*
============
Cbuf_InsertText

Adds command text immediately after the current command
Adds a \n to the text
FIXME: actually change the command buffer to do less copying
============
*/
void Cbuf_InsertText(const char* text)
{
    char* temp;
    int templen;

    // copy off any commands still remaining in the exec buffer
    templen = cmd_text.cursize;
    if(templen)
    {
        temp = (char*)Z_Malloc(templen);
        Q_memcpy(temp, cmd_text.data, templen);
        SZ_Clear(&cmd_text);
    }
    else
    {
        temp = nullptr; // shut up compiler
    }

    // add the entire text of the file
    Cbuf_AddText(text);
    SZ_Write(&cmd_text, "\n", 1);
    // add the copied off data
    if(templen)
    {
        SZ_Write(&cmd_text, temp, templen);
        Z_Free(temp);
    }
}

// QSS
// Spike: for renderer/server isolation
void Cbuf_Waited()
{
    cmd_wait = false;
}

/*
============
Cbuf_Execute

// QSS
Spike: reworked 'wait' for renderer/server rate independance
============
*/
void Cbuf_Execute()
{
    int i;
    char* text;
    char line[1024];
    int quotes;
    int comment; // QSS

    while(cmd_text.cursize && !cmd_wait /* QSS */)
    {
        // find a \n or ; line break
        text = (char*)cmd_text.data;

        quotes = 0;
        comment = 0; // QSS
        for(i = 0; i < cmd_text.cursize; i++)
        {
            if(text[i] == '"')
            {
                quotes++;
            }

            if(text[i] == '/' && text[i + 1] == '/')
            {
                comment = true;
            }

            if(!(quotes & 1) && !comment && text[i] == ';')
            {
                break; // don't break if inside a quoted string
            }

            if(text[i] == '\n')
            {
                break;
            }
        }

        if(i > (int)sizeof(line) - 1)
        {
            memcpy(line, text, sizeof(line) - 1);
            line[sizeof(line) - 1] = 0;
        }
        else
        {
            memcpy(line, text, i);
            line[i] = 0;
        }

        // delete the text from the command buffer and move remaining commands
        // down this is necessary because commands (exec, alias) can insert data
        // at the beginning of the text buffer

        if(i == cmd_text.cursize)
        {
            cmd_text.cursize = 0;
        }
        else
        {
            i++;
            cmd_text.cursize -= i;
            memmove(text, text + i, cmd_text.cursize);
        }

        // execute the command line
        Cmd_ExecuteString(line, src_command);
    }
}

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

                        SCRIPT COMMANDS

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

/*
===============
Cmd_StuffCmds_f -- johnfitz -- rewritten to read the "cmdline" cvar, for use
with dynamic mod loading

Adds command line parameters as script statements
Commands lead with a +, and continue until a - or another +
quake +prog jctest.qp +cmd amlev1
quake -nosound +cmd amlev1
===============
*/
void Cmd_StuffCmds_f()
{
    extern cvar_t cmdline;
    char cmds[CMDLINE_LENGTH];
    int i;
    int j;
    int plus;

    plus = false; // On Unix, argv[0] is command name

    for(i = 0, j = 0; cmdline.string[i]; i++)
    {
        if(cmdline.string[i] == '+')
        {
            plus = true;
            if(j > 0)
            {
                cmds[j - 1] = ';';
                cmds[j++] = ' ';
            }
        }
        else if(cmdline.string[i] == '-' &&
                (i == 0 || cmdline.string[i - 1] == ' '))
        {
            // johnfitz -- allow hypenated map names with +map
            plus = false;
        }
        else if(plus)
        {
            cmds[j++] = cmdline.string[i];
        }
    }
    cmds[j] = 0;

    Cbuf_InsertText(cmds);
}


/*
===============
Cmd_Exec_f
===============
*/
void Cmd_Exec_f()
{
    if(Cmd_Argc() != 2)
    {
        Con_Printf("exec <filename> : execute a script file\n");
        return;
    }

    const int mark = Hunk_LowMark();
    char* f = (char*)COM_LoadHunkFile(Cmd_Argv(1), nullptr);
    if(!f)
    {
        if(cmd_warncmd.value) // QSS
        {
            Con_Printf("couldn't exec %s\n", Cmd_Argv(1));
        }

        return;
    }

    if(cmd_warncmd.value) // QSS
    {
        Con_Printf("execing %s\n", Cmd_Argv(1));
    }

    Cbuf_InsertText("\n");
    Cbuf_InsertText(f);
    Hunk_FreeToLowMark(mark);
}


/*
===============
Cmd_Echo_f

Just prints the rest of the line to the console
===============
*/
void Cmd_Echo_f()
{
    int i;

    for(i = 1; i < Cmd_Argc(); i++)
    {
        Con_Printf("%s ", Cmd_Argv(i));
    }
    Con_Printf("\n");
}

/*
===============
Cmd_Alias_f -- johnfitz -- rewritten

Creates a new command that executes a command string (possibly ; seperated)
===============
*/
void Cmd_Alias_f()
{
    cmdalias_t* a;
    char cmd[1024];
    int i;
    int c;
    const char* s;


    switch(Cmd_Argc())
    {
        case 1: // list all aliases
            for(a = cmd_alias, i = 0; a; a = a->next, i++)
            {
                Con_SafePrintf("   %s: %s", a->name, a->value);
            }
            if(i)
            {
                Con_SafePrintf("%i alias command(s)\n", i);
            }
            else
            {
                Con_SafePrintf("no alias commands found\n");
            }
            break;
        case 2: // output current alias string
            for(a = cmd_alias; a; a = a->next)
            {
                if(!strcmp(Cmd_Argv(1), a->name))
                {
                    Con_Printf("   %s: %s", a->name, a->value);
                }
            }
            break;
        default: // set alias string
            s = Cmd_Argv(1);
            if(strlen(s) >= MAX_ALIAS_NAME)
            {
                Con_Printf("Alias name is too long\n");
                return;
            }

            // if the alias allready exists, reuse it
            for(a = cmd_alias; a; a = a->next)
            {
                if(!strcmp(s, a->name))
                {
                    Z_Free(a->value);
                    break;
                }
            }

            if(!a)
            {
                a = (cmdalias_t*)Z_Malloc(sizeof(cmdalias_t));
                a->next = cmd_alias;
                cmd_alias = a;
            }
            strcpy(a->name, s);

            // copy the rest of the command line
            cmd[0] = 0; // start out with a null string
            c = Cmd_Argc();
            for(i = 2; i < c; i++)
            {
                q_strlcat(cmd, Cmd_Argv(i), sizeof(cmd));
                if(i != c - 1)
                {
                    q_strlcat(cmd, " ", sizeof(cmd));
                }
            }
            if(q_strlcat(cmd, "\n", sizeof(cmd)) >= sizeof(cmd))
            {
                Con_Printf("alias value too long!\n");
                cmd[0] = '\n'; // nullify the string
                cmd[1] = 0;
            }

            a->value = Z_Strdup(cmd);
            break;
    }
}

/*
===============
Cmd_Unalias_f -- johnfitz
===============
*/
void Cmd_Unalias_f()
{
    cmdalias_t* a;
    cmdalias_t* prev;

    switch(Cmd_Argc())
    {
        default:
        case 1: Con_Printf("unalias <name> : delete alias\n"); break;
        case 2:
            prev = nullptr;
            for(a = cmd_alias; a; a = a->next)
            {
                if(!strcmp(Cmd_Argv(1), a->name))
                {
                    if(prev)
                    {
                        prev->next = a->next;
                    }
                    else
                    {
                        cmd_alias = a->next;
                    }

                    Z_Free(a->value);
                    Z_Free(a);
                    return;
                }
                prev = a;
            }
            Con_Printf("No alias named %s\n", Cmd_Argv(1));
            break;
    }
}

// QSS
bool Cmd_AliasExists(const char* aliasname)
{
    for(cmdalias_t* a = cmd_alias; a; a = a->next)
    {
        if(!q_strcasecmp(aliasname, a->name))
        {
            return true;
        }
    }

    return false;
}

/*
===============
Cmd_Unaliasall_f -- johnfitz
===============
*/
void Cmd_Unaliasall_f()
{
    cmdalias_t* blah;

    while(cmd_alias)
    {
        blah = cmd_alias->next;
        Z_Free(cmd_alias->value);
        Z_Free(cmd_alias);
        cmd_alias = blah;
    }
}

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

                    COMMAND EXECUTION

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

#define MAX_ARGS 80

static int cmd_argc;
static char* cmd_argv[MAX_ARGS];
static char cmd_null_string[] = "";
static const char* cmd_args = nullptr;

cmd_source_t cmd_source;
cmd_function_t* cmd_functions; // possible commands to execute

/*
============
Cmd_List_f -- johnfitz
============
*/
void Cmd_List_f()
{
    cmd_function_t* cmd;
    const char* partial;
    int len;
    int count;

    if(Cmd_Argc() > 1)
    {
        partial = Cmd_Argv(1);
        len = Q_strlen(partial);
    }
    else
    {
        partial = nullptr;
        len = 0;
    }

    count = 0;
    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        if(partial && Q_strncmp(partial, cmd->name, len))
        {
            continue;
        }
        Con_SafePrintf("   %s\n", cmd->name);
        count++;
    }

    Con_SafePrintf("%i commands", count);
    if(partial)
    {
        Con_SafePrintf(" beginning with \"%s\"", partial);
    }
    Con_SafePrintf("\n");
}

static char* Cmd_TintSubstring(
    const char* in, const char* substr, char* out, size_t outsize)
{
    int l;
    char* m;
    q_strlcpy(out, in, outsize);
    while((m = q_strcasestr(out, substr)))
    {
        l = strlen(substr);
        while(l-- > 0)
        {
            if(*m >= ' ' && *m < 127)
            {
                *m++ |= 0x80;
            }
        }
    }
    return out;
}

/*
============
Cmd_Apropos_f

scans through each command and cvar names+descriptions for the given
substring we don't support descriptions, so this isn't really all that
useful, but even without the sake of consistency it still combines
cvars+commands under a single command.
============
*/
void Cmd_Apropos_f()
{
    char tmpbuf[256];
    int hits = 0;
    cmd_function_t* cmd;
    cvar_t* var;
    const char* substr = Cmd_Argv(1);
    if(!*substr)
    {
        Con_SafePrintf(
            "%s <substring> : search through commands and cvars for "
            "the given "
            "substring\n",
            Cmd_Argv(0));
        return;
    }
    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        if(q_strcasestr(cmd->name, substr) &&
            cmd->srctype != src_server /* QSS */)
        {
            hits++;
            Con_SafePrintf("%s\n",
                Cmd_TintSubstring(cmd->name, substr, tmpbuf, sizeof(tmpbuf)));
        }
    }

    for(var = Cvar_FindVarAfter("", 0); var; var = var->next)
    {
        if(q_strcasestr(var->name, substr))
        {
            hits++;
            Con_SafePrintf("%s (current value: \"%s\")\n",
                Cmd_TintSubstring(var->name, substr, tmpbuf, sizeof(tmpbuf)),
                var->string);
        }
    }
    if(!hits)
    {
        Con_SafePrintf("no cvars nor commands contain that substring\n");
    }
}

/*
============
Cmd_Init
============
*/
void Cmd_Init()
{
    Cmd_AddCommand("cmdlist", Cmd_List_f);          // johnfitz
    Cmd_AddCommand("unalias", Cmd_Unalias_f);       // johnfitz
    Cmd_AddCommand("unaliasall", Cmd_Unaliasall_f); // johnfitz

    Cmd_AddCommand("stuffcmds", Cmd_StuffCmds_f);
    Cmd_AddCommand("exec", Cmd_Exec_f);
    Cmd_AddCommand("echo", Cmd_Echo_f);
    Cmd_AddCommand("alias", Cmd_Alias_f);
    Cmd_AddCommand("cmd", Cmd_ForwardToServer);
    Cmd_AddCommand("wait", Cmd_Wait_f);

    Cmd_AddCommand("apropos", Cmd_Apropos_f);
    Cmd_AddCommand("find", Cmd_Apropos_f);

    // QSS
    Cvar_RegisterVariable(&cl_nopext);
    Cvar_RegisterVariable(&cmd_warncmd);
}

/*
============
Cmd_Argc
============
*/
int Cmd_Argc()
{
    return cmd_argc;
}

/*
============
Cmd_Argv
============
*/
const char* Cmd_Argv(int arg)
{
    if(arg < 0 || arg >= cmd_argc)
    {
        return cmd_null_string;
    }
    return cmd_argv[arg];
}

/*
============
Cmd_Args
============
*/
const char* Cmd_Args()
{
    // QSS
    if(!cmd_args)
    {
        return "";
    }

    return cmd_args;
}


/*
============
Cmd_TokenizeString

Parses the given string into command line tokens.
============
*/
void Cmd_TokenizeString(const char* text)
{
    int i;

    // clear the args from the last string
    for(i = 0; i < cmd_argc; i++)
    {
        Z_Free(cmd_argv[i]);
    }

    cmd_argc = 0;
    cmd_args = nullptr;

    while(true)
    {
        // skip whitespace up to a /n
        while(*text && *text <= ' ' && *text != '\n')
        {
            text++;
        }

        if(*text == '\n')
        {
            // a newline seperates commands in the buffer
            text++;
            break;
        }

        if(!*text)
        {
            return;
        }

        if(cmd_argc == 1)
        {
            cmd_args = text;
        }

        text = COM_Parse(text);
        if(!text)
        {
            return;
        }

        if(cmd_argc < MAX_ARGS)
        {
            cmd_argv[cmd_argc] = Z_Strdup(com_token);
            cmd_argc++;
        }
    }
}

/*
============
Cmd_AddCommand

// QSS
spike -- added an extra arg for client (also renamed and made a macro)
============
*/
void Cmd_AddCommand2(
    const char* cmd_name, xcommand_t function, cmd_source_t srctype /* QSS */)
{
    cmd_function_t* cmd;
    cmd_function_t* cursor;
    cmd_function_t* prev; // johnfitz -- sorted list insert

    if(host_initialized && function /* QSS */)
    {
        // because hunk allocation would get stomped
        Sys_Error("Cmd_AddCommand after host_initialized");
    }

    // fail if the command is a variable name
    if(Cvar_VariableString(cmd_name)[0])
    {
        Con_Printf("Cmd_AddCommand: %s already defined as a var\n", cmd_name);
        return;
    }

    // fail if the command already exists
    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        // QSS
        if(!Q_strcmp(cmd_name, cmd->name) && cmd->srctype == srctype)
        {
            if(cmd->function != function && function)
            {
                Con_Printf("Cmd_AddCommand: %s already defined\n", cmd_name);
            }

            return;
        }
    }

    // QSS
    if(host_initialized)
    {
        cmd = (cmd_function_t*)malloc(sizeof(cmd_function_t));
    }
    else
    {
        cmd = (cmd_function_t*)Hunk_Alloc(sizeof(cmd_function_t));
    }

    cmd->name = cmd_name;
    cmd->function = function;
    cmd->srctype = srctype;

    // johnfitz -- insert each entry in alphabetical order
    if(cmd_functions == nullptr ||
        strcmp(cmd->name, cmd_functions->name) < 0) // insert at front
    {
        cmd->next = cmd_functions;
        cmd_functions = cmd;
    }
    else // insert later
    {
        prev = cmd_functions;
        cursor = cmd_functions->next;
        while((cursor != nullptr) && (strcmp(cmd->name, cursor->name) > 0))
        {
            prev = cursor;
            cursor = cursor->next;
        }
        cmd->next = prev->next;
        prev->next = cmd;
    }
    // johnfitz
}

/*
============
Cmd_Exists
============
*/
bool Cmd_Exists(const char* cmd_name)
{
    cmd_function_t* cmd;

    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        if(!Q_strcmp(cmd_name, cmd->name))
        {
            return true;
        }
    }

    return false;
}



/*
============
Cmd_CompleteCommand
============
*/
const char* Cmd_CompleteCommand(const char* partial)
{
    cmd_function_t* cmd;
    int len;

    len = Q_strlen(partial);

    if(!len)
    {
        return nullptr;
    }

    // check functions
    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        if(!Q_strncmp(partial, cmd->name, len))
        {
            return cmd->name;
        }
    }
    return nullptr;
}

/*
============
Cmd_ExecuteString

A complete command line has been parsed, so try to execute it
FIXME: lookupnoadd the token to speed search?
============
*/
bool Cmd_ExecuteString(const char* text, cmd_source_t src)
{
    cmd_function_t* cmd;
    cmdalias_t* a;

    cmd_source = src;
    Cmd_TokenizeString(text);

    // execute the command line
    if(!Cmd_Argc())
    {
        return true; // no tokens
    }

    // QSS
    // check functions
    for(cmd = cmd_functions; cmd; cmd = cmd->next)
    {
        if(!q_strcasecmp(cmd_argv[0], cmd->name))
        {
            if(src == src_client && cmd->srctype != src_client)
            {
                Con_DPrintf("%s tried to %s\n", host_client->name,
                    text); // src_client only allows client commands
            }
            else if(src == src_command && cmd->srctype == src_server)
            {
                continue; // src_command can execute anything but server
                          // commands (which it ignores, allowing for
                          // alternative behaviour)
            }
            else if(src == src_server && cmd->srctype != src_server)
            {
                continue; // src_server may only execute server commands (such
                          // commands must be safe to parse within the context
                          // of a network message, so no
                          // disconnect/connect/playdemo/etc)
            }
            else if(cmd->function)
            {
                cmd->function();
            }
            else
            {
                if(cl.qcvm.extfuncs.CSQC_ConsoleCommand)
                {
                    PR_SwitchQCVM(&cl.qcvm);
                    G_INT(OFS_PARM0) = PR_MakeTempString(text);
                    PR_ExecuteProgram(cl.qcvm.extfuncs.CSQC_ConsoleCommand);

                    const bool ret = G_FLOAT(OFS_RETURN);
                    if(!ret)
                    {
                        Con_Printf("gamecode cannot \"%s\"\n", Cmd_Argv(0));
                    }

                    PR_SwitchQCVM(nullptr);
                    return ret;
                }
                else
                {
                    Con_Printf(
                        "gamecode not running, cannot \"%s\"\n", Cmd_Argv(0));
                }
            }
            return true;
        }
    }

    // QSS
    if(src == src_client)
    { // spike -- please don't execute similarly named aliases, nor custom
      // cvars...
        Con_DPrintf("%s tried to %s\n", host_client->name, text);
        return false;
    }

    // QSS
    if(src != src_command)
    {
        return false;
    }

    // check alias
    for(a = cmd_alias; a; a = a->next)
    {
        if(!q_strcasecmp(cmd_argv[0], a->name))
        {
            Cbuf_InsertText(a->value);
            return true;
        }
    }

    // check cvars
    if(!Cvar_Command())
    {
        if(cmd_warncmd.value || quake::vr::developerMode()) // QSS
        {
            Con_Printf("Unknown command \"%s\"\n", Cmd_Argv(0));
        }
    }

    return true;
}


/*
===================
Cmd_ForwardToServer

Sends the entire command line over to the server
===================
*/
void Cmd_ForwardToServer()
{
    if(cls.state != ca_connected)
    {
        Con_Printf("Can't \"%s\", not connected\n", Cmd_Argv(0));
        return;
    }

    if(cls.demoplayback)
    {
        return; // not really connected
    }

    MSG_WriteByte(&cls.message, clc_stringcmd);
    if(q_strcasecmp(Cmd_Argv(0), "cmd") != 0)
    {
        SZ_Print(&cls.message, Cmd_Argv(0));
        SZ_Print(&cls.message, " ");
    }

    if(Cmd_Argc() > 1)
    {
        SZ_Print(&cls.message, Cmd_Args());
    }
    else
    {
        SZ_Print(&cls.message, "\n");
    }
}


/*
================
Cmd_CheckParm

Returns the position (1 to argc-1) in the command's argument list
where the given parameter apears, or 0 if not present
================
*/

int Cmd_CheckParm(const char* parm)
{
    int i;

    if(!parm)
    {
        Sys_Error("Cmd_CheckParm: NULL");
    }

    for(i = 1; i < Cmd_Argc(); i++)
    {
        if(!q_strcasecmp(parm, Cmd_Argv(i)))
        {
            return i;
        }
    }

    return 0;
}
