/**
 *  Command for managing features.
 *      Copyright 2022 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 ACommandFeature extends Command
    dependson(PendingConfigsTool);

var private class<Feature> selectedFeatureClass;
var private Text selectedFeatureName;
var private Text selectedConfigName;

var private PendingConfigsTool pendingConfigs;
var private ACommandFeature_Announcer announcer;

protected function Constructor() {
    pendingConfigs = PendingConfigsTool(_.memory.Allocate(class'PendingConfigsTool'));
    super.Constructor();
}

protected function Finalizer() {
    _.memory.Free(announcer);
    _.memory.Free(pendingConfigs);
    super.Finalizer();
}

protected function BuildData(CommandDataBuilder builder) {
    builder.Group(P("admin"));
    builder.Summary(P("Managing features."));
    builder.Describe(P("Command for managing features and their configs."));
    builder.SubCommand(P("enable"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.OptionalParams();
    builder.ParamText(P("config"));
    builder.Describe(P("Enables specified <feature>. If <config> isn't specified -"
            @ "choses the \"default\" one, making new config with default"
            @ "settings if it doesn't exist."));
    builder.SubCommand(P("disable"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.Describe(P("Disables specified <feature>."));
    builder.SubCommand(P("showconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.OptionalParams();
    builder.ParamText(P("config"));
    builder.Describe(P("Show given <config> for the given <feature>."));
    builder.SubCommand(P("editconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.ParamText(P("config"));
    builder.ParamText(P("variable_path"));
    builder.ParamJSON(P("value"));
    builder.Describe(P("Changes a value inside given <config> of the given"
            @ "<feature> by setting value at JSON path <variable_path> to"
            @ "the JSON value <value>. Changes won't be immediately applied to"
            @ "the game and kept as pending."));
    builder.SubCommand(P("saveconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.ParamText(P("config"));
    builder.Describe(P("Saves pending changes for the given <config> of the given"
            @ "<feature>."));
    builder.SubCommand(P("newconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.ParamText(P("config"));
    builder.Describe(P("Creates new config for the given <feature>."));
    builder.SubCommand(P("removeconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.ParamText(P("config"));
    builder.Describe(P("Removes specified <config> of the specified <feature>."));
    builder.SubCommand(P("autoconf"));
    builder.ParamText(P("feature"),, P("feature"));
    builder.OptionalParams();
    builder.ParamText(P("config"));
    builder.Describe(P("Changes current auto config config of the specified"
            @ "<feature>. Auto config is a config that is supposed to be"
            @ "automatically enabled at the start of the Acedia, unless"
            @ "otherwise specified for the loader."));
    builder.Option(P("all"));
    builder.Describe(F("Affects subcommand {$TextEmphasis showconf} by making it"
            @ "show all available configs."));
    builder.Option(P("save"));
    builder.Describe(F("Affects subcommand {$TextEmphasis editconf} by making it"
            @ "also save all pending changes."));
    announcer = ACommandFeature_Announcer(
        _.memory.Allocate(class'ACommandFeature_Announcer'));
}

protected function Executed(
    CallData arguments,
    EPlayer instigator,
    CommandPermissions permissions
) {
    local bool saveFlag, allFlag;

    announcer.Setup(none, instigator, othersConsole);
    saveFlag = arguments.options.HasKey(P("save"));
    allFlag = arguments.options.HasKey(P("all"));
    SelectFeatureAndConfig(arguments);
    if (arguments.subCommandName.IsEmpty()) {
        ShowAllFeatures();
    } else if (arguments.subCommandName.Compare(P("enable"))) {
        EnableFeature();
    } else if (arguments.subCommandName.Compare(P("disable"))) {
        DisableFeature();
    } else if (arguments.subCommandName.Compare(P("showconf"))) {
        ShowSelectedConfigs(allFlag);
    } else if (arguments.subCommandName.Compare(P("editconf"))) {
        EditFeatureConfig(
            arguments.parameters.GetText(P("variable_path")),
            arguments.parameters.GetItem(P("value")),
            saveFlag);
    } else if (arguments.subCommandName.Compare(P("saveconf"))) {
        SaveFeatureConfig();
    } else if (arguments.subCommandName.Compare(P("newconf"))) {
        NewFeatureConfig();
    } else if (arguments.subCommandName.Compare(P("removeconf"))) {
        RemoveFeatureConfig();
    } else if (arguments.subCommandName.Compare(P("autoconf"))) {
        SetAutoFeatureConfig();
    }
    _.memory.Free2(selectedConfigName, selectedFeatureName);
    selectedConfigName = none;
    selectedFeatureName = none;
}

protected function SelectFeatureAndConfig(CallData arguments) {
    local Text featureClassName, userGivenConfigName;

    selectedFeatureName = arguments.parameters.GetTextBy(P("/feature/alias"));
    featureClassName = arguments.parameters.GetTextBy(P("/feature/value"));
    selectedFeatureClass = LoadFeatureClass(featureClassName);
    if (selectedFeatureClass == none && !arguments.subCommandName.IsEmpty()) {
        _.memory.Free(selectedFeatureName);
        selectedFeatureName = none;
        return;
    }
    _.memory.Free(featureClassName);
    userGivenConfigName = arguments.parameters.GetText(P("config"));
    if (userGivenConfigName != none) {
        selectedConfigName = userGivenConfigName.LowerCopy();
        userGivenConfigName.FreeSelf();
    }
    pendingConfigs.SelectConfig(selectedFeatureClass, selectedConfigName);
}

protected function EnableFeature() {
    local bool wasEnabled;
    local Text oldConfig, newConfig;
    local Feature instance;

    wasEnabled = selectedFeatureClass.static.IsEnabled();
    oldConfig = selectedFeatureClass.static.GetCurrentConfig();
    newConfig = PickConfigBasedOnParameter();
    //  Already enabled with the same config!
    if (oldConfig != none && oldConfig.Compare(newConfig, SCASE_INSENSITIVE)) {
        announcer.AnnounceFailedAlreadyEnabled(selectedFeatureName, newConfig);
        _.memory.Free(newConfig);
        _.memory.Free(oldConfig);
        return;
    }
    //  Try enabling and report the result
    instance = selectedFeatureClass.static.EnableMe(newConfig);
    if (instance == none) {
        announcer.AnnounceFailedCannotEnableFeature(
            selectedFeatureName,
            newConfig);
    } else if (wasEnabled) {
        announcer.AnnounceSwappedConfig(
            selectedFeatureName,
            oldConfig,
            newConfig);
    } else {
        announcer.AnnounceEnabledFeature(selectedFeatureName, newConfig);
    }
    _.memory.Free(newConfig);
    _.memory.Free(oldConfig);
}

protected function DisableFeature() {
    if (!selectedFeatureClass.static.IsEnabled()) {
        announcer.AnnounceFailedAlreadyDisabled(selectedFeatureName);
        return;
    }
    selectedFeatureClass.static.DisableMe();
    //  It is possible that this command itself is destroyed after above command
    //  so do the check just in case
    if (IsAllocated()) {
        announcer.AnnounceDisabledFeature(selectedFeatureName);
    }
}

protected function ShowSelectedConfigs(bool showAllFeatures) {
    local int i;
    local array<Text> availableConfigs;
    local MutableText configList;
    local class<FeatureConfig> configClass;

    if (showAllFeatures) {
        configClass = selectedFeatureClass.default.configClass;
        if (configClass != none) {
            availableConfigs = configClass.static.AvailableConfigs();
        }
        for (i = 0; i < availableConfigs.length; i += 1) {
            ShowFeatureConfig(availableConfigs[i]);
        }
        _.memory.FreeMany(availableConfigs);
        return;
    }
    if (selectedConfigName != none) {
        ShowFeatureConfig(selectedConfigName);
        return;
    }
    configList = PrintConfigList(selectedFeatureClass);
    callerConsole
        .Flush()
        .Write(P("Available configs: "))
        .WriteLine(configList);
    _.memory.Free(configList);
}

protected function ShowFeatureConfig(BaseText configName) {
    local MutableText dataAsJSON;
    local HashTable currentData, pendingData;

    if (configName == none) {
        return;
    }
    currentData = GetCurrentConfigData(configName);
    if (currentData == none) {
        announcer.AnnounceFailedNoDataForConfig(
            selectedFeatureName,
            configName);
        return;
    }
    //  Display current data
    dataAsJSON = _.json.PrettyPrint(currentData);
    announcer.AnnounceCurrentConfig(selectedFeatureName, configName);
    callerConsole.Flush().WriteLine(dataAsJSON);
    _.memory.Free(dataAsJSON);
    //  Display pending data
    pendingConfigs.SelectConfig(selectedFeatureClass, configName);
    pendingData = pendingConfigs.GetPendingConfigData();
    if (pendingData != none) {
        dataAsJSON = _.json.PrettyPrint(pendingData);
        announcer.AnnouncePendingConfig(
            selectedFeatureName,
            configName);
        callerConsole.Flush().WriteLine(dataAsJSON);
        _.memory.Free(dataAsJSON);
    }
    _.memory.Free(pendingData);
    _.memory.Free(currentData);
}

protected function Text PickConfigBasedOnParameter() {
    local Text resolvedConfig;
    local class<FeatureConfig> configClass;

    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return none;
    }
    //  If config was specified - simply check that it exists
    if (selectedConfigName != none) {
        if (configClass.static.Exists(selectedConfigName)) {
            return selectedConfigName.Copy();
        }
        announcer.AnnounceFailedConfigMissing(selectedConfigName);
        return none;
    }
    //  If it wasn't specified - try auto config instead
    resolvedConfig = configClass.static.GetAutoEnabledConfig();
    if (resolvedConfig == none) {
        announcer.AnnounceFailedNoConfigProvided(selectedFeatureName);
    }
    return resolvedConfig;
}

protected function class<Feature> LoadFeatureClass(BaseText featureClassName) {
    local class<Feature> featureClass;

    if (featureClassName == none) {
        return none;
    }
    featureClass = class<Feature>(_.memory.LoadClass(featureClassName));
    if (featureClass == none) {
        announcer.AnnounceFailedToLoadFeatureClass(featureClassName);
    }
    return featureClass;
}

protected function ShowAllFeatures() {
    local int i;
    local array< class<Feature> > availableFeatures;

    availableFeatures = _.environment.GetAvailableFeatures();
    for (i = 0; i < availableFeatures.length; i ++) {
        ShowFeature(availableFeatures[i]);
    }
}

protected function ShowFeature(class<Feature> featureClass)
{
    local MutableText featureName;
    local MutableText configList;

    if (featureClass == none) {
        return;
    }
    featureName = _.text
        .FromClassMutable(featureClass)
        .ChangeDefaultColor(_.color.TextEmphasis);
    configList = PrintConfigList(featureClass);
    callerConsole.Flush();
    if (featureClass.static.IsEnabled()) {
        callerConsole.Write(F("[  {$TextPositive enabled} ] "));
    }
    else {
        callerConsole.Write(F("[ {$TextNegative disabled} ] "));
    }
    callerConsole.Write(featureName)
        .Write(P(" with configs: "))
        .WriteLine(configList);
    _.memory.Free(featureName);
    _.memory.Free(configList);
}

protected function MutableText PrintConfigList(class<Feature> featureClass) {
    local int i;
    local Text autoConfig, enabledConfig;
    local ListBuilder configList;
    local MutableText result, nextConfig;
    local array<Text> availableConfigs;
    local class<FeatureConfig> configClass;

    if (featureClass == none)   return none;
    configClass = featureClass.default.configClass;
    if (configClass == none)    return none;

    availableConfigs = configClass.static.AvailableConfigs();
    enabledConfig = featureClass.static.GetCurrentConfig();
    autoConfig = configClass.static.GetAutoEnabledConfig();
    configList = ListBuilder(_.memory.Allocate(class'ListBuilder'));
    for (i = 0; i < availableConfigs.length; i += 1) {
        nextConfig = availableConfigs[i].MutableCopy();
        if (enabledConfig != none && enabledConfig.Compare(nextConfig, SCASE_INSENSITIVE)) {
            nextConfig.ChangeDefaultColor(_.color.TextPositive);
        }
        if (pendingConfigs.HasPendingConfigFor(featureClass, nextConfig)) {
            nextConfig.Append(F("{$TextEmphasis *}"));
        }
        configList.Item(nextConfig);
        _.memory.Free(nextConfig);
        if (autoConfig != none && autoConfig.Compare(availableConfigs[i], SCASE_INSENSITIVE)) {
            if (autoConfig.Compare(enabledConfig, SCASE_INSENSITIVE)) {
                configList.Comment(F("{$TextPositive auto enabled}"));
            } else {
                configList.Comment(F("{$TextNegative auto enabled}"));
            }
        }
    }
    result = configList.GetMutable();
    _.memory.Free3(configList, autoConfig, enabledConfig);
    _.memory.FreeMany(availableConfigs);
    return result;
}

protected function MutableText PrettyPrintValueAt(BaseText pathToValue) {
    local MutableText   printedValue;
    local AcediaObject  value;
    local HashTable     relevantData;

    relevantData = pendingConfigs.GetPendingConfigData();
    if (relevantData == none) {
        relevantData = GetCurrentSelectedConfigData();
    }
    if (relevantData != none) {
        value = relevantData.GetItemBy(pathToValue);
    }
    if (value != none) {
        printedValue = _.json.PrettyPrint(value);
        _.memory.Free(value);
    }
    _.memory.Free(relevantData);
    return printedValue;
}

protected function EditFeatureConfig(BaseText pathToValue, AcediaObject newValue, bool saveConfig) {
    local MutableText printedOldValue;
    local MutableText printedNewValue;
    local PendingConfigsTool.PendingConfigToolResult error;

    if (selectedFeatureClass == none) {
        return;
    }
    printedOldValue = PrettyPrintValueAt(pathToValue);
    error = pendingConfigs.EditConfig(pathToValue, newValue);
    if (error == PCTE_None) {
        printedNewValue = PrettyPrintValueAt(pathToValue);
    }
    if (error == PCTE_ConfigMissing) {
        announcer.AnnounceFailedConfigMissing(selectedConfigName);
    }
    else if (error == PCTE_ExpectedObject) {
        announcer.AnnounceFailedExpectedObject();
    } else if (error == PCTE_BadPointer) {
        announcer.AnnounceFailedBadPointer(
            selectedFeatureName,
            selectedConfigName,
            pathToValue);
    } else if (printedOldValue == none) {
        announcer.AnnounceConfigNewValue(
            selectedFeatureName,
            selectedConfigName,
            pathToValue,
            printedNewValue);
    } else {
        announcer.AnnounceConfigEdited(
            selectedFeatureName,
            selectedConfigName,
            pathToValue,
            printedOldValue,
            printedNewValue);
    }
    if (saveConfig && error == PCTE_None) {
        SaveFeatureConfig();
    }
    _.memory.Free(printedOldValue);
    _.memory.Free(printedNewValue);
    _.memory.Free(pathToValue);
    _.memory.Free(newValue);
}

protected function SaveFeatureConfig() {
    local BaseText enabledConfigName;
    local HashTable pendingData;
    local class<FeatureConfig> configClass;

    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return;
    }
    pendingData = pendingConfigs.GetPendingConfigData();
    if (pendingData == none) {
        announcer.AnnounceFailedPendingConfigMissing(selectedConfigName);
        return;
    }
    //  Make sure config exists
    configClass.static.NewConfig(selectedConfigName);
    configClass.static.SaveData(selectedConfigName, pendingData);
    //  Re-apply config if it is active?
    enabledConfigName = selectedFeatureClass.static.GetCurrentConfig();
    if (selectedConfigName.Compare(enabledConfigName, SCASE_INSENSITIVE)) {
        selectedFeatureClass.static.EnableMe(selectedConfigName);
        announcer.AnnouncePublicPendingConfigSaved(selectedFeatureName);
    } else {
        announcer.AnnouncePrivatePendingConfigSaved(selectedFeatureName, selectedConfigName);
    }
    _.memory.Free(enabledConfigName);
    pendingData.FreeSelf();
    pendingConfigs.RemoveConfig();
    return;
}

protected function NewFeatureConfig() {
    local BaseText enabledConfigName;
    local class<FeatureConfig> configClass;

    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return;
    }
    if (configClass.static.Exists(selectedConfigName)) {
        announcer.AnnounceFailedConfigAlreadyExists(selectedFeatureName, selectedConfigName);
        return;
    }
    if (!configClass.static.NewConfig(selectedConfigName)) {
        announcer.AnnounceFailedBadConfigName(selectedConfigName);
        return;
    }
    enabledConfigName = selectedFeatureClass.static.GetCurrentConfig();
    if (selectedConfigName.Compare(enabledConfigName, SCASE_INSENSITIVE)) {
        selectedFeatureClass.static.EnableMe(selectedConfigName);
        announcer.AnnouncePublicPendingConfigSaved(selectedFeatureName);
    }
    _.memory.Free(enabledConfigName);
    announcer.AnnounceConfigCreated(selectedFeatureName, selectedConfigName);
}

protected function RemoveFeatureConfig() {
    local class<FeatureConfig> configClass;

    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return;
    }
    if (!configClass.static.Exists(selectedConfigName)) {
        announcer.AnnounceFailedConfigDoesNotExist(
            selectedFeatureName,
            selectedConfigName);
        return;
    }
    pendingConfigs.RemoveConfig();
    configClass.static.DeleteConfig(selectedConfigName);
    announcer.AnnounceConfigRemoved(selectedFeatureName, selectedConfigName);
}

protected function SetAutoFeatureConfig() {
    local Text currentAutoEnabledConfig;
    local class<FeatureConfig> configClass;

    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return;
    }
    if (selectedConfigName != none && !configClass.static.Exists(selectedConfigName)) {
        announcer.AnnounceFailedConfigDoesNotExist(selectedFeatureName, selectedConfigName);
        return;
    }
    currentAutoEnabledConfig = configClass.static.GetAutoEnabledConfig();
    if (selectedConfigName == none && currentAutoEnabledConfig == none) {
        announcer.AnnounceFailedAlreadyNoAutoEnabled(selectedFeatureName);
    }
    else if (selectedConfigName != none &&
        selectedConfigName.Compare(currentAutoEnabledConfig, SCASE_INSENSITIVE)) {
        announcer.AnnounceFailedAlreadySameAutoEnabled(
            selectedFeatureName,
            selectedConfigName);
    } else {
        configClass.static.SetAutoEnabledConfig(selectedConfigName);
        if (selectedConfigName != none) {
            announcer.AnnounceAutoEnabledConfig(
                selectedFeatureName,
                selectedConfigName);
        } else {
            announcer.AnnounceRemovedAutoEnabledConfig(selectedFeatureName);
        }
    }
    _.memory.Free(currentAutoEnabledConfig);
}

private function HashTable GetCurrentConfigData(BaseText configName) {
    local class<FeatureConfig> configClass;

    if (configName == none) {
        return none;
    }
    configClass = selectedFeatureClass.default.configClass;
    if (configClass == none) {
        announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
        return none;
    }
    return configClass.static.LoadData(configName);
}

private function HashTable GetCurrentSelectedConfigData() {
    return GetCurrentConfigData(selectedConfigName);
}

defaultproperties {
    preferredName = "feature"
}