using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Terminal.Gui.Configuration;
///
/// Holds a property's value and the that allows to
/// retrieve and apply the property's value.
///
///
/// Configuration properties must be / and and have the
/// attribute. If the type of the property requires specialized JSON
/// serialization, a must be provided using the
/// attribute.
///
public class ConfigProperty
{
/// Describes the property.
public PropertyInfo? PropertyInfo { get; set; }
/// INTERNAL: Cached value of ConfigurationPropertyAttribute.OmitClassName; makes more AOT friendly.
internal bool OmitClassName { get; set; }
/// INTERNAL: Cached value of ConfigurationPropertyAttribute.Scope; makes more AOT friendly.
internal string? ScopeType { get; set; }
private object? _propertyValue;
///
/// Holds the property's value as it was either read from the class's implementation or from a config file. If the
/// property has not been set (e.g. because no configuration file specified a value), will be .
///
public object? PropertyValue
{
get => _propertyValue;
set
{
if (Immutable)
{
throw new InvalidOperationException ($"Property {PropertyInfo?.Name} is immutable and cannot be set.");
}
// TODO: Verify value is correct type?
_propertyValue = value;
HasValue = true;
}
}
///
/// Gets or sets whether this config property has a value. This is set to when is set.
///
public bool HasValue { get; set; }
///
/// INTERNAL: Gets or sets whether this property is immutable. If , the property cannot be changed.
///
internal bool Immutable { get; set; }
/// Applies the to the static property described by .
///
[RequiresDynamicCode ("Uses reflection to get and set property values")]
[RequiresUnreferencedCode ("Uses DeepCloner which requires types to be registered in SourceGenerationContext")]
public bool Apply ()
{
try
{
if (PropertyInfo?.GetValue (null) is { })
{
// Use DeepCloner to create a deep copy of PropertyValue
object? val = DeepCloner.DeepClone (PropertyValue);
Debug.Assert (!Immutable);
PropertyInfo.SetValue (null, val);
}
}
catch (TargetInvocationException tie)
{
if (tie.InnerException is { })
{
throw new JsonException (
$"Error Applying Configuration Change: {tie.InnerException.Message}",
tie.InnerException
);
}
throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
}
catch (ArgumentException ae)
{
throw new JsonException (
$"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}",
ae
);
}
return PropertyValue != null;
}
///
/// INTERNAL: Creates a copy of a ConfigProperty with the same metadata but no value.
///
/// The source ConfigProperty.
/// A new ConfigProperty instance.
internal static ConfigProperty CreateCopy (ConfigProperty source)
{
return new ConfigProperty
{
Immutable = false,
PropertyInfo = source.PropertyInfo,
OmitClassName = source.OmitClassName,
ScopeType = source.ScopeType,
HasValue = false
};
}
///
/// INTERNAL: Create an immutable ConfigProperty with cached attribute information
///
/// The PropertyInfo to create from
/// A new ConfigProperty with attribute data cached
[RequiresDynamicCode ("Uses reflection to access custom attributes")]
internal static ConfigProperty CreateImmutableWithAttributeInfo (PropertyInfo propertyInfo)
{
var attr = propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute;
return new ConfigProperty
{
PropertyInfo = propertyInfo,
OmitClassName = attr?.OmitClassName ?? false,
ScopeType = attr?.Scope!.Name,
// By default, properties are immutable
Immutable = true
};
}
///
/// INTERNAL: Helper method to get the ConfigurationPropertyAttribute for a PropertyInfo
///
/// The PropertyInfo to get the attribute from
/// The ConfigurationPropertyAttribute if found; otherwise, null
[RequiresDynamicCode ("Uses reflection to access custom attributes")]
internal static ConfigurationPropertyAttribute? GetConfigurationPropertyAttribute (PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute;
}
///
/// INTERNAL: Helper method to check if a PropertyInfo has a ConfigurationPropertyAttribute
///
/// The PropertyInfo to check
/// True if the PropertyInfo has a ConfigurationPropertyAttribute; otherwise, false
[RequiresDynamicCode ("Uses reflection to access custom attributes")]
internal static bool HasConfigurationPropertyAttribute (PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) != null;
}
///
/// INTERNAL: Helper to get either the Json property named (specified by [JsonPropertyName(name)] or the actual property
/// name.
///
///
///
[RequiresDynamicCode ("Uses reflection to access custom attributes")]
internal static string GetJsonPropertyName (PropertyInfo pi)
{
var attr = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
return attr?.Name ?? pi.Name;
}
///
/// Updates (using reflection) the from the static
/// property described in .
///
///
[RequiresDynamicCode ("Uses reflection to retrieve property values")]
public object? UpdateToCurrentValue ()
{
return PropertyValue = PropertyInfo!.GetValue (null);
}
///
/// INTERNAL: Updates with the value in using a deep memberwise copy that
/// copies only the values that .
///
/// The source object to copy values from.
/// The updated property value.
/// Thrown when the source type doesn't match the property type.
[RequiresUnreferencedCode ("Uses DeepCloner which requires types to be registered in SourceGenerationContext")]
[RequiresDynamicCode ("Calls Terminal.Gui.DeepCloner.DeepClone(T)")]
internal object? UpdateFrom (object? source)
{
// If the source (higher-priority layer) doesn't provide a value, keep the existing value
// In the context of layering, a null source means the higher-priority layer doesn't specify a value,
// so we should retain the value from the lower-priority layer.
if (source is null)
{
return PropertyValue;
}
// Process the source based on its type
if (source is ConcurrentDictionary themeDictSource &&
PropertyValue is ConcurrentDictionary themeDictDest)
{
UpdateThemeScopeDictionary (themeDictSource, themeDictDest);
}
else if (source is ConcurrentDictionary concurrentDictSource &&
PropertyValue is ConcurrentDictionary concurrentDictDest)
{
UpdateConfigPropertyConcurrentDictionary (concurrentDictSource, concurrentDictDest);
}
else if (source is Dictionary dictSource &&
PropertyValue is Dictionary dictDest)
{
UpdateConfigPropertyDictionary (dictSource, dictDest);
}
else if (source is ConfigProperty configProperty)
{
if (configProperty.HasValue)
{
PropertyValue = DeepCloner.DeepClone (configProperty.PropertyValue);
}
}
else if (source is Dictionary dictSchemeSource &&
PropertyValue is Dictionary dictSchemesDest)
{
UpdateSchemeDictionary (dictSchemeSource, dictSchemesDest);
}
else if (source is Scheme scheme)
{
PropertyValue = new Scheme (scheme); // use copy constructor
}
else
{
// Validate type compatibility for non-dictionary types
ValidateTypeCompatibility (source);
// For non-scope types, perform a deep copy of the source value to ensure immutability
PropertyValue = DeepCloner.DeepClone (source);
}
return PropertyValue;
}
///
/// Validates that the source type is compatible with the property type.
///
/// The source object to validate.
/// Thrown when the source type doesn't match the property type.
private void ValidateTypeCompatibility (object source)
{
Type? underlyingType = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
bool isCompatibleType = source.GetType () == PropertyInfo.PropertyType ||
(underlyingType is { } && source.GetType () == underlyingType);
if (!isCompatibleType)
{
throw new ArgumentException (
$"The source object ({PropertyInfo.DeclaringType}.{PropertyInfo.Name}) is not of type {PropertyInfo.PropertyType}."
);
}
}
///
/// Updates a Scheme object by selectively applying explicitly set attributes from the source.
///
/// The source Scheme.
/// The destination Scheme to update.
private void UpdateScheme (Scheme sourceScheme, Scheme destScheme)
{
// We can't modify properties of a record directly, so we need to create a new one
// First, create a clone of the destination to preserve any values
var updatedScheme = new Scheme (destScheme);
//// Use with expressions to update only explicitly set attributes
//// For each role, check if the source has an explicitly set attribute
//if (sourceScheme.Normal.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Normal = sourceScheme.Normal };
//}
//if (sourceScheme.HotNormal.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { HotNormal = sourceScheme.HotNormal };
//}
//if (sourceScheme.Focus.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Focus = sourceScheme.Focus };
//}
//if (sourceScheme.HotFocus.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { HotFocus = sourceScheme.HotFocus };
//}
//if (sourceScheme.Active.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Active = sourceScheme.Active };
//}
//if (sourceScheme.HotActive.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { HotActive = sourceScheme.HotActive };
//}
//if (sourceScheme.Highlight.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Highlight = sourceScheme.Highlight };
//}
//if (sourceScheme.Editable.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Editable = sourceScheme.Editable };
//}
//if (sourceScheme.ReadOnly.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { ReadOnly = sourceScheme.ReadOnly };
//}
//if (sourceScheme.Disabled.IsExplicitlySet)
//{
// updatedScheme = updatedScheme with { Disabled = sourceScheme.Disabled };
//}
// Update the PropertyValue with the merged scheme
PropertyValue = updatedScheme;
}
///
/// Updates a ThemeScope dictionary with values from a source dictionary.
///
/// The source ThemeScope dictionary.
/// The destination ThemeScope dictionary.
[RequiresUnreferencedCode ("Calls Terminal.Gui.Scope.UpdateFrom(Scope)")]
[RequiresDynamicCode ("Calls Terminal.Gui.Scope.UpdateFrom(Scope)")]
private static void UpdateThemeScopeDictionary (
ConcurrentDictionary source,
ConcurrentDictionary destination)
{
foreach (KeyValuePair scope in source)
{
if (!destination.ContainsKey (scope.Key))
{
destination.TryAdd (scope.Key, scope.Value);
continue;
}
destination [scope.Key].UpdateFrom (scope.Value);
}
}
///
/// Updates a ConfigProperty dictionary with values from a source dictionary.
///
/// The source ConfigProperty dictionary.
/// The destination ConfigProperty dictionary.
[RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
[RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
private static void UpdateConfigPropertyConcurrentDictionary (
ConcurrentDictionary source,
ConcurrentDictionary destination)
{
foreach (KeyValuePair sourceProp in source)
{
// Skip properties without values
if (!sourceProp.Value.HasValue)
{
continue;
}
if (!destination.ContainsKey (sourceProp.Key))
{
// Add the property to the destination
var copy = CreateCopy (sourceProp.Value);
destination.TryAdd (sourceProp.Key, copy);
}
// Update the value in the destination
destination [sourceProp.Key].UpdateFrom (sourceProp.Value);
}
}
///
/// Updates a ConfigProperty dictionary with values from a source dictionary.
///
/// The source ConfigProperty dictionary.
/// The destination ConfigProperty dictionary.
[RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
[RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
private static void UpdateConfigPropertyDictionary (
Dictionary source,
Dictionary destination)
{
foreach (KeyValuePair sourceProp in source)
{
// Skip properties without values
if (!sourceProp.Value.HasValue)
{
continue;
}
if (!destination.ContainsKey (sourceProp.Key))
{
// Add the property to the destination
var copy = CreateCopy (sourceProp.Value);
destination.Add (sourceProp.Key, copy);
}
// Update the value in the destination
destination [sourceProp.Key].UpdateFrom (sourceProp.Value);
}
}
///
/// Updates a ConfigProperty dictionary with values from a source dictionary.
///
/// The source ConfigProperty dictionary.
/// The destination ConfigProperty dictionary.
private static void UpdateSchemeDictionary (
Dictionary source,
Dictionary destination)
{
foreach (KeyValuePair sourceProp in source)
{
if (!destination.ContainsKey (sourceProp.Key))
{
// Add the property to the destination
// Schemes are structs are passed by val
destination.Add (sourceProp.Key, sourceProp.Value);
}
// Update the value in the destination
// Schemes are structs are passed by val
destination [sourceProp.Key] = sourceProp.Value;
}
}
#region Initialization
///
/// INTERNAL: A cache of all classes that have properties decorated with the .
///
/// Is until is called.
private static ImmutableSortedDictionary? _classesWithConfigProps;
///
/// INTERNAL: Called from the method to initialize the
/// _classesWithConfigProps dictionary.
///
[RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. " +
"Only called during initialization and not needed during normal operation. " +
"In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
[RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. " +
"Use the SourceGenerationContext to register all configuration property types.")]
internal static void Initialize ()
{
if (_classesWithConfigProps is { })
{
return;
}
Dictionary dict = new (StringComparer.InvariantCultureIgnoreCase);
// Process assemblies directly to avoid LINQ overhead
Assembly [] assemblies = AppDomain.CurrentDomain.GetAssemblies ();
foreach (Assembly assembly in assemblies)
{
try
{
if (assembly.IsDynamic)
{
continue;
}
foreach (Type type in assembly.GetTypes ())
{
PropertyInfo [] properties = type.GetProperties ();
// Check if any property has the ConfigurationPropertyAttribute
var hasConfigProp = false;
foreach (PropertyInfo prop in properties)
{
if (HasConfigurationPropertyAttribute (prop))
{
hasConfigProp = true;
break;
}
}
if (hasConfigProp)
{
dict [type.Name] = type;
}
}
}
// Skip problematic assemblies that can't be loaded or analyzed
catch (ReflectionTypeLoadException)
{
continue;
}
catch (BadImageFormatException)
{
continue;
}
}
_classesWithConfigProps = dict.ToImmutableSortedDictionary ();
}
///
/// INTERNAL: Retrieves a dictionary of all properties annotated with from the classes in the module.
/// The dictionary case-insensitive and sorted.
/// The items have set, but not .
/// is set to .
///
[RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. " +
"Only called during initialization and not needed during normal operation. " +
"In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
[RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. " +
"Use the SourceGenerationContext to register all configuration property types.")]
internal static ImmutableSortedDictionary GetAllConfigProperties ()
{
if (_classesWithConfigProps is null)
{
throw new InvalidOperationException ("Initialize has not been called.");
}
// Estimate capacity to reduce resizing operations
int estimatedCapacity = _classesWithConfigProps.Count * 5; // Assume ~5 properties per class
Dictionary allConfigProperties = new (estimatedCapacity, StringComparer.InvariantCultureIgnoreCase);
// Process each class with direct iteration instead of LINQ
foreach (KeyValuePair classEntry in _classesWithConfigProps)
{
Type type = classEntry.Value;
// Get all public static/instance properties
BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
PropertyInfo [] properties = type.GetProperties (bindingFlags);
foreach (PropertyInfo propertyInfo in properties)
{
// Skip properties without our attribute
if (!HasConfigurationPropertyAttribute (propertyInfo))
{
continue;
}
// Verify the property is static
if (!propertyInfo.GetGetMethod (true)!.IsStatic)
{
throw new InvalidOperationException (
$"Property {propertyInfo.Name} in class {propertyInfo.DeclaringType?.Name} is not static. " +
"[ConfigurationProperty] properties must be static.");
}
// Create config property with cached attribute data
ConfigProperty configProperty = CreateImmutableWithAttributeInfo (propertyInfo);
// Use cached attribute data to determine the key
string key = configProperty.OmitClassName
? GetJsonPropertyName (propertyInfo)
: $"{propertyInfo.DeclaringType?.Name}.{propertyInfo.Name}";
allConfigProperties.Add (key, configProperty);
}
}
return allConfigProperties.ToImmutableSortedDictionary (StringComparer.InvariantCultureIgnoreCase);
}
#endregion Initialization
}