using System.Collections.Immutable;
using System.Reflection;
using System.Text.Json.Serialization;

namespace Sandbox;

/// <summary>
/// We cache results for some expensive reflection queries.
/// This results in large performance improvements during various operations (Cloning, NetworkSpawn, Serilization...)
/// </summary>
internal static class ReflectionQueryCache
{
	private static Dictionary<Type, bool> _isTypeCloneableByCopy = new();
	private static Dictionary<Type, MemberDescription[]> _orderedMemberCache = new();
	private static Dictionary<Type, PropertyDescription[]> _requiredComponentMemberCache = new();

	public record SyncVarPropertyAndAttribute( PropertyInfo Property, SyncAttribute Attribute );
	private static Dictionary<Type, SyncVarPropertyAndAttribute[]> _syncVarMemberCache = new();

	/// <summary>
	/// Clears the type cache, called after HotLoad and after a game ended.
	/// Called from EditorUtilities.ClearCloneTypeCache and Game.Close
	/// </summary>
	public static void ClearTypeCache()
	{
		_isTypeCloneableByCopy.Clear();
		_orderedMemberCache.Clear();
		_requiredComponentMemberCache.Clear();
		_syncVarMemberCache.Clear();
	}

	/// <summary>
	/// Returns all properties and fields that should be (de)serialized.
	/// Also sorts the members for historic reasons.
	/// </summary>
	public static IEnumerable<MemberDescription> OrderedSerializableMembers( Type t )
	{
		if ( _orderedMemberCache.TryGetValue( t, out var members ) )
		{
			return members;
		}

		var type = Game.TypeLibrary.GetType( t );

		if ( type is null )
		{
			Log.Warning( $"TypeLibrary could not find {t}" );
			return Array.Empty<MemberDescription>();
		}

		// It's fucked that we need to order the members for cloning, but some games actually rely on that order.
		// See https://github.com/Facepunch/sbox/issues/1785
		var fieldAndPropertyMembers = type.Members.Where( ShouldSerializeMember ).OrderBy( x => x.Name ).ToArray();
		_orderedMemberCache[t] = fieldAndPropertyMembers;

		return fieldAndPropertyMembers;
	}

	private static bool ShouldSerializeMember( MemberDescription memberDesc )
	{
		if ( memberDesc is not PropertyDescription && memberDesc is not FieldDescription ) return false;

		return memberDesc.HasAttribute<PropertyAttribute>() && !memberDesc.HasAttribute<JsonIgnoreAttribute>();
	}

	/// <summary>
	/// Returns all properties that have a [RequireComponent] attribute.
	/// </summary>
	public static IEnumerable<PropertyDescription> RequiredComponentMembers( Type t )
	{
		if ( _requiredComponentMemberCache.TryGetValue( t, out var members ) )
		{
			return members;
		}

		var type = Game.TypeLibrary.GetType( t );

		if ( type is null )
		{
			Log.Warning( $"TypeLibrary could not find {t}" );
			return Array.Empty<PropertyDescription>();
		}

		var requiredComponentProps = type.Properties
			.Where( IsRequiredComponent )
			.ToArray();

		_requiredComponentMemberCache[t] = requiredComponentProps;

		return requiredComponentProps;
	}

	private static bool IsRequiredComponent( PropertyDescription prop )
	{
		return prop.HasAttribute<RequireComponentAttribute>();
	}

	/// <summary>
	/// Returns all properties that have a [Sync] attribute.
	/// </summary>
	public static IEnumerable<SyncVarPropertyAndAttribute> SyncProperties( Type t )
	{
		if ( _syncVarMemberCache.TryGetValue( t, out var members ) )
		{
			return members;
		}

		var properties = new List<PropertyInfo>();
		var currentType = t;

		// Collect all properties from the type and its base types
		while ( currentType != null )
		{
			var ourProperties = currentType.GetProperties( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly );
			properties.AddRange( ourProperties );
			currentType = currentType.BaseType;
		}

		// Find properties with the [Sync] attribute and create records
		var syncProperties = properties
			.Select( prop =>
			{
				var syncAttribute = prop.GetCustomAttributes( typeof( SyncAttribute ), inherit: true ).FirstOrDefault() as SyncAttribute;
				if ( syncAttribute != null )
				{
					return new SyncVarPropertyAndAttribute( prop, syncAttribute );
				}
				return null;
			} )
			.Where( x => x != null )
			.ToArray();

		_syncVarMemberCache[t] = syncProperties;

		return syncProperties;
	}

