Extensions/TypeLibraryExtensions.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Internal;
using Sandbox.UI;

namespace DataTables;

internal static class TypeLibraryHelperExtensions
{
	public static List<MemberDescription> GetFieldsAndProperties( this TypeLibrary typeLibrary, TypeDescription type )
	{
		var props = type.Properties.Where( x => x.IsPublic && !x.IsStatic && x.CanRead && x.CanWrite &&
		                                        !x.HasAttribute( typeof(JsonIgnoreAttribute) ) &&
		                                        !x.HasAttribute( typeof(HideAttribute) ) );

		var fields = type.Fields.Where( x => x.IsPublic && !x.IsStatic &&
		                                     !x.HasAttribute( typeof(JsonIgnoreAttribute) ) &&
		                                     !x.HasAttribute( typeof(HideAttribute) ) );

		return props.Concat<MemberDescription>( fields ).OrderBy( x => x.Order )
			.ThenBy( x => x.SourceFile )
			.ThenBy( x => x.SourceLine )
			.ToList();
	}

	public static T Clone<T>( this TypeLibrary typeLibrary, T target )
	{
		return (T)CloneInternal( typeLibrary, target );
	}

	public static object CloneInternal( this TypeLibrary typeLibrary, object target )
	{
		if ( target is null )
			return null;

		var targetType = target.GetType();
		TypeDescription type = typeLibrary.GetType( targetType );

		if ( type.IsValueType || targetType.IsAssignableTo( typeof(Resource) ) )
			return target;

		if ( targetType.IsAssignableTo( typeof(IList) ) )
		{
			IList listTarget = (IList)target;
			IList result = (IList)typeLibrary.Create<object>( targetType );

			foreach ( var elem in listTarget )
			{
				result.Add( CloneInternal( typeLibrary, elem ) );
			}

			return result;
		}

		if ( targetType.IsAssignableTo( typeof(IDictionary) ) )
		{
			IDictionary dictTarget = (IDictionary)target;
			IDictionary result = (IDictionary)typeLibrary.Create<object>( targetType );

			foreach ( var key in dictTarget.Keys )
			{
				if ( key is null )
					continue;

				var value = dictTarget[key];
				if ( value is null )
					continue;

				result.Add( key, CloneInternal( typeLibrary, value ) );
			}

			return result;
		}

		return CloneObject( typeLibrary, target );
	}

	public static object CloneObject( this TypeLibrary typeLibrary, object target )
	{
		var targetType = target.GetType();
		TypeDescription type = typeLibrary.GetType( targetType );

		if ( type.IsValueType || targetType.IsAssignableTo( typeof(string) ) || targetType.IsAssignableTo( typeof(Resource) ) )
			return target;

		object instance = typeLibrary.Create<object>( targetType );

		var members = typeLibrary.GetFieldsAndProperties( type );
		foreach ( var member in members )
		{
			object value = null;
			if ( member.IsField )
			{
				FieldDescription field = (FieldDescription)member;
				value = field.GetValue( target );
				if ( value is null )
					continue;

				field.SetValue( instance, CloneInternal( typeLibrary, value ) );
				continue;
			}

			PropertyDescription property = (PropertyDescription)member;
			value = property.GetValue( target );
			if ( value is null )
				continue;

			property.SetValue( instance, CloneInternal( typeLibrary, value ) );
		}

		return instance;
	}

	public static T Merge<T>( this TypeLibrary typeLibrary, T target, T merger ) where T : class
	{
		Assert.True( target is not null );
		Assert.True( merger is not null );

		var targetType = target.GetType();
		TypeDescription type = typeLibrary.GetType( targetType );

		var mergerType = merger.GetType();
		TypeDescription mergerTypeDesc = typeLibrary.GetType( targetType );

		if ( mergerTypeDesc.IsValueType || mergerType.IsAssignableTo( typeof(string) ) ||
		     mergerType.IsAssignableTo( typeof(Resource) ) )
		{
			return merger;
		}

		var members = typeLibrary.GetFieldsAndProperties( type );
		foreach ( var member in members )
		{
			if ( member.IsField )
			{
				MergeField( typeLibrary, (FieldDescription)member, target, merger );
				continue;
			}

			MergeProperty( typeLibrary, (PropertyDescription)member, target, merger );
		}

		return target;
	}

