/**
 * Author: dkanus
 * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
 * License: GPL
 * Copyright 2021-2023 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia 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, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia 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 Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class Command extends AcediaObject
    dependson(BaseText);

//! This class is meant to represent a command type.
//!
//! Command class provides an automated way to add a command to a server through
//! AcediaCore's features. It takes care of:
//!
//!     1. Verifying that player has passed correct (expected parameters);
//!     2. Parsing these parameters into usable values (both standard, built-in
//!         types like `bool`, `int`, `float`, etc. and more advanced types such
//!         as players lists and JSON values);
//!     3. Allowing you to easily specify a set of players you are targeting by
//!         supporting several ways to refer to them, such as *by name*, *by id*
//!         and *by selector* (@ and @self refer to caller player, @all refers
//!         to all players).
//!     4. It can be registered inside AcediaCore's commands feature and be
//!         automatically called through the unified system that supports *chat*
//!         and *mutate* inputs (as well as allowing you to hook in any other
//!         input source);
//!     5. Will also automatically provide a help page through built-in "help"
//!         command;
//!     6. Subcommand support - when one command can have several distinct
//!         functions, depending on how its called (e.g. "inventory add" vs
//!         "inventory remove"). These subcommands have a special treatment in
//!         help pages, which makes them more preferable, compared to simply
//!         matching first `Text` argument;
//!     7. Add support for "options" - additional flags that can modify commands
//!         behavior and behave like usual command options "--force"/"-f".
//!         Their short versions can even be combined:
//!         "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af".
//!         And they can have their own parameters: "give@all --list sharp".
//!
//! # Implementation
//!
//! The idea of `Command`'s implementation is simple: command is basically the
//! `Command.Data` struct that is filled via `CommandDataBuilder`.
//! Whenever command is called it uses `CommandParser` to parse user's input
//! based on its `Command.Data` and either report error (in case of failure) or
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
//!
//! When command is called is decided by `Commands_Feature` that tracks possible
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
//! adding custom command inputs). That feature basically parses first part of
//! the command: its name (not the subcommand's names) and target players
//! (using `PlayersParser`, but only if command is targeted).
//!
//! Majority of the command-related code either serves to build `Command.Data`
//! or to parse command input by using it (`CommandParser`).

/// Possible errors that can arise when parsing command parameters from user
/// input
enum ErrorType {
    /// No error
    CET_None,
    /// Bad parser was provided to parse user input (this should not be possible)
    CET_BadParser,
    /// Sub-command name was not specified or was incorrect
    /// (this should not be possible)
    CET_NoSubCommands,
    /// Specified sub-command does not exist
    /// (only relevant when it is enforced for parser, e.g. by an alias)
    CET_BadSubCommand,
    /// Required param for command / option was not specified
    CET_NoRequiredParam,
    CET_NoRequiredParamForOption,
    /// Unknown option key was specified
    CET_UnknownOption,
    /// Unknown short option key was specified
    CET_UnknownShortOption,
    /// Same option appeared twice in one command call
    CET_RepeatedOption,
    /// Part of user's input could not be interpreted as a part of
    /// command's call
    CET_UnusedCommandParameters,
    /// In one short option specification (e.g. '-lah') several options require
    /// parameters: this introduces ambiguity and is not allowed
    CET_MultipleOptionsWithParams,
    /// Targets are specified incorrectly (for targeted commands only)
    CET_IncorrectTargetList,
    // No targets are specified (for targeted commands only)
    CET_EmptyTargetList
};

/// Structure that contains all the information about how `Command` was called.
struct CallData {
    /// Targeted players (if applicable)
    var public array<EPlayer> targetPlayers;
    /// Specified sub-command and parameters/options
    var public Text subCommandName;
    /// Provided parameters and specified options
    var public HashTable parameters;
    var public HashTable options;
    /// Errors that occurred during command call processing are described by
    /// error type.
    var public ErrorType parsingError;
    /// Optional error textual name of the object (parameter, option, etc.)
    /// that caused it.
    var public Text errorCause;
};

/// Possible types of parameters.
enum ParameterType {
    /// Parses into `BoolBox`
    CPT_Boolean,
    /// Parses into `IntBox`
    CPT_Integer,
    /// Parses into `FloatBox`
    CPT_Number,
    /// Parses into `Text`
    CPT_Text,
    /// Special parameter that consumes the rest of the input into `Text`
    CPT_Remainder,
    /// Parses into `HashTable`
    CPT_Object,
    /// Parses into `ArrayList`
    CPT_Array,
    /// Parses into any JSON value
    CPT_JSON,
    /// Parses into an array of specified players
    CPT_Players
};

/// Possible forms a boolean variable can be used as.
/// Boolean parameter can define it's preferred format, which will be used for
/// help page generation.
enum PreferredBooleanFormat {
    PBF_TrueFalse,
    PBF_EnableDisable,
    PBF_OnOff,
    PBF_YesNo
};

// Defines a singular command parameter
struct Parameter {
    /// Display name (for the needs of help page displaying)
    var Text displayName;
    /// Type of value this parameter would store
    var ParameterType type;
    /// Does it take only a singular value or can it contain several of them,
    /// written in a list
    var bool allowsList;
    /// Variable name that will be used as a key to store parameter's value
    var Text variableName;
    /// (For `CPT_Boolean` type variables only) - preferred boolean format,
    /// used in help pages
    var PreferredBooleanFormat  booleanFormat;
    /// `CPT_Text` can be attempted to be auto-resolved as an alias from some
    /// source during parsing.
    /// For command to attempt that, this field must be not-`none` and contain
    /// the name of the alias source (either "weapon", "color", "feature",
    /// "entity" or some kind of custom alias source name).
    ///
    /// Only relevant when given value is prefixed with "$" character.
    var Text aliasSourceName;
};

/// Defines a sub-command of a this command
/// (specified as "<command> <sub_command>").
///
/// Using sub-command is not optional, but if none defined
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
/// one is automatically created / used.
struct SubCommand {
    /// Name of the sub command. Cannot be `none`.
    var Text name;
    /// Human-readable description of the subcommand. Can be `none`.
    var Text description;
    /// List of required parameters of this [`Command`].
    var array<Parameter> required;
    /// List of optional parameters of this [`Command`].
    var array<Parameter> optional;
};

/// Defines command's option (options are specified by "--long" or "-l").
/// Options are independent from sub-commands.
struct Option {
    /// [`Option`]'s short name, i.e. a single letter "f" that can be specified
    /// in, e.g. "-laf" type option listings
    var BaseText.Character shortName;
    /// [`Option`]'s full name, e.g. "--force".
    var Text longName;
    /// Human-readable description of the option. Can be `none`.
    var Text description;
    /// List of required parameters of this [`Command::Option`].
    var array<Parameter> required;
    /// List of required parameters of this [`Command::Option`].
    var array<Parameter> optional;
};

/// Structure that defines what sub-commands and options command has
/// (and what parameters they take)
struct Data {
    /// Command group this command belongs to
    var protected Text group;
    /// Short summary of what command does (recommended to
    /// keep it to 80 characters)
    var protected Text summary;
    /// Available subcommands.
    var protected array<SubCommand> subCommands;
    /// Available options, common to all subcommands.
    var protected array<Option> options;
    /// `true` iff related [`Command`] targets players.
    var protected bool requiresTarget;
};
var protected Data commandData;

/// Setting variable that defines a name that will be chosen for command by
/// default.
var protected const string preferredName;
/// Name that was used to register this command.
var protected Text usedName;
/// Settings variable that defines a class to be used for this [`Command`]'s
/// permissions config
var protected const class<CommandPermissions> permissionsConfigClass;

// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance;

/// When command is being executed we create several instances of
/// `ConsoleWriter` that can be used for command output.
/// They will also be automatically deallocated once command is executed.
///
/// DO NOT modify them or deallocate any of them manually.
///
/// This should make output more convenient and standardized.
///
/// 1. `publicConsole` - sends messages to all present players;
/// 2. `callerConsole` - sends messages to the player that called the command;
/// 3. `targetConsole` - sends messages to the player that is currently being
///     targeted (different each call of `ExecutedFor()` and `none` during
///     `Executed()` call);
/// 4. `othersConsole` - sends messaged to every player that is neither
///     "caller" or "target".
var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole;

protected function Constructor() {
    local CommandDataBuilder dataBuilder;

    if (permissionsConfigClass != none) {
        permissionsConfigClass.static.Initialize();
    }
    dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
    // Let user fill-in the rest
    BuildData(dataBuilder);
    commandData = dataBuilder.BorrowData();
    dataBuilder.FreeSelf();
    dataBuilder = none;
}

protected function Finalizer() {
    local int i;
    local array<SubCommand> subCommands;
    local array<Option> options;

    DeallocateConsoles();
    _.memory.Free(usedName);
    _.memory.Free(commandData.summary);
    usedName = none;
    commandData.summary = none;
    subCommands = commandData.subCommands;
    for (i = 0; i < options.length; i += 1) {
        _.memory.Free(subCommands[i].name);
        _.memory.Free(subCommands[i].description);
        CleanParameters(subCommands[i].required);
        CleanParameters(subCommands[i].optional);
        subCommands[i].required.length = 0;
        subCommands[i].optional.length = 0;
    }
    commandData.subCommands.length = 0;
    options = commandData.options;
    for (i = 0; i < options.length; i += 1) {
        _.memory.Free(options[i].longName);
        _.memory.Free(options[i].description);
        CleanParameters(options[i].required);
        CleanParameters(options[i].optional);
        options[i].required.length = 0;
        options[i].optional.length = 0;
    }
    commandData.options.length = 0;
}

/// Initializes command, providing it with a specific name.
///
/// Argument cannot be `none`, otherwise initialization fails.
/// [`Command`] can only be successfully initialized once.
public final function bool Initialize(BaseText commandName) {
    if (commandName == none)    return false;
    if (usedName != none)       return false;

    usedName = commandName.LowerCopy();
    return true;
}

/// Overload this method to use `builder` to define parameters and options for
/// your command.
protected function BuildData(CommandDataBuilder builder){}

/// Overload this method to perform required actions when your command is
/// called.
///
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function Executed(
    CallData arguments,
    EPlayer instigator,
    CommandPermissions permissions) {}

/// Overload this method to perform required actions when your command is called
/// with a given player as a target.
///
/// If several players have been specified - this method will be called once
/// for each.
///
/// If your command does not require a target - this method will not be called.
///
/// [`target`] is a player that this command must perform an action on.
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function ExecutedFor(
    EPlayer target,
    CallData arguments,
    EPlayer instigator,
    CommandPermissions permissions) {}

/// Returns an instance of command (of particular class) that is stored
/// "as a singleton" in command's class itself. Do not deallocate it.
public final static function Command GetInstance() {
    if (default.mainInstance == none) {
        default.mainInstance = Command(__().memory.Allocate(default.class));
    }
    return default.mainInstance;
}

/// Forces command to process (parse) player's input, producing a structure with
/// parsed data in Acedia's format instead.
///
/// Use `Execute()` for actually performing command's actions.
///
/// [`subCommandName`] can be optionally specified to use as sub-command.
/// If this argument's value is `none` - sub-command name will be parsed from
/// the `parser`'s data.
///
/// Returns `CallData` structure that contains all the information about
/// parameters specified in `parser`'s contents.
/// Returned structure contains objects that must be deallocated, which can
/// easily be done by the auxiliary `DeallocateCallData()` method.
public final function CallData ParseInputWith(
    Parser parser,
    EPlayer callerPlayer,
    optional BaseText subCommandName
) {
    local array<EPlayer> targetPlayers;
    local CommandParser commandParser;
    local CallData callData;

    if (parser == none || !parser.Ok()) {
        callData.parsingError = CET_BadParser;
        return callData;
    }
    // Parse targets and handle errors that can arise here
    if (commandData.requiresTarget) {
        targetPlayers = ParseTargets(parser, callerPlayer);
        if (!parser.Ok()) {
            callData.parsingError = CET_IncorrectTargetList;
            return callData;
        }
        if (targetPlayers.length <= 0) {
            callData.parsingError = CET_EmptyTargetList;
            return callData;
        }
    }
    // Parse parameters themselves
    commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
    callData = commandParser.ParseWith(
        parser,
        commandData,
        callerPlayer,
        subCommandName);
    callData.targetPlayers = targetPlayers;
    commandParser.FreeSelf();
    return callData;
}

/// Executes caller `Command` with data provided by `callData` if it is in
/// a correct state and reports error to `callerPlayer` if `callData` is
/// invalid.
///
/// Returns `true` if command was successfully executed and `false` otherwise.
/// Execution is considered successful if `Execute()` call was made, regardless
/// of whether `Command` can actually perform required action.
/// For example, giving a weapon to a player can fail because he does not have
/// enough space in his inventory, but it will still be considered a successful
/// execution as far as return value is concerned.
///
/// [`permissions`] argument is supposed to specify permissions with which this
/// command runs.
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
/// being `none` should mean running with minimal priviledges.
public final function bool Execute(
    CallData callData,
    EPlayer callerPlayer,
    CommandPermissions permissions
) {
    local int i;
    local array<EPlayer> targetPlayers;

    if (callerPlayer == none)       return false;
    if (!callerPlayer.IsExistent()) return false;

    // Report or execute
    if (callData.parsingError != CET_None) {
        ReportError(callData, callerPlayer);
        return false;
    }
    targetPlayers = callData.targetPlayers;
    publicConsole = _.console.ForAll();
    callerConsole = _.console.For(callerPlayer);
    callerConsole
        .Write(P("Executing command `"))
        .Write(usedName)
        .Say(P("`"));
    // `othersConsole` should also exist in time for `Executed()` call
    othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
    Executed(callData, callerPlayer, permissions);
    _.memory.Free(othersConsole);
    if (commandData.requiresTarget) {
        for (i = 0; i < targetPlayers.length; i += 1) {
            targetConsole = _.console.For(targetPlayers[i]);
            othersConsole = _.console
                .ForAll()
                .ButPlayer(callerPlayer)
                .ButPlayer(targetPlayers[i]);
            ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
            _.memory.Free(othersConsole);
            _.memory.Free(targetConsole);
        }
    }
    othersConsole = none;
    targetConsole = none;
    DeallocateConsoles();
    return true;
}

/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
public final static function DeallocateCallData(/* take */ CallData callData) {
    __().memory.Free(callData.subCommandName);
    __().memory.Free(callData.parameters);
    __().memory.Free(callData.options);
    __().memory.Free(callData.errorCause);
    __().memory.FreeMany(callData.targetPlayers);
    if (callData.targetPlayers.length > 0) {
        callData.targetPlayers.length = 0;
    }
}

