/**
 * Author: dkanus
 * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
 * License: GPL
 * Copyright 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 CommandAPI extends AcediaObject;

/// Possible statuses of command locks.
///
/// Command locks can prevent certain commands from being used.
/// A good example is having a "cheat" lock that commands can check to before
/// doing anything that can be considered cheating.
/// Their purpose is to track the "purity" of the game matches
/// (e.g. whether cheats were used).
enum LockStatus {
    /// Lock is currently closed, preventing commands from doing actions that
    /// might require it opened.
    /// Lock can still be opened.
    LS_Closed,
    /// Lock is currently opened, allowing commands to perform actions that
    /// might require it opened.
    /// Lock can still be closed.
    LS_Open,
    /// Lock is currently closed, preventing commands from doing actions that
    /// might require it opened.
    /// Lock can no longer be opened during the game session.
    LS_ClosedFixed,
    /// Lock is currently opened, allowing commands to perform actions that
    /// might require it opened.
    /// Lock can no longer be closed during the game session.
    /// For "cheat" lock, for example, it can happen if someone has already
    /// used cheats.
    LS_OpenFixed
};

/// Describes possible outcomes of starting a voting by its name
enum StartVotingResult {
    /// Voting was successfully started.
    SVR_Success,
    /// There is no people that are allowed to vote.
    SVR_NoVoters,
    /// Voting itself decided to reject being started, most likely because of
    /// the specified arguments.
    SVR_Rejected,
    /// Voting wasn't started because another one was still in progress.
    SVR_AlreadyInProgress,
    /// Voting wasn't started because voting with that name hasn't been
    /// registered.
    SVR_UnknownVoting,
    /// `CommandAPI` isn't functioning properly.
    SVR_InvalidState
};

/// Struct for storing the result of "resolving" [`Command`] access for
/// some user.
struct CommandConfigInfo {
    /// Instance of the command. If it equals to `none`, then [`configName`]
    /// will also equal `none`.
    var public Command instance;
    /// Config that determines permissions for the command [`instance`].
    /// Can equal to `none` if:
    ///
    /// * [`Command`] class didn't setup a custom permissions config.
    /// * Using provided instance is forbidden
    ///     (in this case `usageForbidden` will be set to `true`).
    var public CommandPermissions config;
    /// Set to `true` in case using provided command if forbidden.
    var public bool usageForbidden;
};

/// Struct for storing the result of "resolving" [`Voting`] access for
/// some user.
struct VotingConfigInfo {
    /// [`Voting`]'s class. If it equals to `none`, then [`configName`]
    /// will also equal `none`.
    var public class<Voting> votingClass;
    /// Config that determines permissions for the [`votingClass`].
    /// Can equal to `none` if using provided instance is forbidden
    /// (in this case `usageForbidden` will be set to `true`).
    var public VotingPermissions config;
    /// Set to `true` in case using provided voting if forbidden.
    var public bool usageForbidden;
};

/// Internal enum that describes all types of scheduled jobs that this API can
/// produce.
enum AsyncJobType {
    CAJT_AddCommand,
    CAJT_AuthorizeCommand,
    CAJT_AddVoting,
    CAJT_AuthorizeVoting
};

/// Internal struct that can describe data for any scheduled job that this API
/// can produce.
struct AsyncTask {
    var public AsyncJobType type;
    // For all jobs
    var public Text entityName;
    // For `CAJT_Add...`
    var public class<AcediaObject> entityClass;
    // For `CAJT_Authorize`
    var public Text userGroup;
    // For `CAJT_Authorize...`
    var public Text configName;
};

/// Internal struct for storing all the utility objects that `Commands_Feature`
/// uses.
struct CommandFeatureTools {
    var public CommandsTool commands;
    var public VotingsTool votings;
};

/// Internal struct for representing the result of the resolve command by either
/// [`CommandsTool`] or [`VotingsTool`].
///
/// Defined in `CommandAPI` instead of their base [`CmdItemsTool`] to avoid
/// compiler issues with resolving `dependson`-graph.
struct ItemConfigInfo {
    var public AcediaObject instance;
    var public class<AcediaObject> class;
    // Name of config that determines permissions for the item [`instance`]
    var public Text configName;
};

/// Classes registered to be added in async way
var private array<AsyncTask> pendingAsyncJobs;
/// Job that is supposed to register pending commands, will be asking for new
/// jobs from [`pendingAsyncJobs`] once it completes its current task.
var private CommandRegistrationJob registeringJob;

/// Saves `HashTable` with command locks.
/// Locks are simply boolean switches that mark for commands whether they
/// can be executed.
///
/// Lock is considered "unlocked" if this `HashTable` stores `true` at the key
/// with its name and `false` otherwise.
var private HashTable commandLocks;

var private CommandsTool commandsTool;
var private VotingsTool votingsTool;
var private int commandsToolLifeVersion;
var private int votingsToolLifeVersion;

var protected CommandsAPI_OnCommandAdded_Signal onCommandAddedSignal;
var protected CommandsAPI_OnCommandRemoved_Signal onCommandRemovedSignal;
var protected CommandsAPI_OnVotingAdded_Signal onVotingAddedSignal;
var protected CommandsAPI_OnVotingRemoved_Signal onVotingRemovedSignal;

protected function Constructor() {
    onCommandAddedSignal = CommandsAPI_OnCommandAdded_Signal(
        _.memory.Allocate(class'CommandsAPI_OnCommandAdded_Signal'));
    onCommandRemovedSignal = CommandsAPI_OnCommandRemoved_Signal(
        _.memory.Allocate(class'CommandsAPI_OnCommandRemoved_Signal'));
    onVotingAddedSignal = CommandsAPI_OnVotingAdded_Signal(
        _.memory.Allocate(class'CommandsAPI_OnVotingAdded_Signal'));
    onVotingRemovedSignal = CommandsAPI_OnVotingRemoved_Signal(
        _.memory.Allocate(class'CommandsAPI_OnVotingRemoved_Signal'));
}

protected function Finalizer() {
    _.memory.Free(onCommandAddedSignal);
    _.memory.Free(onCommandRemovedSignal);
    _.memory.Free(onVotingAddedSignal);
    _.memory.Free(onVotingRemovedSignal);
    onCommandAddedSignal = none;
    onCommandRemovedSignal = none;
    onVotingAddedSignal = none;
    onVotingRemovedSignal = none;
}

/// Signal that will be emitted when a new [`Command`] class is successfully
/// added through this API.
///
/// # Slot description
///
/// bool <slot>(class<Command> addedClass, Text usedName)
///
/// ## Parameters
///
/// * [`addedClass`]: Class of the command that got added.
/// * [`usedName`]: Name, under which command class was added.
public /*signal*/ function CommandsAPI_OnCommandAdded_Slot OnCommandAdded(AcediaObject receiver) {
    return CommandsAPI_OnCommandAdded_Slot(onCommandAddedSignal.NewSlot(receiver));
}

