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 }