/*
	Copyright (C) 2006 - 2024
	by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
	Copyright (C) 2003 by David White <dave@whitevine.net>
	Part of the Battle for Wesnoth Project https://www.wesnoth.org/

	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.

	See the COPYING file for more details.
*/

#pragma once
#include "config.hpp"
#include "serialization/string_utils.hpp"
#include "formula/string_utils.hpp"
#include "gettext.hpp"

#include <boost/algorithm/string.hpp>

namespace events {

//simple command args parser, separated from command_handler for clarity.
//a word begins with a nonspace
//n-th arg is n-th word up to the next space
//n-th data is n-th word up to the end
//cmd is 0-th arg, begins at 0 always.
class cmd_arg_parser
{
public:
	cmd_arg_parser() :
		str_(""),
		args(1, 0),
		args_end(false)
	{
	}

	explicit cmd_arg_parser(const std::string& str) :
		str_(str),
		args(1, 0),
		args_end(false)
	{
	}

	void parse(const std::string& str)
	{
		str_ = str;
		args.clear();
		args.push_back(0);
		args_end = false;
	}

	const std::string& get_str() const
	{
		return str_;
	}
	std::string get_arg(unsigned n) const
	{
		advance_to_arg(n);
		if (n < args.size()) {
			return std::string(str_, args[n], str_.find(' ', args[n]) - args[n]);
		}
		else {
			return "";
		}
	}
	std::string get_data(unsigned n) const
	{
		advance_to_arg(n);
		if (n < args.size()) {
			std::string data(str_, args[n]);
			boost::trim(data);
			return data;
		}
		else {
			return "";
		}
	}
	std::string get_cmd() const
	{
		return get_arg(0);
	}
private:
	cmd_arg_parser& operator=(const cmd_arg_parser&);
	cmd_arg_parser(const cmd_arg_parser&);
	void advance_to_arg(unsigned n) const
	{
		while (n < args.size() && !args_end) {
			std::size_t first_space = str_.find_first_of(' ', args.back());
			std::size_t next_arg_begin = str_.find_first_not_of(' ', first_space);
			if (next_arg_begin != std::string::npos) {
				args.push_back(next_arg_begin);
			}
			else {
				args_end = true;
			}
		}
	}
	std::string str_;
	mutable std::vector<std::size_t> args;
	mutable bool args_end;
};


//A helper class template with a slim public interface
//This represents a map of strings to void()-member-function-of-Worker-pointers
//with all the common functionality like general help, command help and aliases
//Usage (of a derived class): Derived(specific-arguments) d; d.dispatch(command);
//Derived classes should override virtual functions where noted.
//The template parameter currently must be the dervived class itself,
//i.e. class X : public map_command_handler<X>
//To add a new command in a derived class:
//  * add a new private void function() to the derived class
//  * add it to the function map in init_map there, setting flags like
//    "D" for debug only (checking the flag is also done in the derived class)
//  * remember to add some help and/or usage information in init_map()
template <class Worker>
class map_command_handler
{
public:
	typedef void (Worker::*command_handler)();
	struct command
	{
		command_handler handler;
		std::string help; //long help text
		std::string usage; //only args info
		std::string flags; //< see implementation of get_command_flags_description() for meaning of flags
		explicit command(command_handler h, const std::string& help = "",
			const std::string& usage = "", const std::string& flags = "")
			: handler(h), help(help), usage(usage), flags(flags)
		{
		}
		bool has_flag(const char f) const
		{
			return flags.find(f) != flags.npos;
		}
		command& add_flag(const char f)
		{
			flags += f;
			return *this;
		}
	};
	typedef std::map<std::string, command> command_map;
	typedef std::map<std::string, std::string> command_alias_map;

	map_command_handler() : cap_("")
	{
	}

	virtual ~map_command_handler() {}

