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

//! API for management of running [`Feature`]s and loaded packages.
//!
//! Instance of this class will be used by Acedia to manage resources available
//! from different packages like [`Feature`]s and such other etc..
//! This is mostly necessary to implement Acedia loader (and, possibly,
//! its alternatives) that would load available packages and enable [`Feature`]s
//! admin wants to be enabled.

/// Flag indicating whether Acedia has already undergone its shutdown process.
var private bool acediaShutDown;

/// Global data that can be changed by any active mods, allowing limited
/// interaction between them.
var private HashTable globalData;

/// Collection of all registered package manifests, representing different sets
/// of features and resources.
var private array< class<_manifest> > availablePackages;

/// List of all feature classes available from currently registered packages.
var private array< class<Feature> > availableFeatures;
/// Instances of features that are currently enabled and active within
/// this environment.
var private array<Feature> enabledFeatures;
var private array<int> enabledFeaturesLifeVersions;

/// Suffix appended to package names when searching for their manifest classes.
var private string manifestSuffix;

/// Indicates if the system should run in debug mode, enabling extra
/// development-related functionality.
var private const config bool debugMode;

var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled, errNoGlobals;

var private SimpleSignal onShutdownSignal;
var private SimpleSignal onShutdownSystemSignal;
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;

protected function Constructor() {
    globalData = _.collections.EmptyHashTable();
    //  Always register our core package
    RegisterPackage_S("AcediaCore");
    onShutdownSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onShutdownSystemSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
        _.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
    onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
        _.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
}

protected function Finalizer() {
    _.memory.Free(globalData);
    _.memory.Free(onShutdownSignal);
    _.memory.Free(onShutdownSystemSignal);
    _.memory.Free(onFeatureEnabledSignal);
    _.memory.Free(onFeatureDisabledSignal);
}

/// Signal that will be emitted right before Acedia shuts down.
///
/// At the point of emission all APIs should still exist and function.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDown(AcediaObject receiver) {
    return SimpleSlot(onShutdownSignal.NewSlot(receiver));
}

/// Signal that will be emitted during Acedia shut down.
///
/// System API will use it to clean up after themselves, so one shouldn't rely
/// on using them.
///
/// There is no reason to use this signal unless you're reimplementing one of
/// the APIs.
/// Otherwise you probably want to use [`AcediaEnvironment::OnShutDown()`]
/// signal instead.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDownSystem(AcediaObject receiver) {
    return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver));
}

/// Signal that will be emitted right after a new [`Feature`] is enabled and its
/// [`Feature::OnEnabled()`] method was called.
///
/// # Signature
///
/// void <slot>(Feature enabledFeature)
///
/// [`enabledFeature`] argument is an instance of [`Feature`] that was just
/// enabled.
public final /*signal*/ function Environment_FeatureEnabled_Slot OnFeatureEnabled(
    AcediaObject receiver
) {
    return Environment_FeatureEnabled_Slot(onFeatureEnabledSignal.NewSlot(receiver));
}

/// Signal that will be emitted right after when a [`Feature`] is disabled
/// and its [`Feature::OnDisabled()`] method was called.
///
/// # Signature
///
/// void <slot>(class<Feature> disabledFeatureClass)
///
/// Argument [`disabledFeatureClass`] describes class of the [`Feature`]
/// instance that was just disabled.
public final /*signal*/ function Environment_FeatureDisabled_Slot OnFeatureDisabled(
    AcediaObject receiver
) {
    return Environment_FeatureDisabled_Slot(onFeatureDisabledSignal.NewSlot(receiver));
}

/// Shuts AcediaCore down, performing all the necessary clean up.
public final function Shutdown() {
    local LevelCore core;

    if (acediaShutDown) {
        return;
    }
    DisableAllFeatures();
    onShutdownSignal.Emit();
    onShutdownSystemSignal.Emit();
    core = class'ServerLevelCore'.static.GetInstance();
    if (core != none) {
        core.Destroy();
    }
    core = class'ClientLevelCore'.static.GetInstance();
    if (core != none) {
        core.Destroy();
    }
    _.DropCoreAPI();
    acediaShutDown = true;
}

/// Registers an Acedia package with a given name.
///
/// Returns `true` if package was successfully registered, `false` if it either
/// does not exist, was already registered or [`packageName`] is `none`.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either
/// missing or not an Acedia package).
public final function bool RegisterPackage(BaseText packageName) {
    local class<_manifest> manifestClass;

    if (packageName == none) {
        return false;
    }
    _.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy());
    manifestClass = class<_manifest>(DynamicLoadObject(
        packageName.ToString() $ manifestSuffix, class'Class', true));
    if (manifestClass == none) {
        _.logger.Auto(errNotRegistered).Arg(packageName.Copy());
        return false;
    }
    if (IsManifestRegistered(manifestClass)) {
        _.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy());
        return false;
    }
    availablePackages[availablePackages.length] = manifestClass;
    ReadManifest(manifestClass);
    return true;
}