private final function CleanParameters(array<Parameter> parameters) {
    local int i;

    for (i = 0; i < parameters.length; i += 1) {
        _.memory.Free(parameters[i].displayName);
        _.memory.Free(parameters[i].variableName);
        _.memory.Free(parameters[i].aliasSourceName);
    }
}

/// Returns name (in lower case) of the caller command class.
public final static function Text GetPreferredName() {
    return __().text.FromString(Locs(default.preferredName));
}

/// Returns name (in lower case) of the caller command class.
public final static function string GetPreferredName_S() {
    return Locs(default.preferredName);
}

/// Returns name (in lower case) of the caller command class.
public final function Text GetName() {
    if (usedName == none) {
        return P("").Copy();
    }
    return usedName.LowerCopy();
}

/// Returns name (in lower case) of the caller command class.
public final function string GetName_S() {
    if (usedName == none) {
        return "";
    }
    return _.text.IntoString(/*take*/ usedName.LowerCopy());
}

/// Returns group name (in lower case) of the caller command class.
public final function Text GetGroupName() {
    if (commandData.group == none) {
        return P("").Copy();
    }
    return commandData.group.LowerCopy();
}

/// Returns group name (in lower case) of the caller command class.
public final function string GetGroupName_S() {
    if (commandData.group == none) {
        return "";
    }
    return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
}