/// Signal that will be emitted when a [`Command`] class is removed through
/// this API.
///
/// # Slot description
///
/// bool <slot>(class<Command> removedClass)
///
/// ## Parameters
///
/// * [`removedClass`]: Class of the command that got removed.
public /*signal*/ function CommandsAPI_OnCommandRemoved_Slot OnCommandRemoved(
    AcediaObject receiver
) {
    return CommandsAPI_OnCommandRemoved_Slot(onCommandRemovedSignal.NewSlot(receiver));
}

/// Signal that will be emitted when a new [`Voting`] class is successfully
/// added through this API.
///
/// # Slot description
///
/// bool <slot>(class<Voting> addedClass, Text usedName)
///
/// ## Parameters
///
/// * [`addedClass`]: Class of the voting that got added.
/// * [`usedName`]: Name, under which voting class was added.
public /*signal*/ function CommandsAPI_OnVotingAdded_Slot OnVotingAdded(AcediaObject receiver) {
    return CommandsAPI_OnVotingAdded_Slot(onVotingAddedSignal.NewSlot(receiver));
}

/// Signal that will be emitted when a [`Voting`] class is removed through
/// this API.
///
/// # Slot description
///
/// bool <slot>(class<Voting> removedClass)
///
/// ## Parameters
///
/// * [`removedClass`]: Class of the voting that got removed.
public /*signal*/ function CommandsAPI_OnVotingRemoved_Slot OnVotingRemoved(AcediaObject receiver) {
    return CommandsAPI_OnVotingRemoved_Slot(onVotingRemovedSignal.NewSlot(receiver));
}

/// Checks if `Commands_Feature` is enabled, which is required for this API
/// to be functional.
public final function bool AreCommandsEnabled() {
    local Commands_Feature feature;

    feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
    _.memory.Free(feature);
    // We can still compare to `none` even after deallocation to see if
    // `GetEnabledInstance()` returned a valid instance.
    return (feature != none);
}

/// Returns array of names of all available commands.
///
/// Resulting array cannot contain duplicates.
public final function array<Text> GetAllCommandNames() {
    local array<Text> emptyResult;

    if (VerifyCommandsTool()) {
        return commandsTool.GetItemsNames();
    }
    return emptyResult;
}

/// Registers given command class, making it available via [`Execute()`].
///
/// Optionally a [`BaseText`] can be specified to be used as this command's name
/// (the main name, that command aliases are to be resolved into).
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a command's preferred name will be used.
///
/// If command name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// # Errors
///
/// Returns `true` if command was successfully registered and `false`
/// otherwise`.
///
/// If [`commandClass`] provides command with a name that is already taken
/// (comparison is case-insensitive) by a different command - a warning will be
/// logged and command class won't be registered.
///
/// If `commandClass` provides command with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and command class
/// won't be registered.
public final function bool AddCommand(
    class<Command> commandClass,
    optional BaseText commandName
) {
    local bool result;
    local Text immutableCommandName;

    if (commandClass == none) {
        return false;
    }
    if (VerifyCommandsTool()) {
        if (commandName != none) {
            immutableCommandName = commandName.Copy();
        } else {
            immutableCommandName = commandClass.static.GetPreferredName();
        }
        result = commandsTool.AddItemClass(commandClass, immutableCommandName);
        if (result) {
            onCommandAddedSignal.Emit(commandClass, immutableCommandName);
        }
        _.memory.Free(immutableCommandName);
    }
    return false;
}