/// Registers an Acedia package with a given name.
///
/// Returns `true` if package was successfully registered, `false` if it either
/// does not exist or was already registered.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either
/// missing or not an Acedia package).
public final function RegisterPackage_S(string packageName) {
    local Text wrapper;

    wrapper = _.text.FromString(packageName);
    RegisterPackage(wrapper);
    _.memory.Free(wrapper);
}

private final function bool IsManifestRegistered(class<_manifest> manifestClass) {
    local int i;

    for (i = 0; i < availablePackages.length; i += 1) {
        if (manifestClass == availablePackages[i]) {
            return true;
        }
    }
    return false;
}

private final function ReadManifest(class<_manifest> manifestClass) {
    local int i;

    for (i = 0; i < manifestClass.default.features.length; i += 1) {
        if (manifestClass.default.features[i] == none) {
            continue;
        }
        manifestClass.default.features[i].static.LoadConfigs();
        availableFeatures[availableFeatures.length] = manifestClass.default.features[i];
    }
    for (i = 0; i < manifestClass.default.testCases.length; i += 1) {
        class'TestingService'.static.RegisterTestCase(manifestClass.default.testCases[i]);
    }
}

/// Returns `true` iff AcediaCore is running in the debug mode.
///
/// AcediaCore's debug mode allows features to enable functionality that is only
/// useful during development.
/// Whether AcediaCore is running in a debug mode is decided at launch and
/// cannot be changed.
public final function bool IsDebugging() {
    return debugMode;
}

/// Returns all packages registered in the caller [`AcediaEnvironment`].
public final function array< class<_manifest> > GetAvailablePackages() {
    return availablePackages;
}

/// Returns all [`Feature`]s available in the caller [`AcediaEnvironment`].
public final function array< class<Feature> > GetAvailableFeatures() {
    return availableFeatures;
}

/// Returns all currently enabled [`Feature`]s.
public final function array<Feature> GetEnabledFeatures() {
    local int i;

    for (i = 0; i < enabledFeatures.length; i += 1) {
        enabledFeatures[i].NewRef();
    }
    return enabledFeatures;
}

// CleanRemove [`Feature`]s that got deallocated.
// This shouldn't happen unless someone messes up.
private final function CleanEnabledFeatures()
{
    local int i;

    while (i < enabledFeatures.length) {
        if (enabledFeatures[i].GetLifeVersion() != enabledFeaturesLifeVersions[i]) {
            enabledFeatures.Remove(i, 1);
        } else {
            i += 1;
        }
    }
}

/// Checks if [`Feature`] of given class is enabled.
///
/// Even if If feature of class [`featureClass`] is enabled, it's not
/// necessarily that the instance you have reference to is enabled.
///
/// Although unlikely, it is possible that someone spawned another instance of
/// the same class that isn't considered enabled.
/// If you want to check whether some particular instance of given class
/// [`featureClass`] is enabled, use [`AcediaEnvironment::IsFeatureEnabled()`]
/// method instead.
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) {
    local int i;

    if (featureClass == none) {
        return false;
    }
    CleanEnabledFeatures();
    for (i = 0; i < enabledFeatures.length; i += 1) {
        if (featureClass == enabledFeatures[i].class) {
            return true;
        }
    }
    return false;
}

/// Checks if given `Feature` instance is enabled.
///
/// If you want to check if any instance instance of given class
/// [`classToCheck`] is enabled (and not [`feature`] specifically),
/// use [`AcediaEnvironment::IsFeatureClassEnabled()`] method instead.
public final function bool IsFeatureEnabled(Feature feature) {
    local int i;

    if (feature == none)        return false;
    if (!feature.IsAllocated()) return false;

    CleanEnabledFeatures();
    for (i = 0; i < enabledFeatures.length; i += 1) {
        if (feature == enabledFeatures[i]) {
            return true;
        }
    }
    return false;
}

/// Returns enabled [`Feature`] instance of the given class.
///
/// Returns `none` only if [`featureClass`] is not enabled (or also `none`).
public final function Feature GetEnabledFeature(class<Feature> featureClass) {
    local int i;

    if (featureClass == none) {
        return none;
    }
    CleanEnabledFeatures();
    for (i = 0; i < enabledFeatures.length; i += 1) {
        if (featureClass == enabledFeatures[i].class) {
            enabledFeatures[i].NewRef();
            return enabledFeatures[i];
        }
    }
    return none;
}

