/**
 *  Command for displaying help information about registered Acedia's commands.
 *      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 ACommandHelp extends Command
    dependson(LoggerAPI)
    dependson(CommandAPI);

/**
 *  # `ACommandHelp`
 *
 *  This built-in command is meant to do two things:
 *
 *      1. List all available commands and aliases related to them;
 *      2. Display help pages for available commands (both by command and
 *          alias names).
 *
 *  ## Implementation
 *
 *  ### Aliases loading
 *
 *      First thing this command tries to do upon creation is to read all
 *  command aliases and build a reverse map that allows to access aliases names
 *  by command + subcommand (even if latter one is empty) that these names
 *  refer to. This allows us to display relevant aliases names alongside
 *  command names. Map is stored inside `commandToAliasesMap` variable.
 *      (If aliases feature isn't yet available, `ACommandHelp` connects to
 *  `OnFeatureEnabled()` signal to do its job the moment required feature is
 *  enabled).
 *      This map is also made to be two-level one - it is...
 *
 *      * a `HashTable` with command names as keys,
 *      * that points to `HashTable`s with subcommand names as keys,
 *      * that points at `ArrayList` of aliases.
 *
 *  This allows us to also sort aliases by the subcommand name when displaying
 *  their list. This work is done by `FillCommandToAliasesMap()` method (that
 *  uses `ParseCommandNames()` method, which is also used for displaying
 *  command list).
 *
 *  ### Command list displaying
 *
 *  This is a simple process performed by `DisplayCommandLists()` that calls on
 *  a bunch of auxiliary `Print...()` methods.
 *
 *  ### Displaying help pages
 *
 *  Similar to the above, this is mostly a simple process that displays
 *  `Command`s' `CommandData` as a help page. Work is performed by
 *  `DisplayCommandHelpPages()` method that calls on a bunch of auxiliary
 *  `Print...()` methods, most notably `PrintHelpPageFor()` that can display
 *  help pages both for full commands, as well as only for a singular
 *  subcommand, which allows us to print proper help pages for command aliases
 *  that refer to a particular subcommand.
 */

//      For each key (given by lower case command name) stores another `HashMap`
//  that uses sub-command names as keys and returns `ArrayList` of aliases.
var private HashTable commandToAliasesMap;

var private User callerUser;

var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN;
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;

protected function Constructor()
{
    local Feature preenabledInstance;

    super.Constructor();
    //  We need to update aliases map every time aliases feature is reenabled
    _.environment.OnFeatureEnabled(self).connect = FillCommandToAliasesMap;
    //  If `Aliases_Feature` is already enabled - read its command aliases now
    preenabledInstance = class'Aliases_Feature'.static.GetEnabledInstance();
    FillCommandToAliasesMap(Aliases_Feature(preenabledInstance));
    _.memory.Free(preenabledInstance);
}

protected function Finalizer()
{
    super.Finalizer();
    _.memory.Free(commandToAliasesMap);
    commandToAliasesMap = none;
}

protected function BuildData(CommandDataBuilder builder)
{
    builder.Group(P("core"));
    builder.Summary(P("Displays detailed information about available commands."));
    builder.OptionalParams();
    builder.ParamTextList(P("commands"));

    builder.Option(P("aliases"));
    builder.Describe(P("When displaying available commands, specifying this flag will additionally"
        @ "make command to display all of their available aliases."));

    builder.Option(P("list"));
    builder.Describe(P("Display available commands. Optionally command groups can be specified and"
        @ "then only commands from such groups will be listed. Otherwise all commands will"
        @ "be displayed."));
    builder.OptionalParams();
    builder.ParamTextList(P("groups"));
}

