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

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

#include <algorithm>
#include <cstring>
#include <sstream>
#include <vector>

#include "quakedef.hpp"

constexpr int CMDLINE_LENGTH = 256;
constexpr int MAX_ARGS = 80;

extern convar cmdline;


namespace command {
    source last_source;

    namespace {
        constexpr int MAX_ALIAS_LENGTH = 32;

        bool waiting{false};
        std::map<std::string, definition, std::less<> > REFS{};
        std::map<std::string, alias, std::less<> > ALIASES{};
        std::vector<std::string> _argv{};
        std::string _args{};

        /**
        * 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 _set_wait() {
            waiting = true;
        }

        void stop_waiting() {
            waiting = false;
        }

        bool is_waiting() {
            return waiting;
        }

        void _list() {
            std::optional<std::string> partial{std::nullopt};

            if (argc() > 1) {
                partial = argv(1);
            }

            auto count = 0;
            for (auto &[name, ref]: std::ranges::views::values(REFS)) {
                if (partial.has_value() && !name.starts_with(partial.value())) {
                    continue;
                }
                console::safe_print("   %s\n", name.c_str());
                count++;
            }

            console::safe_print("%i commands", count);
            if (partial) {
                console::safe_print(" beginning with \"%s\"", partial->c_str());
            }
            console::safe_print("\n");
        }

        void _unalias() {
            switch (argc()) {
                default:
                case 1:
                    console::info("unalias <name> : delete alias\n");
                    break;
                case 2: {
                    const auto name = argv(1).value();
                    if (ALIASES.contains(name)) {
                        ALIASES.erase(name);
                    } else {
                        console::info("No alias named %s\n", name.c_str());
                    }
                    break;
                }
            }
        }

        void _alias() {
            switch (argc()) {
                case 1: {
                    //list all aliases
                    auto count = 0;
                    for (auto &[name, alias]: ALIASES) {
                        console::safe_print("   %s: %s", name.c_str(), alias.value.c_str());
                        count++;
                    }
                    if (count != 0)
                        console::safe_print("%i alias command(s)\n", count);
                    else
                        console::safe_print("no alias commands found\n");
                    break;
                }
                case 2: {
                    //output current alias string
                    const auto lookup = argv(1).value();
                    for (auto &[name, alias]: ALIASES)
                        if (name.starts_with(lookup))
                            console::info("   %s: %s", name.c_str(), alias.value.c_str());
                    break;
                }
                default: {
                    //set alias string
                    const auto new_name = argv(1).value();
                    if (new_name.length() >= MAX_ALIAS_LENGTH) {
                        console::info("Alias name is too long\n");
                        return;
                    }

                    std::optional<alias> new_alias = std::nullopt;
                    // if the alias already exists, reuse it
                    for (auto &[name, alias]: ALIASES) {
                        if (name == new_name) {
                            new_alias = alias;
                            alias.value.clear();
                            break;
                        }
                    }

                    if (!new_alias.has_value()) {
                        new_alias = alias{};
                    }
                    new_alias->name = new_name;

                    // copy the rest of the command line
                    auto cmd = std::string{};
                    const auto count = argc();
                    for (auto i = 2; i < count; i++) {
                        cmd += argv(i).value();
                        if (i != count - 1) {
                            cmd += ' ';
                        }
                    }
                    cmd += '\n';
                    new_alias->value = cmd;
                    break;
                }
            }
        }

        void _unalias_all() {
            ALIASES.clear();
        }

        /*
        ===============
        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 _stuffcmds() {
            char cmds[CMDLINE_LENGTH];
            int i, j;

            auto 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;

            buffer::insert_text(cmds);
        }

#include "default_cfg.hpp"

        void _exec() {
            if (argc() != 2) {
                console::info("exec <filename> : execute a script file\n");
                return;
            }

            const auto mark = Hunk_LowMark();
            const auto filename = argv(1);

            auto f = reinterpret_cast<const char *>(COM_LoadHunkFile(filename->c_str(), nullptr));

            if (f == nullptr && argv(1) == "default.cfg") {
                f = default_cfg; /* see above.. */
            }
            if (f == nullptr) {
                console::info("couldn't exec %s\n", argv(1)->c_str());
                return;
            }
            console::info("execing %s\n", argv(1)->c_str());