/// Enables given [`Feature`] instance [`newEnabledFeature`] with a given
/// config.
/// Does not change a config for already enabled feature, failing instead.
///
/// Returns `true` if given [`newEnabledFeature`] was enabled and `false`
/// otherwise (including if feature of the same class has already been enabled).
///
/// # Errors
///
/// Will log an error if feature with the same class as [`newEnabledFeature`] is
/// already enabled.
/// In case already enabled feature is the same instance as
/// [`newEnabledFeature`] a warning will be logged instead.
public final function bool EnableFeature(Feature newEnabledFeature, optional BaseText configName) {
    local int i;

    if (newEnabledFeature == none)          return false;
    if (!newEnabledFeature.IsAllocated())   return false;

    CleanEnabledFeatures();
    for (i = 0; i < enabledFeatures.length; i += 1) {
        if (newEnabledFeature.class == enabledFeatures[i].class) {
            if (newEnabledFeature == enabledFeatures[i]) {
                _.logger
                    .Auto(warnFeatureAlreadyEnabled)
                    .Arg(_.text.FromClass(newEnabledFeature.class));
            }
            else {
                _.logger
                    .Auto(errFeatureClassAlreadyEnabled)
                    .Arg(_.text.FromClass(newEnabledFeature.class));
            }
            return false;
        }
    }
    newEnabledFeature.NewRef();
    enabledFeatures[enabledFeatures.length] = newEnabledFeature;
    enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] =
        newEnabledFeature.GetLifeVersion();
    newEnabledFeature.EnableInternal(configName);
    onFeatureEnabledSignal.Emit(newEnabledFeature);
    return true;
}

/// Disables given [`Feature`] instance [`featureToDisable`].
///
/// Returns `true` if given [`newEnabledFeature`] was disabled and `false`
/// otherwise (including if it already was disabled).
public final function bool DisableFeature(Feature featureToDisable) {
    local int i;

    if (featureToDisable == none)           return false;
    if (!featureToDisable.IsAllocated())    return false;

    CleanEnabledFeatures();
    for (i = 0; i < enabledFeatures.length; i += 1) {
        if (featureToDisable == enabledFeatures[i]) {
            enabledFeatures.Remove(i, 1);
            enabledFeaturesLifeVersions.Remove(i, 1);
            featureToDisable.DisableInternal();
            onFeatureDisabledSignal.Emit(featureToDisable.class);
            _.memory.Free(featureToDisable);
            return true;
        }
    }
    return false;
}

/// Disables all currently enabled [`Feature`]s.
///
/// Mainly intended for the clean up when Acedia shuts down.
public final function DisableAllFeatures() {
    local int i;
    local array<Feature> featuresCopy;

    CleanEnabledFeatures();
    // Move features from `enabledFeatures`, which marks them as disabled,
    // allowing us to successfully call their `DisableInternal()` methods.
    featuresCopy = enabledFeatures;
    enabledFeatures.length = 0;
    enabledFeaturesLifeVersions.length = 0;
    for (i = 0; i < featuresCopy.length; i += 1) {
        featuresCopy[i].DisableInternal();
        onFeatureDisabledSignal.Emit(featuresCopy[i].class);
    }
    _.memory.FreeMany(featuresCopy);
}

/// Returns globally defined value with a specified name.
///
/// Returns `none` if [`valueName`] is `none`.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
/// Also returns `none` no matter the argument.
public final function AcediaObject GetGlobalValue(BaseText valueName) {
    if (valueName == none) {
        return none;
    }
    if (globalData == none) {
        _.logger.Auto(errNoGlobals);
        return none;
    }
    return globalData.GetItem(valueName);
}

/// Returns globally defined value with a specified name.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
/// Also returns `none` no matter the argument.
public final function AcediaObject GetGlobalValue_S(string valueName) {
    local Text wrapper;
    local AcediaObject value;

    wrapper = _.text.FromString(valueName);
    value = GetGlobalValue(wrapper);
    _.memory.Free(wrapper);
    return value;
}

/// Sets globally defined value for a specified name.
///
/// Does nothing if [`valueName`] is `none`.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
public final function SetGlobalValue(BaseText valueName, AcediaObject value) {
    if (valueName == none) {
        return;
    }
    if (globalData == none) {
        _.logger.Auto(errNoGlobals);
        return;
    }
    globalData.SetItem(valueName, value);
}

/// Sets globally defined value for a specified name.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
public final function SetGlobalValue_S(string valueName, AcediaObject value) {
    local Text wrapper;

    wrapper = _.text.FromString(valueName);
    SetGlobalValue(wrapper, value);
    _.memory.Free(wrapper);
}

defaultproperties {
    manifestSuffix = ".Manifest"
    debugMode = false
    infoRegisteringPackage          = (l=LOG_Info,m="Registering package \"%1\".")
    infoAlreadyRegistered           = (l=LOG_Info,m="Package \"%1\" is already registered.")
    errNotRegistered                = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")
    warnFeatureAlreadyEnabled       = (l=LOG_Warning,m="Trying to enable already enabled instance of `Feature` class `%1`.")
    errFeatureClassAlreadyEnabled   = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.")
    errNoGlobals                    = (l=LOG_Error,m="Global data isn't initialize for `AcediaEnvironment`.")
}