using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace Terminal.Gui.Configuration;
///
/// Provides deep cloning functionality for Terminal.Gui configuration objects.
/// Creates a deep copy of an object by recursively cloning public properties,
/// handling collections, arrays, dictionaries, and circular references.
///
///
/// This class does not use because it does not guarantee deep cloning,
/// may not handle circular references, and is not widely implemented in modern .NET types.
/// Instead, it uses reflection to ensure consistent deep cloning behavior across all types.
///
/// Limitations:
/// - Types without a parameterless constructor (and not handled as simple types, arrays, dictionaries, or
/// collections) may be instantiated using uninitialized objects, which could lead to runtime errors if not
/// properly handled.
/// - Immutable collections (e.g., ) are
/// not supported and will throw a .
/// - Only public, writable properties are cloned; private fields, read-only properties, and non-public members are
/// ignored.
///
///
public static class DeepCloner
{
///
/// Creates a deep copy of the specified object.
///
/// The type of the object to clone.
/// The object to clone.
/// A deep copy of the source object, or default if source is null.
[RequiresUnreferencedCode ("Deep cloning may use reflection which might be incompatible with AOT compilation if types aren't registered in SourceGenerationContext")]
[RequiresDynamicCode ("Deep cloning may use reflection that requires runtime code generation if source generation fails")]
public static T? DeepClone (T? source)
{
if (source is null)
{
return default (T?);
}
//// For AOT environments, use source generation exclusively
//if (IsAotEnvironment ())
//{
// if (TryUseSourceGeneratedCloner (source, out T? result))
// {
// return result;
// }
// // If in AOT but source generation failed, throw an exception
// // instead of silently falling back to reflection
// //throw new InvalidOperationException (
// // $"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
// // $"for AOT-compatible cloning.");
// Logging.Error ($"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
// $"for AOT-compatible cloning.");
//}
// Use reflection-based approach, which should have better performance in non-AOT environments
ConcurrentDictionary visited = new (ReferenceEqualityComparer.Instance);
return (T?)DeepCloneInternal (source, visited);
}
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.CreateInstance(Type)")]
[UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
private static object? DeepCloneInternal (object? source, ConcurrentDictionary visited)
{
if (source is null)
{
return null;
}
// Handle already visited objects to avoid circular references
if (visited.TryGetValue (source, out object? existingClone))
{
return existingClone;
}
Type type = source.GetType ();
// Handle immutable or simple types
if (IsSimpleType (type))
{
return source;
}
// Handle strings explicitly
if (type == typeof (string))
{
return source;
}
// Handle arrays
if (type.IsArray)
{
return CloneArray (source, visited);
}
// Handle dictionaries
if (source is IDictionary)
{
return CloneDictionary (source, visited);
}
// Handle collections
if (typeof (ICollection).IsAssignableFrom (type))
{
return CloneCollection (source, visited);
}
// Create new instance
object clone = CreateInstance (type);
// Add to visited before cloning properties
visited.TryAdd (source, clone);
// Clone writable public and internal properties
foreach (PropertyInfo prop in type.GetProperties (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where (p => !p.IsSpecialName) // exclude private backing fields or compiler-generated props
.Where (p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters ().Length == 0))
{
object? value = prop.GetValue (source);
object? clonedValue = DeepCloneInternal (value, visited);
prop.SetValue (clone, clonedValue);
}
return clone;
}
private static bool IsSimpleType ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
{
if (type.IsPrimitive
|| type.IsEnum
|| type == typeof (decimal)
|| type == typeof (DateTime)
|| type == typeof (DateTimeOffset)
|| type == typeof (TimeSpan)
|| type == typeof (Guid)
|| type == typeof (Rune)
|| type == typeof (string))
{
return true;
}
// Treat structs with no writable public properties as simple types (immutable structs)
if (type.IsValueType)
{
IEnumerable writableProperties = type.GetProperties (BindingFlags.Instance | BindingFlags.Public)
.Where (p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters ().Length == 0);
return !writableProperties.Any ();
}
// Treat PropertyInfo (e.g., RuntimePropertyInfo) as a simple type since it's metadata and shouldn't be cloned
if (typeof (PropertyInfo).IsAssignableFrom (type))
{
return true;
}
return false;
}
[RequiresUnreferencedCode ("Calls System.Text.Json.JsonSerializer.Deserialize(String, Type, JsonSerializerOptions)")]
[RequiresDynamicCode ("Calls System.Text.Json.JsonSerializer.Deserialize(String, Type, JsonSerializerOptions)")]
private static object CreateInstance ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
try
{
// Try parameterless constructor
if (type.GetConstructor (Type.EmptyTypes) != null)
{
return Activator.CreateInstance (type)!;
}
// Record support
if (type.GetMethod ("$") != null)
{
return Activator.CreateInstance (type)!;
}
// In AOT, try using the JsonSerializer if available
if (IsAotEnvironment () && CanSerializeWithJson (type))
{
return JsonSerializer.Deserialize (JsonSerializer.Serialize (new object (), type), type, ConfigurationManager.SerializerContext.Options)!;
}
throw new InvalidOperationException ($"Cannot create instance of type {type.FullName}. No parameterless constructor or clone method found.");
}
catch (MissingMethodException)
{
throw new InvalidOperationException ($"Cannot create instance of type {type.FullName} in AOT context. Consider adding this type to your SourceGenerationContext.");
}
}
[RequiresDynamicCode ("Calls System.Array.CreateInstance(Type, Int32)")]
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepCloneInternal(Object, ConcurrentDictionary)")]
private static object CloneArray (object source, ConcurrentDictionary visited)
{
var array = (Array)source;
Type elementType = array.GetType ().GetElementType ()!;
var newArray = Array.CreateInstance (elementType, array.Length);
visited.TryAdd (source, newArray);
for (var i = 0; i < array.Length; i++)
{
object? value = array.GetValue (i);
object? clonedValue = DeepCloneInternal (value, visited);
newArray.SetValue (clonedValue, i);
}
return newArray;
}
[RequiresDynamicCode ("Calls System.Type.MakeGenericType(params Type[])")]
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepCloneInternal(Object, ConcurrentDictionary)")]
private static object CloneCollection (object source, ConcurrentDictionary visited)
{
Type type = source.GetType ();
Type elementType = type.GetGenericArguments ().FirstOrDefault () ?? typeof (object);
// Check for immutable collections and throw if found
if (type.IsGenericType)
{
Type genericTypeDef = type.GetGenericTypeDefinition ();
if (genericTypeDef.FullName != null && genericTypeDef.FullName.StartsWith ("System.Collections.Immutable"))
{
throw new NotSupportedException ($"Cloning of immutable collections like {type.Name} is not supported.");
}
}
Type listType = typeof (List<>).MakeGenericType (elementType);
var tempList = (IList)Activator.CreateInstance (listType)!;
// Add to visited before cloning contents to prevent circular reference issues
visited.TryAdd (source, tempList);
foreach (object? item in (IEnumerable)source)
{
object? clonedItem = DeepCloneInternal (item, visited);
tempList.Add (clonedItem);
}
// Try to create the original collection type if possible
if (type != listType && type.GetConstructor ([listType]) != null)
{
object result = Activator.CreateInstance (type, tempList)!;
visited [source] = result;
return result;
}
return tempList;
}
#region Dictionary Support
[RequiresDynamicCode ("Calls System.Type.MakeGenericType(params Type[])")]
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepCloneInternal(Object, ConcurrentDictionary)")]
private static object CloneDictionary (object source, ConcurrentDictionary visited)
{
var sourceDict = (IDictionary)source;
Type type = source.GetType ();
// Check for frozen or immutable dictionaries and throw if found
if (type.IsGenericType)
{
CheckForUnsupportedDictionaryTypes (type);
}
// Determine dictionary type and comparer
Type [] genericArgs = type.GetGenericArguments ();
Type dictType;
if (genericArgs.Length == 2)
{
if (type.GetGenericTypeDefinition () == typeof (Dictionary<,>))
{
dictType = typeof (Dictionary<,>).MakeGenericType (genericArgs);
}
else if (type.GetGenericTypeDefinition () == typeof (ConcurrentDictionary<,>))
{
dictType = typeof (ConcurrentDictionary<,>).MakeGenericType (genericArgs);
}
else
{
throw new InvalidOperationException (
$"Unsupported dictionary type: {type}. Only Dictionary<,> and ConcurrentDictionary<,> are supported.");
}
}
else
{
dictType = typeof (Dictionary);
}
object? comparer = type.GetProperty ("Comparer")?.GetValue (source);
// Create a temporary dictionary to hold cloned key-value pairs
IDictionary tempDict = CreateDictionaryInstance (dictType, comparer);
visited.TryAdd (source, tempDict);
object? lastKey = null;
try
{
// Clone all key-value pairs
foreach (object? key in sourceDict.Keys)
{
lastKey = key;
object? clonedKey = DeepCloneInternal (key, visited);
object? clonedValue = DeepCloneInternal (sourceDict [key], visited);
if (tempDict.Contains (clonedKey!))
{
tempDict [clonedKey!] = clonedValue;
}
else
{
tempDict.Add (clonedKey!, clonedValue);
}
}
}
catch (InvalidOperationException ex)
{
// Handle cases where the dictionary is modified during enumeration
throw new InvalidOperationException (
$"Error cloning dictionary ({source}) (last key was \"{lastKey}\"). Ensure the source dictionary is not modified during cloning.",
ex);
}
// If the original dictionary type has a parameterless constructor, create a new instance
if (type.GetConstructor (Type.EmptyTypes) != null)
{
return CreateFinalDictionary (type, comparer, tempDict, source, visited);
}
return tempDict;
}
private static IDictionary CreateDictionaryInstance ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors)] Type dictType, object? comparer)
{
try
{
// Try to create the dictionary with the comparer
return comparer != null
? (IDictionary)Activator.CreateInstance (dictType, comparer)!
: (IDictionary)Activator.CreateInstance (dictType)!;
}
catch (MissingMethodException)
{
// Fallback to parameterless constructor if comparer constructor is not available
return (IDictionary)Activator.CreateInstance (dictType)!;
}
}
private static void CheckForUnsupportedDictionaryTypes (Type type)
{
Type? currentType = type;
while (currentType != null && currentType != typeof (object))
{
if (currentType.IsGenericType)
{
string? genericTypeName = currentType.GetGenericTypeDefinition ().FullName;
if (genericTypeName != null &&
(genericTypeName.StartsWith ("System.Collections.Frozen") ||
genericTypeName.StartsWith ("System.Collections.Immutable")))
{
throw new NotSupportedException ($"Cloning of frozen or immutable dictionaries like {type.Name} is not supported.");
}
}
currentType = currentType.BaseType;
}
}
[RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepCloneInternal(Object, ConcurrentDictionary)")]
private static object CreateFinalDictionary (
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type,
object? comparer,
IDictionary tempDict,
object source,
ConcurrentDictionary visited)
{
IDictionary newDict;
try
{
// Try to create the dictionary with the comparer
newDict = comparer != null
? (IDictionary)Activator.CreateInstance (type, comparer)!
: (IDictionary)Activator.CreateInstance (type)!;
}
catch (MissingMethodException)
{
// Fallback to parameterless constructor if comparer constructor is not available
newDict = (IDictionary)Activator.CreateInstance (type)!;
}
newDict.Clear ();
visited [source] = newDict;
// Copy cloned key-value pairs to the new dictionary
foreach (object? key in tempDict.Keys)
{
if (newDict.Contains (key))
{
newDict [key] = tempDict [key];
}
else
{
newDict.Add (key, tempDict [key]);
}
}
// Clone additional properties of the derived dictionary type
foreach (PropertyInfo prop in type.GetProperties (BindingFlags.Instance | BindingFlags.Public)
.Where (p => p.CanRead && p.CanWrite && p.GetIndexParameters ().Length == 0))
{
object? value = prop.GetValue (source);
object? clonedValue = DeepCloneInternal (value, visited);
prop.SetValue (newDict, clonedValue);
}
return newDict;
}
#endregion Dictionary Support
#region AOT Support
///
/// Determines if a type can be serialized using System.Text.Json based on the types
/// registered in the SourceGenerationContext.
///
/// The type to check
/// True if the type can be serialized using System.Text.Json; otherwise, false.
private static bool CanSerializeWithJson (Type type)
{
// Check if the type or any of its base types is registered in SourceGenerationContext
return ConfigurationManager.SerializerContext.GetType ()
.GetProperties (BindingFlags.Public | BindingFlags.Static)
.Any (p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition () == typeof (JsonTypeInfo<>) &&
(p.PropertyType.GetGenericArguments () [0] == type ||
p.PropertyType.GetGenericArguments () [0].IsAssignableFrom (type)));
}
private static bool IsAotEnvironment () =>
// Check if running in an AOT environment
Type.GetType ("System.Runtime.CompilerServices.RuntimeFeature")?.GetProperty ("IsDynamicCodeSupported")?.GetValue (null) is bool isDynamicCodeSupported && !isDynamicCodeSupported;
///
/// Attempts to clone an object using source-generated serialization from System.Text.Json.
/// This provides an AOT-compatible alternative to reflection-based deep cloning.
///
/// The type of the object to clone
/// The source object to clone
/// The cloned result, if successful
/// True if cloning succeeded using source generation; otherwise, false
private static bool TryUseSourceGeneratedCloner (T source, [NotNullWhen (true)] out T? result)
{
result = default;
try
{
// Check if the type has a JsonTypeInfo in our SourceGenerationContext
JsonTypeInfo? jsonTypeInfo = GetJsonTypeInfo ();
if (jsonTypeInfo != null)
{
// Use JSON serialization for deep cloning
string json = JsonSerializer.Serialize (source, jsonTypeInfo);
result = JsonSerializer.Deserialize (json, jsonTypeInfo);
return result != null;
}
return false;
}
catch
{
// If any exception occurs during serialization/deserialization,
// return false to fall back to reflection-based approach
return false;
}
}
///
/// Gets JsonTypeInfo for a type from the SourceGenerationContext, if available.
///
/// The type to get JsonTypeInfo for
/// JsonTypeInfo if found; otherwise, null
private static JsonTypeInfo? GetJsonTypeInfo ()
{
// Try to find a matching JsonTypeInfo property in the SourceGenerationContext
var contextType = ConfigurationManager.SerializerContext.GetType ();
// First try for an exact type match
var exactProperty = contextType.GetProperty (typeof (T).Name);
if (exactProperty != null &&
exactProperty.PropertyType.IsGenericType &&
exactProperty.PropertyType.GetGenericTypeDefinition () == typeof (JsonTypeInfo<>) &&
exactProperty.PropertyType.GetGenericArguments () [0] == typeof (T))
{
return (JsonTypeInfo?)exactProperty.GetValue (null);
}
// Then look for any compatible JsonTypeInfo
foreach (var prop in contextType.GetProperties (BindingFlags.Public | BindingFlags.Static))
{
if (prop.PropertyType.IsGenericType &&
prop.PropertyType.GetGenericTypeDefinition () == typeof (JsonTypeInfo<>) &&
prop.PropertyType.GetGenericArguments () [0].IsAssignableFrom (typeof (T)))
{
// This is a bit tricky - we've found a compatible type but need to cast it
// Warning: This might not work for all types and is a bit of a hack
return (JsonTypeInfo?)prop.GetValue (null);
}
}
return null;
}
#endregion AOT Support
}