            buffer::insert_text(f);
            if (f != default_cfg) {
                Hunk_FreeToLowMark(mark);
            }
        }

        void _echo() {
            for (auto i = 1; i < argc(); i++)
                console::info("%s ", argv(i)->c_str());
            console::info("\n");
        }

        char *tint_substring(const char *in, const char *substr, char *out, const 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;
        }

        /*
        ============
        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 _apropos() {
            char tmpbuf[256];
            int hits = 0;
            const auto substr = argv(1);
            if (!substr.has_value()) {
                console::safe_print("%s <substring> : search through commands and cvars for the given substring\n",
                               argv(0).value().c_str());
                return;
            }
            for (const auto &name: REFS | std::views::keys) {
                if (q_strcasestr(name.c_str(), substr.value().c_str())) {
                    hits++;
                    console::safe_print(
                        "%s\n", tint_substring(name.c_str(), substr.value().c_str(), tmpbuf, sizeof(tmpbuf)));
                }
            }

            for (const auto &var: convar::get_variables()) {
                if (q_strcasestr(var->name.c_str(), substr.value().c_str())) {
                    hits++;
                    console::safe_print("%s (current value: \"%s\")\n",
                                   tint_substring(var->name.c_str(), substr.value().c_str(), tmpbuf, sizeof(tmpbuf)),
                                   var->string);
                }
            }
            if (!hits)
                console::safe_print("no cvars nor commands contain that substring\n");
        }
    }


    void init() {
        add("cmdlist", _list); //johnfitz
        add("unalias", _unalias); //johnfitz
        add("unaliasall", _unalias_all); //johnfitz

        add("stuffcmds", _stuffcmds);
        add("exec", _exec);
        add("echo", _echo);
        add("alias", _alias);
        add("cmd", forward_to_server);
        add("wait", _set_wait);

        add("apropos", _apropos);
        add("find", _apropos);
    }

    std::optional<std::string> complete(const std::string &partial) {
        if (partial.empty())
            return std::nullopt;

        for (const auto &name: REFS | std::views::keys) {
            if (name.compare(0, partial.length(), partial) == 0)
                return name;
        }
        return std::nullopt;
    }

    void add(const std::string &name, const xcommand_t cmd) {
        if (host_initialized) // because hunk allocation would get stomped
            Sys_Error("Cmd_AddCommand after host_initialized");

        // fail if the command is a variable name
        if (convar::variable_string(name).has_value()) {
            console::info("Cmd_AddCommand: %s already defined as a var\n", name.c_str());
            return;
        }

        // fail if the command already exists
        for (auto &[other_name, func]: std::ranges::views::values(REFS)) {
            if (name == other_name) {
                console::info("Cmd_AddCommand: %s already defined\n", name.c_str());
                return;
            }
        }

        REFS.emplace(name, definition{name, cmd});
    }

    bool exists(const std::string_view lookup) {
        return std::ranges::any_of(REFS.begin(), REFS.end(), [lookup](const auto &pair) {
            return pair.first == lookup;
        });
    }

    int argc() {
        return static_cast<int>(_argv.size());
    }

    std::optional<std::string> argv(const int arg) {
        if (arg < 0 || arg >= _argv.size()) {
            return std::nullopt;
        }
        return _argv[arg];
    }

    std::string args() {
        return _args;
    }

    int check_parm(const std::string &parm) {
        if (parm.empty())
            Sys_Error("Cmd_CheckParm: null input\n");

        for (auto i = 1; i < argc(); i++)
            if (std::is_eq(common::strcasecmp(parm, *argv(i))))
                return i;

        return 0;
    }

    /**
     * Parses the given string into command line tokens.
     * @param text
     */
    void tokenize_string(const std::string &text) {
        _argv.clear();
        _args = std::string{};
        std::istringstream ss{text};
        while (true) {
            while (!ss.eof() && ss.peek() <= ' ' && ss.peek() != '\n') {
                ss.get();
            }

            if (ss.peek() == '\n')
                break;

            if (ss.eof())
                return;

            if (_argv.size() == 1)
                _args = ss.str().substr(ss.tellg());

            if (_argv.size() >= MAX_ARGS) {
                return;
            }

            auto token = common::parse_token(ss);
            if (!token.has_value()) {
                return;
            }
            _argv.emplace_back(token.value());
        }
    }

    void execute_string(const std::string &text, const source src) {
        last_source = src;
        tokenize_string(text);

        // execute the command line
        if (argc() == 0)
            return; // no tokens

        const auto needle = argv(0).value();
        // check functions
        for (const auto &[name, ref]: REFS) {
            if (std::is_eq(common::strcasecmp(needle, name.c_str()))) {
                ref.func();
                return;
            }
        }

        // check alias
        for (const auto &[name, alias]: ALIASES) {
            if (std::is_eq(common::strcasecmp(argv(0).value().c_str(), name.c_str()))) {
                buffer::insert_text(alias.value);
                return;
            }
        }

        // check cvars
        if (!Cvar_Command())
            console::info("Unknown command \"%s\"\n", argv(0).value().c_str());
    }

    void forward_to_server() {
        if (cls.state != ca_connected) {
            console::info("Can't \"%s\", not connected\n", argv(0).value_or("").c_str());
            return;
        }

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

        cls.message.write_byte(clc_stringcmd);
        if (std::is_neq(common::strcasecmp(argv(0).value_or(""), "cmd"))) {
            if (argc() > 1) {
                cls.message.write_string(std::format("{} {}", argv(0).value(), args()));
            } else {
                cls.message.write_string(std::format("{} \n", argv(0).value()));
            }
        } else {
            if (argc() > 1) {
                cls.message.write_string(std::format("{}", args()));
            } else {
                cls.message.write_string("\n");
            }
        }
    }

    definition_view get_commands() {
        return std::ranges::views::values(REFS);
    }

    alias_view get_aliases() {
        return std::ranges::views::values(ALIASES);
    }
}


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


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

                        COMMAND BUFFER

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