	bool empty() const
	{
		return command_map_.empty();
	}
	//actual work function
	void dispatch(std::string cmd)
	{
		if (empty()) {
			init_map_default();
			init_map();
		}

		// We recursively resolve alias (100 max to avoid infinite recursion)
		for (int i = 0; i < 100; ++i) {
			parse_cmd(cmd);
			std::string actual_cmd = get_actual_cmd(get_cmd());
			if (actual_cmd == get_cmd())
				break;
			std::string data = get_data(1);
			// translate the command and add space + data if any
			cmd = actual_cmd + (data.empty() ? "" : " ") + data;
		}

		if (get_cmd().empty()) {
			return;
		}

		if (const command* c = get_command(get_cmd())) {
			if (is_enabled(*c)) {
				(static_cast<Worker*>(this)->*(c->handler))();
			}
			else {
				print(get_cmd(), _("This command is currently unavailable."));
			}
		}
		else if (help_on_unknown_) {
			utils::string_map symbols;
			if(!cmd_flag_) {
				symbols["help_command"] = cmd_prefix_ + "help";
				symbols["command"] = cmd_prefix_ + get_cmd();
			}
			else {
				symbols["help_command"] = "help";
				symbols["command"] = get_cmd();
			}
			std::string string_user = get_cmd();
			int distance = 0;
			// Minimum length of the two compared strings.
			int len_min = 0;
			bool has_command_proposal = false;
			// Compare the input with every command (excluding alias).
			for(const auto& [key, index] : command_map_) {
				// No need to test commands that are not enabled.
				if(is_enabled(index)) {
					distance = edit_distance_approx(string_user, key);
					len_min = std::min(string_user.length(), key.length());
					// Maximum of a third of the letters are wrong. The ratio
					// between the edit distance and the minimum length, multiplied
					// by a hundred gives us the  percentage of errors.
					if(distance * 100 / len_min < 34) {
						symbols["command_proposal"] = key;
						has_command_proposal = true;
						// If a good enough candidate is found, exit the loop.
						break;
					}
				}
			}
			// If a proposal for a command is found, print it
			if(has_command_proposal) {
				print("help", VGETTEXT("Unknown command '$command', did you mean '$command_proposal'? try $help_command "
					"for a list of available commands.", symbols));
			}
			else {
				print("help", VGETTEXT("Unknown command '$command', try $help_command "
					"for a list of available commands.", symbols));
			}
		}
	}