/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig(BaseText configName) {
    if (configName == none)                     return none;
    if (default.permissionsConfigClass == none) return none;

    // This creates default config if it is missing
    default.permissionsConfigClass.static.NewConfig(configName);
    return CommandPermissions(default.permissionsConfigClass.static
        .GetConfigInstance(configName));
}

/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig_S(string configName) {
    local MutableText wrapper;
    local CommandPermissions result;

    wrapper = __().text.FromStringM(configName);
    result = LoadConfig(wrapper);
    __().memory.Free(wrapper);
    return result;
}

/// Returns subcommands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
    local int i, j;
    local bool addSubCommand;
    local array<string> forbiddenCommands;
    local array<Text> result;

    forbiddenCommands = permissions.forbiddenSubCommands;
    if (permissions != none) {
        forbiddenCommands = permissions.forbiddenSubCommands;
    }
    for (i = 0; i < commandData.subCommands.length; i += 1) {
        addSubCommand = true;
        for (j = 0; j < forbiddenCommands.length; j += 1) {
            if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
                addSubCommand = false;
                break;
            }
        }
        if (addSubCommand) {
            result[result.length] = commandData.subCommands[i].name.LowerCopy();
        }
    }
    return result;
}

/// Returns sub commands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
    return _.text.IntoStrings(GetSubCommands(permissions));
}