namespace command::buffer {
    namespace {
        constexpr std::size_t MAX_BUFFER_SIZE = 1 << 18;
        std::string _buffer;
    }

    void init() {
        _buffer = std::string{};
        _buffer.reserve(MAX_BUFFER_SIZE);
    }

    void add_text(const std::string &text) {
        if (_buffer.length() + text.length() >= MAX_BUFFER_SIZE) {
            console::info("command::buffer::add_text(): overflow\n");
            return;
        }

        _buffer.append(text);
    }

    void insert_text(const std::string &text) {
        if (_buffer.length() + text.length() >= MAX_BUFFER_SIZE) {
            console::info("command::buffer::insert_text(): overflow\n");
            return;
        }

        _buffer = text + _buffer;
    }

    void execute() {
        while (!_buffer.empty()) {
            std::string line{};
            auto quotes = 0;
            for (const auto c: _buffer) {
                if (c == '"') {
                    quotes++;
                }
                if (!(quotes & 1) && c == ';') {
                    break; // don't break if inside a quoted string
                }
                if (c == '\n') {
                    break;
                }
                line += c;
            }

            // 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 (line.length() + 1 == _buffer.length()) {
                _buffer.clear();
            } else {
                _buffer = _buffer.substr(line.length() + 1);
            }

            // execute the command line
            execute_string(line, source::command);

            if (is_waiting()) {
                // skip out while text still remains in buffer, leaving it
                // for next frame
                stop_waiting();
                break;
            }
        }
    }
}