protected function Executed(
    Command.CallData callData,
    EPlayer callerPlayer,
    CommandPermissions permissions
) {
    local bool printedSomething;
    local HashTable parameters, options;
    local ArrayList commandsToDisplay, commandGroupsToDisplay;

    callerUser = callerPlayer.GetIdentity();
    parameters = callData.parameters;
    options = callData.options;
    //  Print command list if "--list" option was specified
    if (options.HasKey(P("list")))
    {
        commandGroupsToDisplay = options.GetArrayListBy(P("/list/groups"));
        DisplayCommandLists(
            commandGroupsToDisplay,
            options.HasKey(P("aliases")));
        _.memory.Free(commandGroupsToDisplay);
        printedSomething = true;
    }
    //  Help pages.
    //  Only need to print them if:
    //      1. Any commands are specified as parameters;
    //      2. No commands or "--list" option was specified, then we want to
    //          print a help page for this command.
    if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
    {
        commandsToDisplay = parameters.GetArrayList(P("commands"));
        DisplayCommandHelpPages(commandsToDisplay, printedSomething);
        _.memory.Free(commandsToDisplay);
    }
    _.memory.Free(callerUser);
    callerUser = none;
}

//  If instance of the `Aliases_Feature` is passed as an argument (allowing this
//  method to be used as a slot for `OnFeatureEnabled` signal) and
//  `commandAliasSource` is available - empties current `commandToAliasesMap`
//  and refills it with available command aliases.
private final function FillCommandToAliasesMap(Feature enabledFeature)
{
    local int               i;
    local Text              aliasValue;
    local array<Text>       availableAliases;
    local BaseAliasSource   commandAliasSource;
    local MutableText       commandName, subcommandName;

    if (enabledFeature == none)                         return;
    if (enabledFeature.class != class'Aliases_Feature') return;
    commandAliasSource = _.alias.GetCommandSource();
    if (commandAliasSource == none)                     return;

    //  Drop map built by previous aliases (if it was even build up until now)
    _.memory.Free(commandToAliasesMap);
    commandToAliasesMap = _.collections.EmptyHashTable();
    //  Construct a command -> alias map by iterating over aliases
    availableAliases = commandAliasSource.GetAllAliases();
    for (i = 0; i < availableAliases.length; i += 1)
    {
        aliasValue = _.alias.ResolveCommand(availableAliases[i]);
        ParseCommandNames(aliasValue, commandName, subcommandName);
        aliasValue.FreeSelf();
        InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
        commandName.FreeSelf();
        subcommandName.FreeSelf();
        commandName = none;
        subcommandName = none;
    }
    //  Clean up
    _.memory.FreeMany(availableAliases);
    commandAliasSource.FreeSelf();
}

//      Breaks command name as it is intended to be specified in command aliases
//  ("<command>" or "<command>.<subcommand>") into command and subcommand,
//  returning both as `out` parameters.
private final function ParseCommandNames(
    BaseText        source,
    out MutableText commandName,
    out MutableText subcommandName)
{
    local Parser valueParser;

    if (source == none) {
        return;
    }
    if (subcommandName != none) {
        subcommandName.FreeSelf();
    }
    valueParser = source.Parse();
    subcommandName = valueParser
        .MUntil(commandName, T(TDOT).GetCharacter(0))
        .Match(T(TDOT))
        .GetRemainderM();
    _.memory.Free(valueParser);
}

//  Assumes that `commandToAliasesMap` and its arguments are not `none`.
private final function InsertIntoAliasesMap(
    BaseText commandName,
    BaseText subcommandName,
    BaseText alias)
{
    local Text      commandKey, subcommandKey;
    local HashTable subcommandToAliasesMap;
    local ArrayList aliasesArray;

    commandKey      = commandName.LowerCopy();
    subcommandKey   = subcommandName.LowerCopy();
    subcommandToAliasesMap = commandToAliasesMap.GetHashTable(commandKey);
    if (subcommandToAliasesMap == none) {
        subcommandToAliasesMap = _.collections.EmptyHashTable();
    }
    commandToAliasesMap.SetItem(commandKey, subcommandToAliasesMap);
    aliasesArray = subcommandToAliasesMap.GetArrayList(subcommandKey);
    if (aliasesArray == none) {
        aliasesArray = _.collections.EmptyArrayList();
    }
    subcommandToAliasesMap.SetItem(subcommandKey, aliasesArray);
    if (aliasesArray.Find(alias) < 0) {
        aliasesArray.AddItem(alias);
    }
    //  Cleaning up local references to collections and keys
    subcommandToAliasesMap.FreeSelf();
    aliasesArray.FreeSelf();
    commandKey.FreeSelf();
    subcommandKey.FreeSelf();
}