/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed as either argument, returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing subcommand is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed(
    BaseText subCommand,
    CommandPermissions permissions
) {
    if (subCommand == none)     return true;
    if (permissions == none)    return true;

    return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
}

/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed for permissions, always returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing sub command is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed_S(
    string subCommand,
    CommandPermissions permissions
) {
    local int i;
    local array<string> forbiddenCommands;

    if (permissions == none) {
        return true;
    }
    forbiddenCommands = permissions.forbiddenSubCommands;
    for (i = 0; i < forbiddenCommands.length; i += 1) {
        if (subCommand ~= forbiddenCommands[i]) {
            return false;
        }
    }
    return true;
}

/// Returns `Command.Data` struct that describes caller `Command`.
///
/// Returned struct contains `Text` references that are used internally by
/// the `Command` and not their copies.
///
/// Generally this is undesired approach and leaves `Command` more vulnerable to
/// modification, but copying all the data inside would not only introduce
/// a largely pointless computational overhead, but also would require some
/// cumbersome logic.
/// This might change in the future, so deallocating any objects in the returned
/// `struct` would lead to undefined behavior.
public final function Data BorrowData() {
    return commandData;
}

private final function DeallocateConsoles() {
    if (publicConsole != none && publicConsole.IsAllocated()) {
        _.memory.Free(publicConsole);
    }
    if (callerConsole != none && callerConsole.IsAllocated()) {
        _.memory.Free(callerConsole);
    }
    if (targetConsole != none && targetConsole.IsAllocated()) {
        _.memory.Free(targetConsole);
    }
    if (othersConsole != none && othersConsole.IsAllocated()) {
        _.memory.Free(othersConsole);
    }
    publicConsole = none;
    callerConsole = none;
    targetConsole = none;
    othersConsole = none;
}

