#nullable enable
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Terminal.Gui.Configuration;
///
/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
///
/// When constructed, the dictionary will be populated with uninitialized configuration properties for the
/// scope ( will be ).
///
///
public class Scope : ConcurrentDictionary
{
///
/// Creates a new instance. The dictionary will be populated with uninitialized (
/// will be ).
///
[RequiresUnreferencedCode (
"Uses cached configuration properties filtered by type T. This is AOT-safe as long as T is one of the known scope types (SettingsScope, ThemeScope, AppSettingsScope).")]
public Scope () : base (StringComparer.InvariantCultureIgnoreCase) { }
///
/// INTERNAL: Adds a new ConfigProperty given a . Determines the correct PropertyInfo etc... by
/// retrieving the
/// hard coded value for .
///
///
///
internal void AddValue (string name, object? value)
{
ConfigProperty? configProperty = GetHardCodedProperty (name);
if (configProperty is null)
{
throw new InvalidOperationException ($@"{name} is not a hard coded property.");
}
TryAdd (name, ConfigProperty.CreateCopy (configProperty));
this [name].PropertyValue = configProperty.PropertyValue;
}
internal ConfigProperty? GetHardCodedProperty (string name)
{
ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!
.FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name)
.Value;
if (configProperty is null)
{
return null;
}
var copy = ConfigProperty.CreateCopy (configProperty);
copy.PropertyValue = configProperty.PropertyValue;
return copy;
}
internal ConfigProperty GetUninitializedProperty (string name)
{
ConfigProperty? configProperty = ConfigurationManager.GetUninitializedConfigPropertiesByScope (typeof (T).Name)!
.FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name)
.Value;
if (configProperty is null)
{
throw new InvalidOperationException ($@"{name} is not a ConfigProperty.");
}
var copy = ConfigProperty.CreateCopy (configProperty);
copy.PropertyValue = configProperty.PropertyValue;
return copy;
}
///
/// INTERNAL: Updates the values of the properties of this scope to their corresponding static
/// properties.
///
[RequiresDynamicCode ("Uses reflection to retrieve property values")]
internal void UpdateToCurrentValues ()
{
foreach (KeyValuePair validProperties in this.Where (cp => cp.Value.PropertyInfo is { }))
{
validProperties.Value.UpdateToCurrentValue ();
}
}
///
/// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values.
///
internal void LoadHardCodedDefaults ()
{
foreach (KeyValuePair hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!)
{
var copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value);
TryAdd (hardCodedKeyValuePair.Key, copy);
this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue;
}
}
///
/// INTERNAL: Updates this scope with the values in using a deep clone.
///
///
/// The updated scope (this).
[RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
[RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
internal Scope? UpdateFrom (Scope scope)
{
foreach (KeyValuePair prop in scope)
{
if (!prop.Value.HasValue)
{
continue;
}
if (!ContainsKey (prop.Key))
{
if (!prop.Value.HasValue)
{
continue;
}
// Add an empty (HasValue = false) property to this scope
var copy = ConfigProperty.CreateCopy (prop.Value);
copy.PropertyValue = prop.Value.PropertyValue;
TryAdd (prop.Key, copy);
}
// Update the property value
this [prop.Key].UpdateFrom (prop.Value.PropertyValue);
}
return this;
}
///
/// INTERNAL: Applies the values of the properties of this scope to their corresponding
/// properties.
///
/// if one or more property value was applied; otherwise.
[RequiresDynamicCode ("Uses reflection to get and set property values")]
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepClone(T)")]
internal bool Apply ()
{
if (!ConfigurationManager.IsEnabled)
{
Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed.");
}
var set = false;
foreach (KeyValuePair propWithValue in this.Where (t => t.Value.HasValue))
{
if (propWithValue.Value.PropertyInfo != null)
{
object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null);
object? newValue = null;
// QUESTION: Should we avoid setting if currentValue == newValue?
if (propWithValue.Value.PropertyValue is Scope scopeSource && currentValue is Scope scopeDest)
{
newValue = scopeDest.UpdateFrom (scopeSource);
}
else
{
// Use DeepCloner to create a deep copy of the property value
newValue = DeepCloner.DeepClone (propWithValue.Value.PropertyValue);
}
// Logging.Debug($"{propWithValue.Key}: {currentValue} -> {newValue}");
Debug.Assert (!propWithValue.Value.Immutable);
propWithValue.Value.PropertyInfo.SetValue (null, newValue);
set = true;
}
}
return set;
}
}