	private static void MergeField( TypeLibrary typeLibrary, FieldDescription field, object target, object merger )
	{
		Assert.True( target is not null && merger is not null );

		var hasIgnore = field.HasAttribute<JsonIgnoreAttribute>();
		var hasHide = field.HasAttribute<HideAttribute>();

		if ( hasIgnore || hasHide )
			return;

		var mergerType = typeLibrary.GetType( merger.GetType() );
		if ( mergerType.IsGenericType )
			return;

		var mergerValue = field.GetValue( merger );
		var targetValue = field.GetValue( target );

		if ( mergerValue is null )
		{
			field.SetValue( target, null );
			return;
		}

		Type mergerValueType = mergerValue?.GetType();
		Type targetValueType = targetValue?.GetType();

		if ( mergerValueType != targetValueType )
		{
			object cloneObj = typeLibrary.CloneInternal( mergerValue );
			if ( cloneObj.GetType().IsAssignableTo( field.FieldType ) )
				field.SetValue( target, cloneObj );
			return;
		}

		var fieldType = typeLibrary.GetType( mergerValueType );

		if ( fieldType.IsValueType || mergerValueType.IsAssignableTo( typeof(string) ) ||
		     mergerValueType.IsAssignableTo( typeof(Resource) ) )
		{
			field.SetValue( target, mergerValue );
			return;
		}

		if ( targetValue is null )
		{
			field.SetValue( target, typeLibrary.CloneInternal( mergerValue ) );
			return;
		}

		if ( fieldType.TargetType.IsAssignableTo( typeof(IList) ) )
		{
			IList targetList = (IList)targetValue;
			IList mergerList = (IList)mergerValue;

			MergeList( typeLibrary, targetList, mergerList );
			return;
		}

		if ( fieldType.TargetType.IsAssignableTo( typeof(IDictionary) ) )
		{
			IDictionary targetDict = (IDictionary)targetValue;
			IDictionary mergerDict = (IDictionary)mergerValue;

			MergeDictionary( typeLibrary, targetDict, mergerDict );
			return;
		}

		var members = typeLibrary.GetFieldsAndProperties( fieldType );
		foreach ( var member in members )
		{
			if ( member.IsField )
			{
				MergeField( typeLibrary, (FieldDescription)member, field.GetValue( target ), mergerValue );
				continue;
			}

			MergeProperty( typeLibrary, (PropertyDescription)member, field.GetValue( target ), mergerValue );
		}
	}

	private static void MergeProperty( this TypeLibrary typeLibrary, PropertyDescription property, object target, object merger )
	{
		Assert.True( target is not null && merger is not null );

		var hasIgnore = property.HasAttribute<JsonIgnoreAttribute>();
		var hasHide = property.HasAttribute<HideAttribute>();

		if ( hasIgnore || hasHide || !property.CanRead || !property.CanWrite )
			return;

		var mergerType = typeLibrary.GetType( merger.GetType() );
		if ( mergerType.IsGenericType )
			return;

		var mergerValue = property.GetValue( merger );
		var targetValue = property.GetValue( target );

		if ( mergerValue is null )
		{
			property.SetValue( target, null );
			return;
		}

		Type mergerValueType = mergerValue?.GetType();
		Type targetValueType = targetValue?.GetType();

		if ( mergerValueType != targetValueType )
		{
			object cloneObj = typeLibrary.CloneInternal( mergerValue );
			if ( cloneObj.GetType().IsAssignableTo( property.PropertyType ) )
				property.SetValue( target, cloneObj );
			return;
		}

		var propertyType = typeLibrary.GetType( mergerValueType );

		if ( propertyType.IsValueType || mergerValueType.IsAssignableTo( typeof(string) ) ||
		     mergerValueType.IsAssignableTo( typeof(Resource) ) )
		{
			property.SetValue( target, mergerValue );
			return;
		}

		if ( targetValue is null )
		{
			property.SetValue( target, typeLibrary.CloneInternal( mergerValue ) );
			return;
		}

		if ( propertyType.TargetType.IsAssignableTo( typeof(IList) ) )
		{
			IList targetList = (IList)targetValue;
			IList mergerList = (IList)mergerValue;

			MergeList( typeLibrary, targetList, mergerList );
			return;
		}

		if ( propertyType.TargetType.IsAssignableTo( typeof(IDictionary) ) )
		{
			IDictionary targetDict = (IDictionary)targetValue;
			IDictionary mergerDict = (IDictionary)mergerValue;

			MergeDictionary( typeLibrary, targetDict, mergerDict );
			return;
		}

		var members = typeLibrary.GetFieldsAndProperties( propertyType );
		foreach ( var member in members )
		{
			if ( member.IsField )
			{
				MergeField( typeLibrary, (FieldDescription)member, property.GetValue( target ), mergerValue );
				continue;
			}

			MergeProperty( typeLibrary, (PropertyDescription)member, property.GetValue( target ), mergerValue );
		}
	}