	/// <summary>
	/// Determines if a type can be cloned by a simple copy.
	/// This recursively walks through all properties and fields of the type to determine if they are cloneable by copy.
	/// Since this is fairly expensive, we cache the results for each type.
	/// </summary>
	public static bool IsTypeCloneableByCopy( Type t )
	{
		if ( _isTypeCloneableByCopy.TryGetValue( t, out var result ) )
		{
			return result;
		}

		// Use a HashSet to track types being processed to avoid infinite recursion
		_processingTypesCache.Clear();
		var isCloneableByCopy = IsTypeCloneableByCopyInternal( t, _processingTypesCache );

		_isTypeCloneableByCopy[t] = isCloneableByCopy;

		return isCloneableByCopy;
	}

	// Avoid allocation -> cache this
	private static HashSet<Type> _processingTypesCache = new();

	private static bool IsTypeCloneableByCopyInternal( Type t, HashSet<Type> processingTypes )
	{
		if ( t.IsPrimitive || t.IsEnum || t == typeof( string ) )
		{
			return true;
		}

		// Resource references can just be copied,
		if ( t.HasBaseType( "Sandbox.Resource" ) )
		{
			return true;
		}

		// Immutable lists are safe to copy, if their containing type is safe to copy
		if ( IsImmutableList( t ) )
		{
			return IsTypeCloneableByCopyInternal( t.GetGenericArguments()[0], processingTypes );
		}

		// Other Ref types are not cloneable by copy
		if ( !t.IsValueType )
		{
			return false;
		}

		if ( processingTypes.Contains( t ) )
		{
			// If the type is already being processed, return to avoid infinite recursion
			return true;
		}

		processingTypes.Add( t );

		// For value types check if all properties are cloneable by copy
		foreach ( var prop in t.GetProperties() )
		{
			if ( ShouldSkipPropertyTypeCheck( prop ) )
			{
				continue;
			}

			var isCloneable = IsTypeCloneableByCopyInternal( prop.PropertyType, processingTypes );
			_isTypeCloneableByCopy[prop.PropertyType] = isCloneable;
			if ( !isCloneable )
			{
				processingTypes.Remove( t );
				return false;
			}
		}

		foreach ( var field in t.GetFields() )
		{
			if ( ShouldSkipFieldTypeCheck( field ) )
			{
				continue;
			}

			var isCloneable = IsTypeCloneableByCopyInternal( field.FieldType, processingTypes );
			_isTypeCloneableByCopy[field.FieldType] = isCloneable;
			if ( !isCloneable )
			{
				processingTypes.Remove( t );
				return false;
			}
		}

		processingTypes.Remove( t );
		return true;
	}

	private static bool ShouldSkipPropertyTypeCheck( PropertyInfo prop )
	{
		var alwaysCheck = prop.HasAttribute( typeof( JsonIncludeAttribute ) ) || prop.HasAttribute( typeof( PropertyAttribute ) );
		var ignoredByDefault = prop.HasAttribute( typeof( JsonIgnoreAttribute ) ) || !prop.CanWrite || prop.SetMethod is null || prop.SetMethod.IsPrivate || prop.SetMethod.IsVirtual || (prop.GetMethod is not null && (prop.GetMethod.IsStatic || prop.GetMethod.IsVirtual));
		return !alwaysCheck && ignoredByDefault;
	}

	private static bool ShouldSkipFieldTypeCheck( FieldInfo field )
	{
		var alwaysCheck = field.HasAttribute( typeof( JsonIncludeAttribute ) ) || field.HasAttribute( typeof( PropertyAttribute ) );
		var ignoredByDefault = field.HasAttribute( typeof( JsonIgnoreAttribute ) ) || field.IsPrivate || field.IsStatic;
		return !alwaysCheck && ignoredByDefault;
	}

	private static bool IsImmutableList( Type t )
	{
		if ( !t.IsGenericType ) return false;

		return t.GetGenericTypeDefinition() == typeof( ImmutableList<> );
	}
}