private final function DisplayCommandLists(
    ArrayList   commandGroupsToDisplay,
    bool        displayAliases)
{
    local int           i;
    local array<Text>   commandNames, groupsNames;

    if (commandGroupsToDisplay == none) {
        groupsNames = _.commands.GetGroupsNames();
    }
    else
    {
        for (i = 0; i < commandGroupsToDisplay.GetLength(); i += 1) {
            groupsNames[groupsNames.length] = commandGroupsToDisplay.GetText(i);
        }
    }
    callerConsole.WriteLine(T(TLIST_REGIRESTED_CMDS));
    for (i = 0; i < groupsNames.length; i += 1)
    {
        if (groupsNames[i] == none) {
            continue;
        }
        commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]);
        if (commandNames.length > 0)
        {
            callerConsole.UseColorOnce(_.color.TextSubHeader);
            if (groupsNames[i].IsEmpty()) {
                callerConsole.WriteLine(T(TEMPTY_GROUP));
            }
            else {
                callerConsole.WriteLine(groupsNames[i]);
            }
            PrintCommandsNamesArray(
                commandNames,
                displayAliases);
            _.memory.FreeMany(commandNames);
        } else {
            callerConsole.UseColor(_.color.TextFailure);
            callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
            callerConsole.Write(groupsNames[i]);
            callerConsole.WriteLine(T(TEMPTY_GROUP_END));
            callerConsole.ResetColor();
        }
    }
    _.memory.FreeMany(groupsNames);
}

private final function PrintCommandsNamesArray(
    array<Text> commandsNamesArray,
    bool displayAliases
) {
    local int i;
    local Command.Data nextData;
    local CommandAPI.CommandConfigInfo nextCommandPair;

    for (i = 0; i < commandsNamesArray.length; i += 1)
    {
        nextCommandPair = _.commands.ResolveCommandForUser(
            commandsNamesArray[i],
            callerUser);
        if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) {
            nextData = nextCommandPair.instance.BorrowData();
            callerConsole
                .UseColorOnce(_.color.textEmphasis)
                .Write(commandsNamesArray[i])
                .Write(T(TCOLON_SPACE))
                .WriteLine(nextData.summary);
            if (displayAliases) {
                PrintCommandAliases(commandsNamesArray[i]);
            }
        }
        _.memory.Free(nextCommandPair.instance);
    }
}

private final function PrintCommandAliases(BaseText commandName)
{
    local CollectionIterator    iter;
    local Text                  commandKey;
    local Text                  nextSubCommand;
    local ArrayList             nextAliasesArray;
    local HashTable             subCommandToAliasesMap;

    if (commandName == none)            return;
    if (commandToAliasesMap == none)    return;

    commandKey = commandName.LowerCopy();
    subCommandToAliasesMap = commandToAliasesMap.GetHashTable(commandName);
    commandKey.FreeSelf();
    if (subCommandToAliasesMap == none) {
        return;
    }
    //  Display aliases to command itself first
    nextAliasesArray = subCommandToAliasesMap.GetArrayList(T(TEMPTY));
    PrintAliasesArray(commandName, none, nextAliasesArray);
    _.memory.Free(nextAliasesArray);
    //  Then aliases to all of its subcommands, in no particular order
    iter = subCommandToAliasesMap.Iterate();
    while (!iter.HasFinished())
    {
        nextSubCommand = Text(iter.Getkey());   //  cannot be `none`
        if (nextSubCommand.IsEmpty())
        {
            nextSubCommand.FreeSelf();
            iter.Next();
            continue;
        }
        nextAliasesArray = ArrayList(iter.Get());
        PrintAliasesArray(commandName, nextSubCommand, nextAliasesArray);
        _.memory.Free(nextAliasesArray);
        _.memory.Free(nextSubCommand);
        iter.Next();
    }
    iter.FreeSelf();
}