	private static void MergeDictionary( TypeLibrary typeLibrary, IDictionary targetDict, IDictionary mergerDict )
	{
		if ( mergerDict.Count == 0 )
			targetDict.Clear();
		else
		{
			Type targetKeyArg = typeLibrary.GetGenericArguments( targetDict.GetType() )[0];
			Type targetValueArg = typeLibrary.GetGenericArguments( targetDict.GetType() )[1];
			Type mergerKeyArg = typeLibrary.GetGenericArguments( mergerDict.GetType() )[0];
			Type mergerValueArg = typeLibrary.GetGenericArguments( mergerDict.GetType() )[1];

			if ( mergerKeyArg != targetKeyArg || mergerValueArg != targetValueArg )
				targetDict.Clear();

			foreach ( var key in mergerDict.Keys )
			{
				var value = mergerDict[key];

				bool isCorrectKeyType = key.GetType().IsAssignableTo( targetKeyArg );
				bool isCorrectValueType = value.GetType().IsAssignableTo( targetValueArg );

				if ( !isCorrectKeyType || !isCorrectValueType )
					continue;

				if ( !targetDict.Contains( key ) )
				{
					if ( value is not null )
						targetDict.Add( key, typeLibrary.CloneInternal( value ) );
				}
				else
				{
					var elemTargetType = value.GetType();
					var elemType = typeLibrary.GetType( elemTargetType );
					if ( elemType.IsValueType )
					{
						if ( value is not null )
							targetDict[key] = value;
					}
					else
					{
						var mergerValue = mergerDict[key];
						var targetValue = targetDict[key];

						if ( mergerValue.GetType() == targetValue.GetType() )
						{
							targetDict[key] = Merge( typeLibrary, targetValue, mergerValue );
							continue;
						}

						if ( mergerValue.GetType().IsAssignableTo( targetValueArg ) )
							targetDict[key] = typeLibrary.CloneInternal( mergerValue );
					}
				}
			}

			List<object> nullKeys = new();
			foreach ( var key in targetDict.Keys )
			{
				if ( !mergerDict.Contains( key ) )
					nullKeys.Add( key );
			}

			foreach ( var key in nullKeys )
			{
				targetDict.Remove( key );
			}
		}
	}

	private static void MergeList( this TypeLibrary typeLibrary, IList targetList, IList mergerList )
	{
		if ( mergerList.Count == 0 )
			targetList.Clear();
		else
		{
			Type targetArg = typeLibrary.GetGenericArguments( targetList.GetType() ).First();
			Type mergerArg = typeLibrary.GetGenericArguments( mergerList.GetType() ).First();

			if ( mergerArg != targetArg )
				targetList.Clear();

			int i;
			for ( i = 0; i < mergerList.Count; i++ )
			{
				if ( i > targetList.Count - 1 )
				{
					var value = mergerList[i];
					if ( value.GetType().IsAssignableTo( targetArg ) )
						targetList.Add( typeLibrary.CloneInternal( value ) );
				}
				else
				{
					var elemTargetType = targetList[i].GetType();
					var elemType = typeLibrary.GetType( elemTargetType );
					if ( elemType.IsValueType )
					{
						var value = mergerList[i];
						if ( value.GetType().IsAssignableTo( targetArg ) )
							targetList[i] = mergerList[i];
					}
					else
					{
						var mergerValue = mergerList[i];
						var targetValue = targetList[i];

						if ( mergerValue.GetType() == targetValue.GetType() )
						{
							targetList[i] = Merge( typeLibrary, targetValue, mergerValue );
							continue;
						}

						if ( mergerValue.GetType().IsAssignableTo( targetArg ) )
							targetList[i] = typeLibrary.CloneInternal( mergerList[i] );
					}
				}
			}

			for ( int j = targetList.Count - 1; j >= i; j-- )
			{
				targetList.RemoveAt( j );
			}
		}
	}
}