/// Registers given command class, making it available via [`Execute()`].
///
/// Optionally a [`BaseText`] can be specified to be used as this command's name
/// (the main name, that command aliases are to be resolved into).
/// If name parameter is omitted (specified as empty [`string`]), a command's
/// default name (defined via its [`CommandDataBuilder`]) will be used.
///
/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent
/// the [`Command`] from being authorized.
///
/// # Errors
///
/// Returns `true` if command was successfully registered and
/// `false` otherwise`.
///
/// If `commandClass` provides command with a name that is already taken
/// (comparison is case-insensitive) by a different command - a warning will be
/// logged and command class won't be registered.
///
/// If `commandClass` provides command with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and command class
/// won't be registered.
public final function bool AddCommand_S(
    class<Command> commandClass,
    optional string commandName
) {
    local bool result;
    local MutableText wrapper;

    if (commandName != "") {
        wrapper = _.text.FromStringM(commandName);
    }
    result = AddCommand(commandClass, wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Registers given command class asynchronously, making it available
/// via [`Execute()`].
///
/// Doesn't register commands immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// Optionally a [`BaseText`] can be specified to be used as this command's name
/// (the main name, that command aliases are to be resolved into).
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a command's default name (defined via its
/// [`CommandDataBuilder`]) will be used.
///
/// If command name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// # Errors
///
/// Returns `true` if command was successfully registered and
/// `false` otherwise`.
///
/// If [`commandClass`] provides command with a name that is already taken
/// (comparison is case-insensitive) by a different command - a warning will be
/// logged and command class won't be registered.
///
/// If [`commandClass`] provides command with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and command class
/// won't be registered.
///
/// Warnings might be logged *after* the call itself, when the command class is
/// actually added.
public final function AddCommandAsync(
    class<Command> commandClass,
    optional BaseText commandName
) {
    local AsyncTask newJob;

    if (!VerifyCommandsTool()) {
        return;
    }
    newJob.type = CAJT_AddCommand;
    newJob.entityClass = commandClass;
    if (commandName != none) {
        newJob.entityName = commandName.Copy();
    }
    pendingAsyncJobs[pendingAsyncJobs.length] = newJob;
    if (registeringJob == none || registeringJob.IsCompleted()) {
        _.memory.Free(registeringJob);
        registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob'));
        _.scheduler.AddJob(registeringJob);
    }
}

/// Registers given command class asynchronously, making it available
/// via [`Execute()`].
///
/// Doesn't register commands immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// Optionally a [`BaseText`] can be specified to be used as this command's name
/// (the main name, that command aliases are to be resolved into).
/// If name parameter is omitted (specified as empty [`string`]), a command's
/// default name (defined via its [`CommandDataBuilder`]) will be used.
///
/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent
/// the [`Command`] from being authorized.
///
/// # Errors
///
/// Returns `true` if command was successfully registered and
/// `false` otherwise`.
///
/// If [`commandClass`] provides command with a name that is already taken
/// (comparison is case-insensitive) by a different command - a warning will be
/// logged and command class won't be registered.
///
/// If [`commandClass`] provides command with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and command class
/// won't be registered.
///
/// Warnings might be logged *after* the call itself, when the command class is
/// actually added.
public final function AddCommandAsync_S(
    class<Command> commandClass,
    optional string commandName
) {
    local MutableText wrapper;

    if (commandName != "") {
        wrapper = _.text.FromStringM(commandName);
    }
     AddCommandAsync(commandClass, wrapper);
    _.memory.Free(wrapper);
}

/// Authorizes new user group to use the specified command, optionally
/// specifying name of the config (config's class is determined by the
/// [`Command`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Command`] with a given name is added.
///
/// If config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no command with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a command, then it
/// will log a warning message about it.
public function bool AuthorizeCommandUsage(
    BaseText commandName,
    BaseText groupName,
    optional BaseText configName
) {
    if (VerifyCommandsTool()) {
        return commandsTool.AuthorizeUsage(commandName, groupName, configName);
    }
    return false;
}

/// Authorizes new user group to use the specified command, optionally
/// specifying name of the config (config's class is determined by the
/// [`Command`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Command`] with a given name is added.
///
/// If this config name is specified as empty, then "default" will be
/// used instead. For non-empty values, an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// authorized.
///
/// # Errors
///
/// If specified group was already authorized to use a command, then it
/// will log a warning message about it.
public function bool AuthorizeCommandUsage_S(
    string commandName,
    string userGroupName,
    optional string configName
) {
    local bool result;
    local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName;

    wrapperVotingName = _.text.FromStringM(commandName);
    wrapperGroupName = _.text.FromStringM(userGroupName);
    wrapperConfigName = _.text.FromStringM(configName);
    result = AuthorizeCommandUsage(wrapperVotingName, wrapperGroupName, wrapperConfigName);
    _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName);
    return result;
}

/// Authorizes new user group to use the specified command, optionally
/// specifying name of the config (config's class is determined by the
/// [`Command`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Command`] with a given name is added.
///
/// Doesn't authorize commands immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// If config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no command with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a command, then it
/// will log a warning message about it.
public function AuthorizeCommandUsageAsync(
    BaseText commandName,
    BaseText userGroupName,
    optional BaseText configName
) {
    local AsyncTask newTask;

    if (!VerifyCommandsTool()) {
        return;
    }
    newTask.type = CAJT_AuthorizeCommand;
    if (commandName != none) {
        newTask.entityName = commandName.Copy();
    }
    if (userGroupName != none) {
        newTask.userGroup = userGroupName.Copy();
    }
    if (configName != none) {
        newTask.configName = configName.Copy();
    }
    pendingAsyncJobs[pendingAsyncJobs.length] = newTask;
    if (registeringJob == none || registeringJob.IsCompleted()) {
        _.memory.Free(registeringJob);
        registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob'));
        _.scheduler.AddJob(registeringJob);
    }
}

/// Authorizes new user group to use the specified command, optionally
/// specifying name of the config (config's class is determined by the
/// [`Command`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Command`] with a given name is added.
///
/// Doesn't authorize commands immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// If this config name is specified as empty, then "default" will be
/// used instead. For non-empty values, an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no command with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a command, then it
/// will log a warning message about it.
public function AuthorizeCommandUsageAsync_S(
    string commandName,
    string userGroupName,
    optional string configName
) {
    local MutableText wrapperCommandName, wrapperGroupName, wrapperConfigName;

    wrapperCommandName = _.text.FromStringM(commandName);
    wrapperGroupName = _.text.FromStringM(userGroupName);
    wrapperConfigName = _.text.FromStringM(configName);
    AuthorizeCommandUsageAsync(wrapperCommandName, wrapperGroupName, wrapperConfigName);
    _.memory.Free3(wrapperCommandName, wrapperGroupName, wrapperConfigName);
}

/// Removes command of given class from the list of registered commands.
///
/// Removing once registered commands is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of commands).
///
/// Returns `true` if successfully removed registered [`Command`] class and
/// `false` otherwise (command wasn't registered).
public final function bool RemoveCommand(class<Command> commandClass) {
    local bool result;

    if (VerifyCommandsTool()) {
        result = commandsTool.RemoveItemClass(commandClass);
        if (result) {
            onCommandRemovedSignal.Emit(commandClass);
        }
    }
    return result;
}

/// Returns instance of the [`Command`] that was added under a specified name.
public final function Command GetCommand(BaseText commandName) {
    local ItemConfigInfo intermediaryResult;

    if (VerifyCommandsTool()) {
        // `none` means we'll get instance + class, but without config,
        // so nothing to deallocate in `intermediaryResult`
        intermediaryResult = commandsTool.ResolveItem(commandName, none);
        return Command(intermediaryResult.instance);
    }
    return none;
}

/// Returns instance of the [`Command`] that was added under a specified name.
public final function Command GetCommand_S(string commandName) {
    local Command result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(commandName);
    result = GetCommand(wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and text ID of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found.
public final function CommandConfigInfo ResolveCommandForTextID(BaseText itemName, BaseText id) {
    local ItemConfigInfo intermediaryResult;
    local CommandConfigInfo result;

    if (VerifyCommandsTool()) {
        intermediaryResult = commandsTool.ResolveItem(itemName, id);
        result.instance = Command(intermediaryResult.instance);
        if (result.instance == none) {
            return result;
        }
        if (intermediaryResult.configName == none) {
            result.usageForbidden = true;
        } else {
            result.config = result.instance.LoadConfig(intermediaryResult.configName);
            _.memory.Free(intermediaryResult.configName);
        }
    }
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and text ID of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found.
public final function CommandConfigInfo ResolveCommandForTextID_S(string itemName, string id) {
    local CommandConfigInfo result;
    local MutableText wrapperItemname, wrapperID;

    wrapperItemname = _.text.FromStringM(itemName);
    wrapperID = _.text.FromStringM(id);
    result = ResolveCommandForTextID(wrapperItemname, wrapperID);
    _.memory.Free2(wrapperItemname, wrapperID);
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and [`UserID`] of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found or provided id was `none`.
public final function CommandConfigInfo ResolveCommandForUserID(BaseText itemName, UserID id) {
    local CommandConfigInfo result;
    local Text textID;

    if (itemName == none)   return result;
    if (id == none)         return result;
    textID = id.GetUniqueID();
    if (textID == none)    return result;

    result = ResolveCommandForTextID(itemName, textID);
    textID.FreeSelf();
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and [`UserID`] of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found or provided id was `none`.
public final function CommandConfigInfo ResolveCommandForUserID_S(string itemName, UserID id) {
    local CommandConfigInfo result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(itemName);
    result = ResolveCommandForUserID(wrapper, id);
    _.memory.Free(wrapper);
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and [`User`] of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found or provided id was `none`.
public final function CommandConfigInfo ResolveCommandForUser(BaseText itemName, User user) {
    local CommandConfigInfo result;
    local UserID id;

    if (itemName == none)   return result;
    if (user == none)       return result;

    id = user.GetID();
    result = ResolveCommandForUserID(itemName, id);
    _.memory.Free(id);
    return result;
}

/// Returns pair of [`Command`] and config name based on a given
/// case-insensitive name and [`User`] of the caller player.
///
/// Function only returns `none` for [`Command`] instance if [`Command`] with
/// a given name wasn't found or provided id was `none`.
public final function CommandConfigInfo ResolveCommandForUser_S(string itemName, User user) {
    local CommandConfigInfo result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(itemName);
    result = ResolveCommandForUser(wrapper, user);
    _.memory.Free(wrapper);
    return result;
}

/// Returns all available command groups' names.
public final function array<Text> GetGroupsNames() {
    local array<Text> emptyResult;

    if (VerifyCommandsTool()) {
        return commandsTool.GetGroupsNames();
    }
    return emptyResult;
}

/// Returns all available command groups' names.
public final function array<string> GetGroupsNames_S() {
    local array<string> emptyResult;

    if (VerifyCommandsTool()) {
        return _.text.IntoStrings(commandsTool.GetGroupsNames());
    }
    return emptyResult;
}

/// Returns array of names of all available commands belonging to the specified
/// command group.
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
     local array<Text> emptyResult;

    if (VerifyCommandsTool()) {
        return commandsTool.GetCommandNamesInGroup(groupName);
    }
    return emptyResult;
}

/// Returns array of names of all available commands belonging to the specifie
/// command group.
public final function array<string> GetCommandNamesInGroup_S(string groupName) {
     local array<string> result;
     local MutableText wrapper;

    if (VerifyCommandsTool()) {
        wrapper = _.text.FromStringM(groupName);
        result = _.text.IntoStrings(commandsTool.GetCommandNamesInGroup(wrapper));
        _.memory.Free(wrapper);
    }
    return result;
}

/// Executes command based on the textual input with a given instigator.
///
/// Input should be provided in a form that players are expected to use.
/// For example, "mutate inventory @all" or "say !inventory @all" will both
/// translate into calling this method with "inventory @all" argument.
///
/// Command's instigator will receive appropriate result/error messages.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`instigator`].
public final function Execute(BaseText commandLine, EPlayer instigator) {
    local Commands_Feature feature;

    feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
    if (feature != none) {
        feature.HandleInput(commandLine, instigator);
    }
    _.memory.Free(feature);
}

/// Executes command based on the textual input with a given instigator.
///
/// Input should be provided in a form that players are expected to use.
/// For example, "mutate inventory @all" or "say !inventory @all" will both
/// translate into calling this method with "inventory @all" argument.
///
/// Command's instigator will receive appropriate result/error messages.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`instigator`].
public final function Execute_S(string commandLine, EPlayer instigator) {
    local MutableText wrapper;
    local Commands_Feature feature;

    feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
    if (feature != none) {
        wrapper = _.text.FromStringM(commandLine);
        feature.HandleInput(wrapper, instigator);
        _.memory.Free(wrapper);
    }
    _.memory.Free(feature);
}

/// Returns current status of a lock with a given name.
public final function LockStatus GetLockStatus(BaseText lockName) {
    local LockStatus result;
    local Text lowerCaseName;

    if (lockName == none) {
        return LS_ClosedFixed;
    }
    if (commandLocks == none) {
        commandLocks = _.collections.EmptyHashTable();
    }
    lowerCaseName = lockName.LowerCopy();
    result = LockStatus(commandLocks.GetInt(lowerCaseName));
    lowerCaseName.FreeSelf();
    return result;
}

/// Returns current status of a lock with a given name.
public final function LockStatus GetLockStatus_S(string lockName) {
    local LockStatus result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(lockName);
    result = GetLockStatus(wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Sets new status for a lock with a given name.
///
/// Can fail and return `false` in case lock was already fixed in the opposite
/// state. Otherwise returns `true`.
public final function bool SetLockStatus(BaseText lockName, LockStatus newStatus) {
    local LockStatus previousStatus;
    local Text lowerCaseName;

    if (lockName == none) {
        return false;
    }
    if (commandLocks == none) {
        commandLocks = _.collections.EmptyHashTable();
    }
    lowerCaseName = lockName.LowerCopy();
    previousStatus = LockStatus(commandLocks.GetInt(lowerCaseName));
    if (previousStatus != LS_OpenFixed) {
        commandLocks.SetInt(lowerCaseName, int(newStatus));
        lowerCaseName.FreeSelf();
        return true;
    }
    lowerCaseName.FreeSelf();
    return false;
}

/// Sets new status for a lock with a given name.
///
/// Can fail and return `false` in case lock was already fixed in the opposite
/// state. Otherwise returns `true`.
public final function bool SetLockStatus_S(string lockName, LockStatus newStatus) {
    local bool result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(lockName);
    result = SetLockStatus(wrapper, newStatus);
    _.memory.Free(wrapper);
    return result;
}

/// Closes a command lock with a given case-insensitive name, preventing it from
/// being opened again.
///
/// Can fail and return `false` in case lock was already fixed in the opened
/// state. Otherwise returns `true`.
public final function bool Lock(BaseText lockName) {
    return SetLockStatus(lockName, LS_ClosedFixed);
}

/// Closes a command lock with a given case-insensitive name, preventing it from
/// being opened again.
///
/// Can fail and return `false` in case lock was already fixed in the opened
/// state. Otherwise returns `true`.
public final function bool Lock_S(string lockName) {
    return SetLockStatus_S(lockName, LS_ClosedFixed);
}

/// Opens a command lock with a given case-insensitive name.
///
/// Lock can still be closed after successful execution of this command.
///
/// Can fail and return `false` in case lock was already fixed in the closed
/// state. Otherwise returns `true`.
public final function bool Unlock(BaseText lockName) {
    return SetLockStatus(lockName, LS_Open);
}

/// Opens a command lock with a given case-insensitive name.
///
/// Lock can still be closed after successful execution of this command.
///
/// Can fail and return `false` in case lock was already fixed in the closed
/// state. Otherwise returns `true`.
public final function bool Unlock_S(string lockName) {
    return SetLockStatus_S(lockName, LS_Open);
}

/// Opens a command lock with a given case-insensitive name, preventing it from
/// being closed again.
///
/// Can fail and return `false` in case lock was already fixed in the closed
/// state. Otherwise returns `true`.
public final function bool BreakOpenLock(BaseText lockName) {
    return SetLockStatus(lockName, LS_OpenFixed);
}

/// Opens a command lock with a given case-insensitive name, preventing it from
/// being closed again.
///
/// Can fail and return `false` in case lock was already fixed in the closed
/// state. Otherwise returns `true`.
public final function bool BreakOpenLock_S(string lockName) {
    return SetLockStatus_S(lockName, LS_OpenFixed);
}

/// Checks if a command lock with a given case-insensitive name is closed.
///
/// If lock is closed, method returns `true` (regardless of whether or not it is
/// fixed in closed state) and `false` if its currently open.
public final function bool IsLocked(BaseText lockName) {
    local LockStatus currentStatus;
    currentStatus = GetLockStatus(lockName);
    return (currentStatus == LS_ClosedFixed || currentStatus == LS_Closed);
}

/// Checks if a command lock with a given case-insensitive name is closed.
///
/// If lock is closed, method returns `true` (regardless of whether or not it is
/// fixed in closed state) and `false` if its currently open.
public final function bool IsLocked_S(string lockName) {
    local LockStatus currentStatus;
    currentStatus = GetLockStatus_S(lockName);
    return (currentStatus == LS_ClosedFixed || currentStatus == LS_Closed);
}

/// Returns array of names of all available commands.
///
/// Resulting array can contain duplicates, but only if the same voting class
/// was registered by several different names.
public final function array< class<Voting> > GetAllVotingClasses() {
    local int i;
    local array< class<AcediaObject> > intermediaryResult;
    local array< class<Voting> > result;

    if (VerifyVotingsTool()) {
        intermediaryResult = votingsTool.GetAllItemClasses();
        for (i = 0; i < intermediaryResult.length; i += 1) {
            result[result.length] = class<Voting>(intermediaryResult[i]);
        }
    }
    return result;
}

/// Returns array of names of all available votings.
///
/// Resulting array cannot contain duplicates.
public final function array<Text> GetAllVotingsNames() {
    local array<Text> emptyResult;

    if (VerifyVotingsTool()) {
        return votingsTool.GetItemsNames();
    }
    return emptyResult;
}

/// Registers given voting class, making it available via [`StartVoting()`].
///
/// Optionally a [`BaseText`] can be specified to be used as this voting's name.
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a voting's preferred name will be used.
///
/// If voting name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// # Errors
///
/// Returns `true` if voting was successfully registered and `false`
/// otherwise`.
///
/// If [`votingClass`] provides voting with a name that is already taken
/// (comparison is case-insensitive) by a different voting - a warning will be
/// logged and voting class won't be registered.
///
/// If votingClass` provides voting with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and voting class
/// won't be registered.
public final function bool AddVoting(
    class<Voting> votingClass,
    optional BaseText votingName
) {
    local bool result;
    local Text immutableVotingName;

    if (votingClass == none) {
        return false;
    }
    if (VerifyVotingsTool()) {
        if (votingName != none) {
            immutableVotingName = votingName.Copy();
        } else {
            immutableVotingName = votingClass.static.GetPreferredName();
        }
        result = votingsTool.AddItemClass(votingClass, immutableVotingName);
        if (result) {
            onVotingAddedSignal.Emit(votingClass, immutableVotingName);
        }
        _.memory.Free(immutableVotingName);
    }
    return false;
}

/// Registers given voting class, making it available via [`StartVoting()`].
///
/// Optionally a [`BaseText`] can be specified to be used as this voting's name.
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a voting's preferred name will be used.
///
/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent
/// the [`Voting`] from being authorized.
///
/// # Errors
///
/// Returns `true` if voting was successfully registered and `false`
/// otherwise`.
///
/// If [`votingClass`] provides voting with a name that is already taken
/// (comparison is case-insensitive) by a different voting - a warning will be
/// logged and voting class won't be registered.
///
/// If votingClass` provides voting with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and voting class
/// won't be registered.
public final function bool AddVoting_S(
    class<Voting> votingClass,
    optional string votingName
) {
    local bool result;
    local MutableText wrapper;

    if (votingName != "") {
        wrapper = _.text.FromStringM(votingName);
    }
    result = AddVoting(votingClass, wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Registers given voting class, making it available via [`StartVoting()`].
///
/// Doesn't register voting immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection.
///
/// Optionally a [`BaseText`] can be specified to be used as this voting's name.
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a voting's preferred name will be used.
///
/// If voting name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// # Errors
///
/// Returns `true` if voting was successfully registered and `false`
/// otherwise`.
///
/// If [`votingClass`] provides voting with a name that is already taken
/// (comparison is case-insensitive) by a different voting - a warning will be
/// logged and voting class won't be registered.
///
/// If votingClass` provides voting with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and voting class
/// won't be registered.
public final function AddVotingAsync(
    class<Voting> votingClass,
    optional BaseText votingName
) {
    local AsyncTask newTask;

    if (!VerifyVotingsTool()) {
        return;
    }
    newTask.type = CAJT_AddVoting;
    newTask.entityClass = votingClass;
    if (votingName != none) {
        newTask.entityName = votingName.Copy();
    }
    pendingAsyncJobs[pendingAsyncJobs.length] = newTask;
    if (registeringJob == none || registeringJob.IsCompleted()) {
        _.memory.Free(registeringJob);
        registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob'));
        _.scheduler.AddJob(registeringJob);
    }
}

/// Registers given voting class, making it available via [`StartVoting()`].
///
/// Doesn't register votings immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// Optionally a [`BaseText`] can be specified to be used as this voting's name.
/// If name parameter is omitted (specified as `none`) or constitutes an empty
/// [`BaseText`], a voting's preferred name will be used.
///
/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent
/// the [`Voting`] from being authorized.
///
/// # Errors
///
/// Returns `true` if voting was successfully registered and `false`
/// otherwise`.
///
/// If [`votingClass`] provides voting with a name that is already taken
/// (comparison is case-insensitive) by a different voting - a warning will be
/// logged and voting class won't be registered.
///
/// If votingClass` provides voting with an empty name (and it isn't
/// overridden by the argument) - a warning will be logged and voting class
/// won't be registered.
public final function AddVotingAsync_S(
    class<Voting> votingClass,
    optional string votingName
) {
    local MutableText wrapper;

    if (votingName != "") {
        wrapper = _.text.FromStringM(votingName);
    }
     AddVotingAsync(votingClass, wrapper);
    _.memory.Free(wrapper);
}

/// Authorizes new user group to use the specified voting, optionally
/// specifying name of the config (config's class is determined by the
/// [`Voting`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Voting`] with a given name is added.
///
/// If config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no voting with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a voting, then it
/// will log a warning message about it.
public function bool AuthorizeVotingUsage(
    BaseText votingName,
    BaseText groupName,
    optional BaseText configName
) {
    if (VerifyVotingsTool()) {
        return votingsTool.AuthorizeUsage(votingName, groupName, configName);
    }
    return false;
}

/// Authorizes new user group to use the specified voting, optionally
/// specifying name of the config (config's class is determined by the
/// [`Voting`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Voting`] with a given name is added.
///
/// If this config name is specified as empty, then "default" will be
/// used instead. For non-empty values, an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no voting with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a voting, then it
/// will log a warning message about it.
public function bool AuthorizeVotingUsage_S(
    string votingName,
    string userGroupName,
    optional string configName
) {
    local bool result;
    local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName;

    wrapperVotingName = _.text.FromStringM(votingName);
    wrapperGroupName = _.text.FromStringM(userGroupName);
    wrapperConfigName = _.text.FromStringM(configName);
    result = AuthorizeVotingUsage(wrapperVotingName, wrapperGroupName, wrapperConfigName);
    _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName);
    return result;
}

/// Authorizes new user group to use the specified voting, optionally
/// specifying name of the config (config's class is determined by the
/// [`Voting`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Voting`] with a given name is added.
///
/// Doesn't authorize votings immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// If config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, invalid name (according to
/// [`BaseText::IsValidName()`] method) will also prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no voting with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a voting, then it
/// will log a warning message about it.
public function AuthorizeVotingUsageAsync(
    BaseText votingName,
    BaseText userGroupName,
    optional BaseText configName
) {
    local AsyncTask newTask;

    if (!VerifyVotingsTool()) {
        return;
    }
    newTask.type = CAJT_AuthorizeVoting;
    if (votingName != none) {
        newTask.entityName = votingName.Copy();
    }
    if (userGroupName != none) {
        newTask.userGroup = userGroupName.Copy();
    }
    if (configName != none) {
        newTask.configName = configName.Copy();
    }
    pendingAsyncJobs[pendingAsyncJobs.length] = newTask;
    if (registeringJob == none || registeringJob.IsCompleted()) {
        _.memory.Free(registeringJob);
        registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob'));
        _.scheduler.AddJob(registeringJob);
    }
}

/// Authorizes new user group to use the specified voting, optionally
/// specifying name of the config (config's class is determined by the
/// [`Voting`]'s class) that describes permissions of that group.
///
/// Method must be called after [`Voting`] with a given name is added.
///
/// Doesn't authorize votings immediately, instead scheduling it to be done at
/// a later moment in time. This can help to reduce amount of work we do every
/// tick during server startup, therefore avoiding crashed due to the faulty
/// infinite loop detection. Different async calls from [`CommandAPI`] are
/// guaranteed to be handled in the order they were called.
///
/// If this config name is specified as empty, then "default" will be
/// used instead. For non-empty values, an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// authorized.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or no voting with
/// specified name was added in the caller tool).
///
/// # Errors
///
/// If specified group was already authorized to use a voting, then it
/// will log a warning message about it.
public function AuthorizeVotingUsageAsync_S(
    string votingName,
    string userGroupName,
    optional string configName
) {
    local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName;

    wrapperVotingName = _.text.FromStringM(votingName);
    wrapperGroupName = _.text.FromStringM(userGroupName);
    wrapperConfigName = _.text.FromStringM(configName);
    AuthorizeVotingUsageAsync(wrapperVotingName, wrapperGroupName, wrapperConfigName);
    _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName);
}

/// Removes voting of given class from the list of registered votings.
///
/// Removing once registered votings is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of votings).
///
/// Returns `true` if successfully removed registered [`Voting`] class and
/// `false` otherwise (voting wasn't registered).
public final function bool RemoveVoting(class<Voting> votingClass) {
    local bool result;

    if (VerifyVotingsTool()) {
        result = votingsTool.RemoveItemClass(votingClass);
        if (result) {
            onVotingRemovedSignal.Emit(votingClass);
        }
    }
    return result;
}

/// Returns class of the [`Voting`] that was added under a specified name.
public final function class<Voting> GetVotingClass(BaseText itemName) {
    local ItemConfigInfo intermediaryResult;

    if (VerifyVotingsTool()) {
        intermediaryResult = votingsTool.ResolveItem(itemName, none);
        return class<Voting>(intermediaryResult.class);
    }
    return none;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and text ID of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found.
public final function VotingConfigInfo ResolveVotingForTextID(BaseText itemName, BaseText id) {
    local ItemConfigInfo intermediaryResult;
    local VotingConfigInfo result;

    if (VerifyVotingsTool()) {
        intermediaryResult = votingsTool.ResolveItem(itemName, id);
        result.votingClass = class<Voting>(intermediaryResult.class);
        if (result.votingClass == none) {
            return result;
        }
        if (intermediaryResult.configName == none) {
            result.usageForbidden = true;
        } else {
            result.config = result.votingClass.static.LoadConfig(intermediaryResult.configName);
            _.memory.Free(intermediaryResult.configName);
        }
    }
    return result;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and text ID of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found.
public final function VotingConfigInfo ResolveVotingForTextID_S(string itemName, string id) {
    local VotingConfigInfo result;
    local MutableText wrapperItemname, wrapperID;

    wrapperItemname = _.text.FromStringM(itemName);
    wrapperID = _.text.FromStringM(id);
    result = ResolveVotingForTextID(wrapperItemname, wrapperID);
    _.memory.Free2(wrapperItemname, wrapperID);
    return result;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and [`UserID`] of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found or provided id was `none`.
public final function VotingConfigInfo ResolveVotingForUserID(BaseText itemName, UserID id) {
    local VotingConfigInfo result;
    local Text textID;

    if (itemName == none)   return result;
    if (id == none)         return result;
    textID = id.GetUniqueID();
    if (textID == none)    return result;

    result = ResolveVotingForTextID(itemName, textID);
    textID.FreeSelf();
    return result;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and [`UserID`] of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found or provided id was `none`.
public final function VotingConfigInfo ResolveVotingForUserID_S(string itemName, UserID id) {
    local VotingConfigInfo result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(itemName);
    result = ResolveVotingForUserID(wrapper, id);
    _.memory.Free(wrapper);
    return result;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and [`User`] of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found or provided id was `none`.
public final function VotingConfigInfo ResolveVotingForUser(BaseText itemName, User user) {
    local VotingConfigInfo result;
    local UserID id;

    if (itemName == none)   return result;
    if (user == none)       return result;

    id = user.GetID();
    result = ResolveVotingForUserID(itemName, id);
    _.memory.Free(id);
    return result;
}

/// Returns pair of [`Voting`] and config name based on a given
/// case-insensitive name and [`User`] of the caller player.
///
/// Function only returns `none` for [`Voting`] instance if [`Voting`] with
/// a given name wasn't found or provided id was `none`.
public final function VotingConfigInfo ResolveVotingForUser_S(string itemName, User user) {
    local VotingConfigInfo result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(itemName);
    result = ResolveVotingForUser(wrapper, user);
    _.memory.Free(wrapper);
    return result;
}

/// Starts a voting process with a given name and arguments.
public final function StartVotingResult StartVoting(
    VotingConfigInfo votingData,
    optional HashTable arguments) {
    if (VerifyVotingsTool()) {
        return votingsTool.StartVoting(votingData, arguments);
    }
    return SVR_InvalidState;
}

/// Returns instance of the active voting.
///
/// `none` iff no voting is currently active.
public final function Voting GetCurrentVoting() {
    if (VerifyVotingsTool()) {
        return votingsTool.GetCurrentVoting();
    }
    return none;
}

// DO NOT CALL MANUALLY
public final /*internal*/ function AsyncTask _popPending() {
    local AsyncTask result;

    if (pendingAsyncJobs.length == 0) {
        return result;
    }
    result = pendingAsyncJobs[0];
    pendingAsyncJobs.Remove(0, 1);
    return result;
}

// DO NOT CALL MANUALLY
public final /*internal*/ function _reloadFeature() {
    local Commands_Feature commandsFeature;
    local CommandFeatureTools toolsBundle;

    commandsToolLifeVersion = -1;
    votingsToolLifeVersion = -1;
    commandsFeature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
    if (commandsFeature != none) {
        toolsBundle = commandsFeature._borrowTools();
        commandsTool = toolsBundle.commands;
        if (commandsTool != none) {
            commandsToolLifeVersion = commandsTool.GetLifeVersion();
        }
        votingsTool = toolsBundle.votings;
        if (votingsTool != none) {
            votingsToolLifeVersion = votingsTool.GetLifeVersion();
        }
    }
    _.memory.Free(commandsFeature);
}

private final function bool VerifyCommandsTool() {
    if (commandsTool == none) {
        return false;
    }
    if (!commandsTool.IsAllocated() || commandsTool.GetLifeVersion() != commandsToolLifeVersion) {
        commandsTool = none;
        return false;
    }
    return true;
}

private final function bool VerifyVotingsTool() {
    if (votingsTool == none) {
        return false;
    }
    if (!votingsTool.IsAllocated() || votingsTool.GetLifeVersion() != votingsToolLifeVersion) {
        votingsTool = none;
        return false;
    }
    return true;
}

defaultproperties {
}