private final function PrintAliasesArray(
    BaseText    commandName,
    BaseText    subcommandName,
    ArrayList   aliasesArray)
{
    local int   i;
    local Text  nextAlias;

    if (commandName == none)    return;
    if (aliasesArray == none)   return;

    callerConsole
        .Write(T(TALIASES_FOR))
        .Write(T(TSPACE))
        .UseColorOnce(_.color.TextEmphasis)
        .Write(commandName);
    if (subcommandName != none)
    {
        callerConsole
            .Write(T(TSPACE))
            .UseColorOnce(_.color.TextEmphasis)
            .Write(subCommandName);
    }
    callerConsole.Write(T(TCOLON_SPACE));
    for (i = 0; i < aliasesArray.GetLength(); i += 1)
    {
        if (i > 0) {
            callerConsole.Write(T(TCOMMA_SPACE));
        }
        nextAlias = aliasesArray.GetText(i);
        callerConsole.UseColorOnce(_.color.TextNeutral).Write(nextAlias);
        nextAlias.FreeSelf();
    }
    callerConsole.WriteBlock();
}

private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) {
    local int i;
    local Text nextUserProvidedName;
    local MutableText referredSubcommand;
    local CommandAPI.CommandConfigInfo nextPair;

    //  If arguments were empty - at least display our own help page
    if (commandList == none) {
        nextPair.instance = self;
        PrintHelpPageFor(usedName, none, nextPair);
        return;
    }
    //  Otherwise - print help for specified commands
    for (i = 0; i < commandList.GetLength(); i += 1) {
        nextUserProvidedName = commandList.GetText(i);
        nextPair = GetCommandFromUserProvidedName(
            nextUserProvidedName,
            /*out*/ referredSubcommand);
        if (nextPair.instance != none && !nextPair.usageForbidden) {
            if (printedSomething) {
                callerConsole.WriteLine(T(TSEPARATOR));
            }
            PrintHelpPageFor(
                nextUserProvidedName,
                referredSubcommand,
                nextPair);
            printedSomething = true;
        } else if (nextPair.instance != none) {
            callerConsole.UseColor(_.color.TextFailure);
            callerConsole.Write(T(TNO_COMMAND_BEGIN));
            callerConsole.Write(nextUserProvidedName);
            callerConsole.WriteLine(T(TNO_COMMAND_END));
            callerConsole.ResetColor();
        }
        _.memory.Free(nextPair.instance);
        _.memory.Free(nextUserProvidedName);
        _.memory.Free(referredSubcommand);
        //  `referredSubcommand` is passed as an `out` parameter on
        //  every iteration, so we need to prevent the possibility of its value
        //  being used.
        //  NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten.
        referredSubcommand = none;
    }
}

//      Returns `Command` based on the name, given by user.
//      `referredSubcommand` is always overwritten (freed if non-`none` value
//  is passed) and is used to return name of the subcommand for returned
//  `Command` that is specified by `nextUserProvidedName` (only relevant for
//  aliases that refer to a particular subcommand).
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName(
    BaseText nextUserProvidedName,
    out MutableText referredSubcommand)
{
    local CommandAPI.CommandConfigInfo result;
    local Text commandAliasValue;
    local MutableText parsedCommandName;

    //  Clear `out` parameter no matter what
    if (referredSubcommand != none) {
        referredSubcommand.FreeSelf();
        referredSubcommand = none;
    }
    //  Try getting command using `nextUserProvidedName` as a literal name
    result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser);
    if (result.instance != none) {
        return result;
    }
    //  On failure - try resolving it as an alias
    commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
    ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
    result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser);
    if (    result.instance == none
        ||  !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
        _.memory.Free(result.instance);
        return result;
    }
    //  Empty subcommand name from the alias is essentially no subcommand name
    if (referredSubcommand != none && referredSubcommand.IsEmpty()) {
        referredSubcommand.FreeSelf();
        referredSubcommand = none;
    }
    _.memory.Free2(commandAliasValue, parsedCommandName);
    return result;
}