	std::vector<std::string> get_commands_list() const
	{
		std::vector<std::string> res;
		for (typename command_map::value_type i : command_map_) {
			res.push_back(i.first);
		}
		return res;
	}
	//command error reporting shorthands
	void command_failed(const std::string& message, bool = false)
	{
		print(get_cmd(), _("Error:") + std::string(" ") + message);
	}
protected:
	void init_map_default()
	{
		register_command("help", &map_command_handler<Worker>::help,
			_("Available commands list and command-specific help. "
				"Use \"help all\" to include currently unavailable commands."),
			// TRANSLATORS: These are the arguments accepted by the "help" command,
			// which are either "all" or the name of another command.
			// As with the command's name, "all" is hardcoded, and shouldn't change in the translation.
			_("[all|<command>]\n“all” = overview of all commands, <command> = name of a specific command (provides more detail)"));
	}
	//derived classes initialize the map overriding this function
	virtual void init_map() = 0;
	//overridden in derived classes to actually print the messages somwehere
	virtual void print(const std::string& title, const std::string& message) = 0;
	//should be overridden in derived classes if the commands have flags
	//this should return a string describing what all the flags mean
	virtual std::string get_flags_description() const
	{
		return "";
	}
	//this should return a string describing the flags of the given command
	virtual std::string get_command_flags_description(const command& /*c*/) const
	{
		return "";
	}
	//this should be overridden if e.g. flags are used to control command
	//availability. Return false if the command should not be executed by dispatch()
	virtual bool is_enabled(const command& /*c*/) const
	{
		return true;
	}
	virtual void parse_cmd(const std::string& cmd_string)
	{
		cap_.parse(cmd_string);
	}
	//safe n-th argunment getter
	virtual std::string get_arg(unsigned argn) const
	{
		return cap_.get_arg(argn);
	}
	//"data" is n-th arg and everything after it
	virtual std::string get_data(unsigned argn = 1) const
	{
		return cap_.get_data(argn);
	}
	virtual std::string get_cmd() const
	{
		return cap_.get_cmd();
	}
	void command_failed_need_arg(int argn)
	{
		utils::string_map symbols;
		symbols["arg_id"] = std::to_string(argn);
		command_failed(VGETTEXT("Missing argument $arg_id", symbols));
	}
	void print_usage()
	{
		help_command(get_cmd());
	}
	//take aliases into account
	std::string get_actual_cmd(const std::string& cmd) const
	{
		command_alias_map::const_iterator i = command_alias_map_.find(cmd);
		return i != command_alias_map_.end() ? i->second : cmd;
	}
	const command* get_command(const std::string& cmd) const
	{
		typename command_map::const_iterator i = command_map_.find(cmd);
		return i != command_map_.end() ? &i->second : 0;
	}
	command* get_command(const std::string& cmd)
	{
		typename command_map::iterator i = command_map_.find(cmd);
		return i != command_map_.end() ? &i->second : 0;
	}
	void help()
	{
		//print command-specific help if available, otherwise list commands
		if (help_command(get_arg(1))) {
			return;
		}
		std::stringstream ss;
		bool show_unavail = show_unavailable_ || get_arg(1) == "all";
		for (typename command_map::value_type i : command_map_) {
			if (show_unavail || is_enabled(i.second)) {
				ss << i.first;
				//if (!i.second.usage.empty()) {
				//	ss << " " << i.second.usage;
				//}
				//uncomment the above to display usage information in command list
				//which might clutter it somewhat
				if (!i.second.flags.empty()) {
					ss << " (" << i.second.flags << ") ";
				}
				ss << "; ";
			}
		}
		utils::string_map symbols;
		symbols["flags_description"] = get_flags_description();
		symbols["list_of_commands"] = ss.str();
		if(!cmd_flag_) {
			symbols["help_command"] = cmd_prefix_ + "help";
		}
		else {
			symbols["help_command"] = "help";
		}
		print(_("help"), VGETTEXT("Available commands $flags_description:\n$list_of_commands", symbols));
		print(_("help"), VGETTEXT("Type $help_command <command> for more info.", symbols));
	}
	//returns true if the command exists.
	bool help_command(const std::string& acmd)
	{
		std::string cmd = get_actual_cmd(acmd);
		const command* c = get_command(cmd);
		if (c) {
			std::stringstream ss;
			if(!cmd_flag_) {
				ss << cmd_prefix_ << cmd;
			}
			else {
				ss << cmd;
			}
			if (c->help.empty() && c->usage.empty()) {
				ss << _(" No help available.");
			}
			else {
				ss << " - " << c->help << "\n";
			}
			if (!c->usage.empty()) {
				if(!cmd_flag_) {
					ss << _("Usage:") << " " << cmd_prefix_ << cmd << " " << c->usage << "\n";
				}
				else {
					ss << _("Usage:") << " " << cmd << " " << c->usage << "\n";
				}
			}
			const auto flags_description = get_command_flags_description(*c);
			if (!flags_description.empty()) {
				// This shares the objectives dialog's translation of "Notes:"
				ss << _("Notes:") << " " << get_command_flags_description(*c) << "\n";
			}
			const std::vector<std::string> l = get_aliases(cmd);
			if (!l.empty()) {
				// TRANSLATORS: alternative names for command-line commands, only shown if
				// there is at least one of them.
				ss << _n("command^Alias:", "Aliases:", l.size()) << " " << utils::join(l, " ") << "\n";
			}
			print(_("help"), ss.str());
		}
		return c != 0;
	}
	cmd_arg_parser cap_;
protected:
	//show a "try help" message on unknown command?
	static void set_help_on_unknown(bool value)
	{
		help_on_unknown_ = value;
	}
	//this is display-only
	static void set_cmd_prefix(const std::string& value)
	{
		cmd_prefix_ = value;
	}
	//sets the "cmd_flag_" flag as "true" when "cmd_prefix_" is in command
	//line mode and to "false" when "cmd_prefix_" is in message line mode.
	//The "help" message's symbols depend on the flag's value.
	static void set_cmd_flag(bool value)
	{
		cmd_flag_ = value;
	}
	virtual void register_command(const std::string& cmd,
		command_handler h, const std::string& help = "",
		const std::string& usage = "", const std::string& flags = "")
	{
		command c = command(h, help, usage, flags);
		std::pair<typename command_map::iterator, bool> r;
		r = command_map_.insert(typename command_map::value_type(cmd, c));
		if (!r.second) { //overwrite if exists
			r.first->second = c;
		}
	}

	virtual void register_alias(const std::string& to_cmd,
		const std::string& cmd)
	{
		command_alias_map_[cmd] = to_cmd;
	}

	//get all aliases of a command.
	static const std::vector<std::string> get_aliases(const std::string& cmd)
	{
		std::vector<std::string> aliases;
		typedef command_alias_map::value_type p;
		for (p i : command_alias_map_) {
			if (i.second == cmd) {
				aliases.push_back(i.first);
			}
		}
		return aliases;
	}
private:
	static inline command_map command_map_ {};
	static inline command_alias_map command_alias_map_ {};
	static inline bool help_on_unknown_ = true;
	static inline bool show_unavailable_ = false;
	static inline std::string cmd_prefix_ {};
	static inline bool cmd_flag_ = false;
};

}
