using System.Collections.Concurrent; using System.Linq.Expressions; using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; using Lua; namespace AIStudio.Settings; public static class ManagedConfiguration { private static readonly ConcurrentDictionary METADATA = new(); /// /// Registers a configuration setting with a default value. /// /// /// When called from the JSON deserializer, the configSelection parameter will be null. /// In this case, the method will return the default value without registering the setting. /// /// The expression to select the configuration class. /// The expression to select the property within the configuration class. /// The default value to use when the setting is not configured. /// The type of the configuration class. /// The type of the property within the configuration class. /// The default value. public static TValue Register(Expression>? configSelection, Expression> propertyExpression, TValue defaultValue) { // When called from the JSON deserializer by using the standard constructor, // we ignore the register call and return the default value: if(configSelection is null) return defaultValue; var configPath = Path(configSelection, propertyExpression); // If the metadata already exists for this configuration path, we return the default value: if (METADATA.ContainsKey(configPath)) return defaultValue; METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) { Default = defaultValue, }; return defaultValue; } /// /// Attempts to retrieve the configuration metadata for a given configuration selection and property expression. /// /// /// When no configuration metadata is found, it returns a NoConfig instance with the default value set to default(TValue). /// This allows the caller to handle the absence of configuration gracefully. In such cases, the return value of the method will be false. /// /// The expression to select the configuration class. /// The expression to select the property within the configuration class. /// The output parameter that will hold the configuration metadata if found. /// The type of the configuration class. /// The type of the property within the configuration class. /// True if the configuration metadata was found, otherwise false. public static bool TryGet(Expression> configSelection, Expression> propertyExpression, out ConfigMeta configMeta) { var configPath = Path(configSelection, propertyExpression); if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) { configMeta = meta; return true; } configMeta = new NoConfig(configSelection, propertyExpression) { Default = default!, }; return false; } /// /// Attempts to process the configuration settings from a Lua table. /// /// /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. /// The setting's value is set to the configured value. /// /// The ID of the related configuration plugin. /// The Lua table containing the settings to process. /// The expression to select the configuration class. /// The expression to select the property within the configuration class. /// When true, the method will not apply any changes, but only check if the configuration can be read. /// The type of the configuration class. /// The type of the property within the configuration class. /// True when the configuration was successfully processed, otherwise false. public static bool TryProcessConfiguration(Expression> configSelection, Expression> propertyExpression, Guid configPluginId, LuaTable settings, bool dryRun) { if(!TryGet(configSelection, propertyExpression, out var configMeta)) return false; var (configuredValue, successful) = configMeta.Default switch { Enum => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue) && configuredEnumValue.TryRead(out var configuredEnumText) && Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum) ? ((TValue)configuredEnum, true) : (configMeta.Default, false), Guid => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredGuidValue) && configuredGuidValue.TryRead(out var configuredGuidText) && Guid.TryParse(configuredGuidText, out var configuredGuid) ? ((TValue)(object)configuredGuid, true) : (configMeta.Default, false), string => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue) && configuredTextValue.TryRead(out var configuredText) ? ((TValue)(object)configuredText, true) : (configMeta.Default, false), bool => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredBoolValue) && configuredBoolValue.TryRead(out var configuredState) ? ((TValue)(object)configuredState, true) : (configMeta.Default, false), int => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredIntValue) && configuredIntValue.TryRead(out var configuredInt) ? ((TValue)(object)configuredInt, true) : (configMeta.Default, false), double => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredDoubleValue) && configuredDoubleValue.TryRead(out var configuredDouble) ? ((TValue)(object)configuredDouble, true) : (configMeta.Default, false), float => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredFloatValue) && configuredFloatValue.TryRead(out var configuredFloat) ? ((TValue)(object)configuredFloat, true) : (configMeta.Default, false), _ => (configMeta.Default, false), }; if(dryRun) return successful; switch (successful) { case true: // // Case: the setting was configured, and we could read the value successfully. // configMeta.SetValue(configuredValue); configMeta.LockManagedState(configPluginId); break; case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: // // Case: the setting was configured previously, but we could not read the value successfully. // This happens when the setting was removed from the configuration plugin. We handle that // case only when the setting was locked and managed by the same configuration plugin. // // The other case, when the setting was locked and managed by a different configuration plugin, // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin // is still available. If it is not available, it resets the managed state of the // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. // configMeta.ResetManagedState(); break; case false: // // Case: the setting was not configured, or we could not read the value successfully. // We do not change the setting, and it remains at whatever value it had before. // break; } return successful; } /// /// Checks if a configuration setting is left over from a configuration plugin that is no longer available. /// If the configuration setting is locked and managed by a configuration plugin that is not available, /// it resets the managed state of the configuration setting and returns true. /// Otherwise, it returns false. /// /// The expression to select the configuration class. /// The expression to select the property within the configuration class. /// The collection of available plugins to check against. /// The type of the configuration class. /// The type of the property within the configuration class. /// True if the configuration setting is left over and was reset, otherwise false. public static bool IsConfigurationLeftOver(Expression> configSelection, Expression> propertyExpression, IEnumerable availablePlugins) { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; if(configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; // Check if the configuration plugin ID is valid against the available plugin IDs: var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); if (plugin is null) { // Remove the locked state: configMeta.ResetManagedState(); return true; } return false; } private static string Path(Expression> configSelection, Expression> propertyExpression) { var className = typeof(TClass).Name; var memberExpressionConfig = configSelection.GetMemberExpression(); var configName = memberExpressionConfig.Member.Name; var memberExpressionProperty = propertyExpression.GetMemberExpression(); var propertyName = memberExpressionProperty.Member.Name; var configPath = $"{configName}.{className}.{propertyName}"; return configPath; } }