private final function PrintHelpPageFor(
    BaseText commandAlias,
    BaseText referredSubcommand,
    CommandAPI.CommandConfigInfo commandPair
) {
    local Text commandNameLowerCase, commandNameUpperCase;

    //  Get capitalized command name
    commandNameUpperCase = commandAlias.UpperCopy();
    //  Print header: name + basic info
    callerConsole.UseColor(_.color.textHeader)
        .Write(commandNameUpperCase)
        .UseColor(_.color.textDefault);
    commandNameUpperCase.FreeSelf();
    if (commandPair.instance.BorrowData().requiresTarget) {
        callerConsole.WriteLine(T(TCMD_WITH_TARGET));
    }
    else {
        callerConsole.WriteLine(T(TCMD_WITHOUT_TARGET));
    }
    //  Print commands and options
    commandNameLowerCase = commandAlias.LowerCopy();
    PrintCommands(commandPair, commandNameLowerCase, referredSubcommand);
    commandNameLowerCase.FreeSelf();
    PrintOptions(commandPair.instance.BorrowData());
    //  Clean up
    callerConsole.ResetColor().Flush();
}

private final function PrintCommands(
    CommandAPI.CommandConfigInfo commandPair,
    BaseText commandName,
    BaseText referredSubcommand
) {
    local int i;
    local array<Command.SubCommand> subCommands;

    subCommands = commandPair.instance.BorrowData().subCommands;
    for (i = 0; i < subCommands.length; i += 1) {
        if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) {
            if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) {
                PrintSubCommand(subCommands[i], commandName, referredSubcommand != none);
            }
        }
    }
}

private final function PrintSubCommand(
    SubCommand  subCommand,
    BaseText    commandName,
    bool        skipSubcommandName)
{
    //  Command + parameters
    //  Command name + sub command name
    callerConsole.UseColor(_.color.textEmphasis)
        .Write(commandName)
        .Write(T(TSPACE));
    if (    !skipSubcommandName
        &&  subCommand.name != none
        &&  !subCommand.name.IsEmpty())
    {
        callerConsole.Write(subCommand.name).Write(T(TSPACE));
    }
    callerConsole.UseColor(_.color.textDefault);
    //  Parameters
    PrintParameters(subCommand.required, subCommand.optional);
    callerConsole.Flush();
    //  Description
    if (subCommand.description != none && !subCommand.description.IsEmpty()) {
        callerConsole.WriteBlock(subCommand.description);
    }
}

private final function PrintOptions(Command.Data data)
{
    local int           i;
    local array<Option> options;
    options = data.options;
    if (options.length <= 0) {
        return;
    }
    callerConsole
        .UseColor(_.color.textSubHeader)
        .WriteLine(T(TOPTIONS))
        .UseColor(_.color.textDefault);
    for (i = 0; i < options.length; i += 1) {
        PrintOption(options[i]);
    }
}

private final function PrintOption(Option option)
{
    local Text shortNameAsText;
    //  Option short and long names with added key characters
    shortNameAsText = _.text.FromCharacter(option.shortName);
    callerConsole
        .UseColor(_.color.textEmphasis)
        .Write(T(TKEY)).Write(shortNameAsText)          //  "-"
        .UseColor(_.color.textDefault)
        .Write(T(TCOMMA_SPACE))                         //  ", "
        .UseColor(_.color.textEmphasis)
        .Write(T(TDOUBLE_KEY)).Write(option.longName)   //  "--"
        .UseColor(_.color.textDefault);
    shortNameAsText.FreeSelf();
    //  Parameters
    if (option.required.length != 0 || option.optional.length != 0)
    {
        callerConsole.Write(T(TSPACE));
        PrintParameters(option.required, option.optional);
    }
    callerConsole.Flush();
    //  Description
    if (option.description != none && !option.description.IsEmpty()) {
        callerConsole.WriteBlock(option.description);
    }
}

