/**
 * 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 CommandDataBuilder extends AcediaObject
    dependson(Command);

//! This is an auxiliary class for convenient creation of [`Command::Data`]
//! using a builder pattern.
//!
//! ## Implementation
//!
//! We will store all defined data in two ways:
//!
//! 1. Selected data: data about parameters for subcommand/option that is
//!     currently being filled;
//! 2. Prepared data: data that was already filled as "selected data" then
//!     stored in these records. Whenever we want to switch to filling another
//!     subcommand/option or return already prepared data we must dump
//!     "selected data" into "prepared data" first and then return the latter.
//!
//! Builder object is automatically created when new `Command` instance is
//! allocated and doesn't normally need to be allocated by hand.

// "Prepared data"
var private Text commandName, commandGroup;
var private Text commandSummary;
var private array<Command.SubCommand> subcommands;
var private array<Command.Option> options;
var private bool requiresTarget;

// Auxiliary arrays signifying that we've started adding optional parameters
// into appropriate `subcommands` and `options`.
//
// All optional parameters must follow strictly after required parameters and
// so, after user have started adding optional parameters to subcommand/option,
// we prevent them from adding required ones (to that particular
// command/option).
var private array<byte> subcommandsIsOptional;
var private array<byte> optionsIsOptional;

// "Selected data"
// `false` means we have selected sub-command, `true` - option
var private bool selectedItemIsOption;
// `name` for sub-commands, `longName` for options
var private Text selectedItemName;
// Description of selected sub-command/option
var private Text selectedDescription;
// Are we filling optional parameters (`true`)? Or required ones (`false`)?
var private bool selectionIsOptional;
// Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray;

var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
var private LoggerAPI.Definition warnSameLongName, warnSameShortName;

protected function Constructor() {
    // Fill empty subcommand (no special key word) by default
    SubCommand(P(""));
}

protected function Finalizer() {
    subcommands.length = 0;
    subcommandsIsOptional.length = 0;
    options.length = 0;
    optionsIsOptional.length = 0;
    selectedParameterArray.length = 0;
    commandName = none;
    commandGroup = none;
    commandSummary = none;
    selectedItemName = none;
    selectedDescription = none;
    requiresTarget = false;
    selectedItemIsOption = false;
    selectionIsOptional = false;
}

/// Method that starts defining a new sub-command.
///
/// Creates new sub-command with a given name (if it's missing) and then selects
/// sub-command with a given name to add parameters to.
///
/// [`name`] defines name of the sub-command user wants, case-sensitive.
/// If `none` is passed, this method will do nothing.
public final function SubCommand(BaseText name) {
    local int subcommandIndex;

    if (name == none) {
        return;
    }
    if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) {
        return;
    }
    RecordSelection();
    subcommandIndex = FindSubCommandIndex(name);
    if (subcommandIndex < 0) {
        MakeEmptySelection(name, false);
        return;
    }
    // Load appropriate prepared data, if it exists for
    // sub-command with name `name`
    selectedItemIsOption = false;
    selectedItemName = subcommands[subcommandIndex].name;
    selectedDescription = subcommands[subcommandIndex].description;
    selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0;
    if (selectionIsOptional) {
        selectedParameterArray = subcommands[subcommandIndex].optional;
    } else {
        selectedParameterArray = subcommands[subcommandIndex].required;
    }
}

/// Method that starts defining a new option.
///
/// This method checks if some of the recorded options are in conflict with
/// given `longName` and `shortName` (already using one and only one of them).
/// In case there is no conflict, it creates new option with specified long and
/// short names (if such option is missing) and selects option with a long name
/// `longName` to add parameters to.
///
/// [`longName`] defines long name of the option, case-sensitive (for using
/// an option in form "--..."). Must be at least two characters long.
/// [`shortName`] defines short name of the option, case-sensitive (for using
/// an option in form "-..."). Must be exactly one character.
///
/// # Errors
///
/// Errors will be logged in case either of arguments are `none`, have
/// inappropriate length or are in conflict with each other.
public final function Option(BaseText longName, optional BaseText shortName) {
    local int                   optionIndex;
    local BaseText.Character    shortNameAsCharacter;

    // Unlike for `SubCommand()`, we need to ensure that option naming is
    // correct and does not conflict with existing options
    // (user might attempt to add two options with same long names and
    // different short ones).
    shortNameAsCharacter = GetValidShortName(longName, shortName);
    if (    !_.text.IsValidCharacter(shortNameAsCharacter)
        ||  VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) {
        //  ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
        //  are responsible for logging warnings/errors
        return;
    }
    SelectOption(longName);
    //  Set short name for new options
    optionIndex = FindOptionIndex(longName);
    if (optionIndex < 0) {
        //  We can only be here if option was created for the first time
        RecordSelection();
        //  So now it cannot fail
        optionIndex = FindOptionIndex(longName);
        options[optionIndex].shortName = shortNameAsCharacter;
    }
}

/// Adds description to the selected sub-command / option.
///
/// Highlights parts of the description in-between "`" characters.
///
/// Does nothing if nothing is yet selected.
public final function Describe(BaseText description) {
    local int fromIndex, toIndex;
    local BaseText.Formatting keyWordFormatting;
    local bool lookingForEnd;
    local MutableText coloredDescription;

    if (description == none) {
        return;
    }
    keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
    coloredDescription = description.MutableCopy();
    while (true) {
        if (lookingForEnd) {
            toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1);
        } else {
            fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1);
        }
        if (toIndex < 0 || fromIndex < 0) {
            break;
        }
        if (lookingForEnd) {
            coloredDescription.ChangeFormatting(
                keyWordFormatting,
                fromIndex,
                toIndex - fromIndex + 1);
            lookingForEnd = false;
        } else {
            lookingForEnd = true;
        }
    }
    coloredDescription.Replace(P("`"), P(""));
    if (lookingForEnd) {
        coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex);
    }
    _.memory.Free(selectedDescription);
    selectedDescription = coloredDescription.IntoText();
}

/// Sets new group of `Command.Data` under construction.
///
/// Group name is meant to be shared among several commands, allowing user to
/// filter or fetch commands of a certain group.
/// Group name is case-insensitive.
public final function Group(BaseText newName) {
    if (newName != none && newName == commandGroup) {
        return;
    }
    _.memory.Free(commandGroup);
    if (newName != none) {
        commandGroup = newName.Copy();
    } else {
        commandGroup = none;
    }
}

/// Sets new summary of `Command.Data` under construction.
///
/// Summary gives a short description of the command on the whole that will
/// be displayed when "help" command is listing available command
public final function Summary(BaseText newSummary) {
    if (newSummary != none && newSummary == commandSummary) {
        return;
    }
    _.memory.Free(commandSummary);
    if (newSummary != none) {
        commandSummary = newSummary.Copy();
    } else {
        commandSummary = none;
    }
}

/// Makes caller builder to mark `Command.Data` under construction to require
/// a player target.
public final function RequireTarget() {
    requiresTarget = true;
}


/// Any parameters added to currently selected sub-command / option after
/// calling this method will be marked as optional.
///
/// Further calls when the same sub-command / option is selected will do
/// nothing.
public final function OptionalParams()
{
    if (selectionIsOptional) {
        return;
    }
    //  Record all required parameters first, otherwise there would be no way
    //  to distinguish between them and optional parameters
    RecordSelection();
    selectionIsOptional = true;
    selectedParameterArray.length = 0;
}

/// Returns data that has been constructed so far by the caller
/// [`CommandDataBuilder`].
///
/// Does not reset progress.
public final function Command.Data BorrowData() {
    local Command.Data newData;

    // TODO: is this copying needed?
    RecordSelection();
    newData.group = commandGroup;
    newData.summary = commandSummary;
    newData.subcommands = subcommands;
    newData.options = options;
    newData.requiresTarget = requiresTarget;
    return newData;
}

// Adds new parameter to selected sub-command / option
private final function PushParameter(Command.Parameter newParameter) {
    selectedParameterArray[selectedParameterArray.length] = newParameter;
}

// Fills `Command.ParameterType` struct with given values
// (except boolean format).
// Assumes `displayName != none`.
private final function Command.Parameter NewParameter(
    BaseText displayName,
    Command.ParameterType parameterType,
    bool isListParameter,
    optional BaseText variableName
) {
    local Command.Parameter newParameter;

    newParameter.displayName = displayName.Copy();
    newParameter.type = parameterType;
    newParameter.allowsList = isListParameter;
    if (variableName != none) {
        newParameter.variableName = variableName.Copy();
    } else {
        newParameter.variableName = displayName.Copy();
    }
    return newParameter;
}

/// Adds new boolean parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command
/// input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBoolean(
    BaseText name,
    optional Command.PreferredBooleanFormat format,
    optional BaseText variableName
) {
    local Command.Parameter newParam;

    if (name != none) {
        newParam = NewParameter(name, CPT_Boolean, false, variableName);
        newParam.booleanFormat = format;
        PushParameter(newParam);
    }
}

/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how
/// parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBooleanList(
    BaseText name,
    optional Command.PreferredBooleanFormat format,
    optional BaseText variableName
) {
    local Command.Parameter newParam;

    if (name != none) {
        newParam = NewParameter(name, CPT_Boolean, true, variableName);
        newParam.booleanFormat = format;
        PushParameter(newParam);
    }
}

/// Adds new integer parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamInteger(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Integer, false, variableName));
    }
}

/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in
/// the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamIntegerList(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Integer, true, variableName));
    }
}

/// Adds new numeric parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumber(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Number, false, variableName));
    }
}

/// Adds new numeric list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumberList(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Number, true, variableName));
    }
}

/// Adds new text parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamText(
    BaseText name,
    optional BaseText variableName,
    optional BaseText aliasSourceName
) {
    local Command.Parameter newParameterValue;

    if (name == none) {
        return;
    }
    newParameterValue = NewParameter(name, CPT_Text, false, variableName);
    if (aliasSourceName != none) {
        newParameterValue.aliasSourceName = aliasSourceName.Copy();
    }
    PushParameter(newParameterValue);
}

/// Adds new text list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed. If left `none`, - will coincide with
/// `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamTextList(
    BaseText name,
    optional BaseText variableName,
    optional BaseText aliasSourceName
) {
    local Command.Parameter newParameterValue;

    if (name == none) {
        return;
    }
    newParameterValue = NewParameter(name, CPT_Text, true, variableName);
    if (aliasSourceName != none) {
        newParameterValue.aliasSourceName = aliasSourceName.Copy();
    }
    PushParameter(newParameterValue);
}

/// Adds new remainder parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// Remainder parameter is a special parameter that will simply consume all
/// remaining command's input as-is.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamRemainder(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Remainder, false, variableName));
    }
}

/// Adds new JSON object parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObject(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Object, false, variableName));
    }
}

/// Adds new JSON object list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObjectList(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Object, true, variableName));
    }
}

/// Adds new JSON array parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArray(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Array, false, variableName));
    }
}

/// Adds new JSON array list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArrayList(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_Array, true, variableName));
    }
}

/// Adds new JSON value parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSON(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_JSON, false, variableName));
    }
}

/// Adds new JSON value list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSONList(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_JSON, true, variableName));
    }
}


/// Adds new parameter that defines a set of players (required or optional
/// depends on whether `OptionalParams()` call happened) to the currently
/// selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayers(BaseText name, optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName));
    }
}

/// Adds new parameter that defines a list of sets of players (required or
/// optional depends on whether `OptionalParams()` call happened) to
/// the currently selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayersList(BaseText name,optional BaseText variableName) {
    if (name != none) {
        PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName));
    }
}

// Find index of sub-command with a given name `name` in `subcommands`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindSubCommandIndex(BaseText name) {
    local int i;

    if (name == none) {
        return -1;
    }
    for (i = 0; i < subcommands.length; i += 1) {
        if (name.Compare(subcommands[i].name)) {
            return i;
        }
    }
    return -1;
}

// Find index of option with a given name `name` in `options`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindOptionIndex(BaseText longName) {
    local int i;

    if (longName == none) {
        return -1;
    }
    for (i = 0; i < options.length; i += 1) {
        if (longName.Compare(options[i].longName)) {
            return i;
        }
    }
    return -1;
}

// Creates an empty selection record for subcommand or option with name (long name) `name`.
// Doe not check whether subcommand/option with that name already exists.
// Copies passed `name`, assumes that it is not `none`.
private final function MakeEmptySelection(BaseText name, bool selectedOption) {
    selectedItemIsOption = selectedOption;
    selectedItemName = name.Copy();
    selectedDescription = none;
    selectedParameterArray.length = 0;
    selectionIsOptional = false;
}

// Select option with a given long name `longName` from `options`.
// If there is no option with specified `longName` in prepared data - creates new record in
// selection, otherwise copies previously saved data.
// Automatically saves previously selected data into prepared data.
// Copies `name` if it has to create new record.
private final function SelectOption(BaseText longName) {
    local int optionIndex;

    if (longName == none) {
        return;
    }
    if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) {
        return;
    }
    RecordSelection();
    optionIndex = FindOptionIndex(longName);
    if (optionIndex < 0) {
        MakeEmptySelection(longName, true);
        return;
    }
    // Load appropriate prepared data, if it exists for
    // option with long name `longName`
    selectedItemIsOption = true;
    selectedItemName = options[optionIndex].longName;
    selectedDescription = options[optionIndex].description;
    selectionIsOptional = optionsIsOptional[optionIndex] > 0;
    if (selectionIsOptional) {
        selectedParameterArray = options[optionIndex].optional;
    } else {
        selectedParameterArray = options[optionIndex].required;
    }
}

//  Saves currently selected data into prepared data.
private final function RecordSelection() {
    if (selectedItemName == none) {
        return;
    }
    if (selectedItemIsOption) {
        RecordSelectedOption();
    } else {
        RecordSelectedSubCommand();
    }
}

//  Saves selected sub-command into prepared records.
//  Assumes that command and not an option is selected.
private final function RecordSelectedSubCommand() {
    local int selectedSubCommandIndex;
    local Command.SubCommand newSubcommand;

    if (selectedItemName == none) {
        return;
    }
    selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
    if (selectedSubCommandIndex < 0) {
        selectedSubCommandIndex = subcommands.length;
        subcommands[selectedSubCommandIndex] = newSubcommand;
    }
    subcommands[selectedSubCommandIndex].name = selectedItemName;
    subcommands[selectedSubCommandIndex].description = selectedDescription;
    if (selectionIsOptional) {
        subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
        subcommandsIsOptional[selectedSubCommandIndex] = 1;
    } else {
        subcommands[selectedSubCommandIndex].required = selectedParameterArray;
        subcommandsIsOptional[selectedSubCommandIndex] = 0;
    }
}

// Saves currently selected option into prepared records.
// Assumes that option and not an command is selected.
private final function RecordSelectedOption() {
    local int selectedOptionIndex;
    local Command.Option newOption;

    if (selectedItemName == none) {
        return;
    }
    selectedOptionIndex = FindOptionIndex(selectedItemName);
    if (selectedOptionIndex < 0) {
        selectedOptionIndex = options.length;
        options[selectedOptionIndex] = newOption;
    }
    options[selectedOptionIndex].longName = selectedItemName;
    options[selectedOptionIndex].description = selectedDescription;
    if (selectionIsOptional) {
        options[selectedOptionIndex].optional = selectedParameterArray;
        optionsIsOptional[selectedOptionIndex] = 1;
    } else {
        options[selectedOptionIndex].required = selectedParameterArray;
        optionsIsOptional[selectedOptionIndex] = 0;
    }
}

//  Validates names (printing errors in case of failure) for the option.
//  Long name must be at least 2 characters long.
//  Short name must be either:
//      1. exactly one character long;
//      2. `none`, which leads to deriving `shortName` from `longName`
//          as a first character.
//  Anything else will result in logging a failure and rejection of
//  the option altogether.
//  Returns `none` if validation failed and chosen short name otherwise
//  (if `shortName` was used for it - it's value will be copied).
private final function BaseText.Character GetValidShortName(
    BaseText longName,
    BaseText shortName
) {
    //  Validate `longName`
    if (longName == none) {
        return _.text.GetInvalidCharacter();
    }
    if (longName.GetLength() < 2) {
        _.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
        return _.text.GetInvalidCharacter();
    }
    //  Validate `shortName`,
    //  deriving if from `longName` if necessary & possible
    if (shortName == none) {
        return longName.GetCharacter(0);
    }
    if (shortName.IsEmpty() || shortName.GetLength() > 1) {
        _.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
        return _.text.GetInvalidCharacter();
    }
    return shortName.GetCharacter(0);
}

//  Checks that if any option record has a long/short name from a given pair of
//  names (`longName`, `shortName`), then it also has another one.
//
//  i.e. we cannot have several options with identical names:
//  (--silent, -s) and (--sick, -s).
private final function bool VerifyNoOptionNamingConflict(
    BaseText longName,
    BaseText.Character shortName
) {
    local int i;
    local bool sameShortNames, sameLongNames;

    //  To make sure we will search through the up-to-date `options`,
    //  record selection into prepared records.
    RecordSelection();
    for (i = 0; i < options.length; i += 1) {
        sameShortNames = _.text.AreEqual(shortName, options[i].shortName);
        sameLongNames = longName.Compare(options[i].longName);
        if (sameLongNames && !sameShortNames) {
            _.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy());
            return true;
        }
        if (!sameLongNames && sameShortNames) {
            _.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName));
            return true;
        }
    }
    return false;
}

defaultproperties
{
    errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
    errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
    warnSameLongName    = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
    warnSameShortName   = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
}