/// Reports given error to the `callerPlayer`, appropriately picking
/// message color
private final function ReportError(CallData callData, EPlayer callerPlayer) {
    local Text errorMessage;
    local ConsoleWriter console;

    if (callerPlayer == none)       return;
    if (!callerPlayer.IsExistent()) return;

    // Setup console color
    console = callerPlayer.BorrowConsole();
    if (callData.parsingError == CET_EmptyTargetList) {
        console.UseColor(_.color.textWarning);
    } else {
        console.UseColor(_.color.textFailure);
    }
    // Send message
    errorMessage = PrintErrorMessage(callData);
    console.Say(errorMessage);
    errorMessage.FreeSelf();
    // Restore console color
    console.ResetColor().Flush();
}

private final function Text PrintErrorMessage(CallData callData) {
    local Text result;
    local MutableText builder;

    builder = _.text.Empty();
    switch (callData.parsingError) {
        case CET_BadParser:
            builder.Append(P("Internal error occurred: invalid parser"));
            break;
        case CET_NoSubCommands:
            builder.Append(P("Ill defined command: no subcommands"));
            break;
        case CET_BadSubCommand:
            builder
                .Append(P("Ill defined sub-command: "))
                .Append(callData.errorCause);
            break;
        case CET_NoRequiredParam:
            builder
                .Append(P("Missing required parameter: "))
                .Append(callData.errorCause);
            break;
        case CET_NoRequiredParamForOption:
            builder
                .Append(P("Missing required parameter for option: "))
                .Append(callData.errorCause);
            break;
        case CET_UnknownOption:
            builder
                .Append(P("Invalid option specified: "))
                .Append(callData.errorCause);
            break;
        case CET_UnknownShortOption:
            builder.Append(P("Invalid short option specified"));
            break;
        case CET_RepeatedOption:
            builder
                .Append(P("Option specified several times: "))
                .Append(callData.errorCause);
            break;
        case CET_UnusedCommandParameters:
            builder.Append(P("Part of command could not be parsed: "))
                .Append(callData.errorCause);
            break;
        case CET_MultipleOptionsWithParams:
            builder
                .Append(P("Multiple short options in one declarations require parameters: "))
                .Append(callData.errorCause);
            break;
        case CET_IncorrectTargetList:
            builder
                .Append(P("Target players are incorrectly specified."))
                .Append(callData.errorCause);
            break;
        case CET_EmptyTargetList:
            builder
                .Append(P("List of target players is empty"))
                .Append(callData.errorCause);
            break;
        default:
    }
    result = builder.Copy();
    builder.FreeSelf();
    return result;
}

// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
// If parsing failed, guaranteed to return an empty array.
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) {
    local array<EPlayer>    targetPlayers;
    local PlayersParser     targetsParser;

    targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
    targetsParser.SetSelf(callerPlayer);
    targetsParser.ParseWith(parser);
    if (parser.Ok()) {
        targetPlayers = targetsParser.GetPlayers();
    }
    targetsParser.FreeSelf();
    return targetPlayers;
}

defaultproperties {
    preferredName = ""
    permissionsConfigClass = none
}