private final function PrintParameters(
    array<Parameter>    required,
    array<Parameter>    optional)
{
    local int i;
    //  Print required
    for (i = 0; i < required.length; i += 1)
    {
        PrintParameter(required[i]);
        if (i < required.length - 1) {
            callerConsole.Write(T(TSPACE));
        }
    }
    if (optional.length <= 0) {
        return;
    }
    //  Print optional
    callerConsole.Write(T(TSPACE)).Write(T(TOPEN_BRACKET));
    for (i = 0; i < optional.length; i += 1)
    {
        PrintParameter(optional[i]);
        if (i < optional.length - 1) {
            callerConsole.Write(T(TSPACE));
        }
    }
    callerConsole.Write(T(TCLOSE_BRACKET));
}

private final function PrintParameter(Parameter parameter)
{
    switch (parameter.type)
    {
    case CPT_Boolean:
        callerConsole.UseColor(_.color.typeBoolean);
        break;
    case CPT_Integer:
        callerConsole.UseColor(_.color.typeNumber);
        break;
    case CPT_Number:
        callerConsole.UseColor(_.color.typeNumber);
        break;
    case CPT_Text:
    case CPT_Remainder:
        callerConsole.UseColor(_.color.typeString);
        break;
    case CPT_Object:
    case CPT_JSON:
    case CPT_Array:
    case CPT_PLAYERS:
        callerConsole.UseColor(_.color.typeLiteral);
        break;
    default:
        callerConsole.UseColor(_.color.textDefault);
    }
    callerConsole.Write(parameter.displayName);
    if (parameter.allowsList) {
        callerConsole.Write(T(TPLUS));
    }
    callerConsole.UseColor(_.color.textDefault);
}

defaultproperties
{
    TSPACE                  = 0
    stringConstants(0)  = " "
    TPLUS                   = 1
    stringConstants(1)  = "(+)"
    TOPEN_BRACKET           = 2
    stringConstants(2)  = "["
    TCLOSE_BRACKET          = 3
    stringConstants(3)  = "]"
    TKEY                    = 4
    stringConstants(4)  = "-"
    TDOUBLE_KEY             = 5
    stringConstants(5)  = "--"
    TCOMMA_SPACE            = 6
    stringConstants(6)  = ", "
    TCOLON_SPACE            = 7
    stringConstants(7)  = ": "
    TINDENT                 = 8
    stringConstants(8)  = "    "
    TBOOLEAN                = 9
    stringConstants(9)  = "boolean"
    TBOOLEAN_TRUE_FALSE     = 10
    stringConstants(10) = "true/false"
    TBOOLEAN_ENABLE_DISABLE = 11
    stringConstants(11) = "enable/disable"
    TBOOLEAN_ON_OFF         = 12
    stringConstants(12) = "on/off"
    TBOOLEAN_YES_NO         = 13
    stringConstants(13) = "yes/no"
    TCMD_WITH_TARGET        = 14
    stringConstants(14) = ": This command requires target to be specified."
    TCMD_WITHOUT_TARGET     = 15
    stringConstants(15) = ": This command does not require target to be specified."
    TOPTIONS                = 16
    stringConstants(16) = "OPTIONS"
    TSEPARATOR              = 17
    stringConstants(17) = "============================="
    TLIST_REGIRESTED_CMDS   = 18
    stringConstants(18) = "{$TextHeader List of registered commands}"
    TEMPTY_GROUP            = 19
    stringConstants(19) = "Empty group"
    TALIASES_FOR            = 20
    stringConstants(20) = "Aliases for"
    TEMPTY                  = 21
    stringConstants(21) = ""
    TDOT                    = 22
    stringConstants(22) = "."
    TNO_COMMAND_BEGIN       = 23
    stringConstants(23) = "Command `"
    TNO_COMMAND_END         = 24
    stringConstants(24) = "` not found!"
    TEMPTY_GROUP_BEGIN      = 25
    stringConstants(25) = "No commands in group \""
    TEMPTY_GROUP_END      = 26
    stringConstants(26) = "\"!"
    preferredName = "help"
}