#if XUNIT_NULLABLE
#nullable enable
#endif

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

#if XUNIT_NULLABLE
using System.Diagnostics.CodeAnalysis;
#endif

namespace Xunit.Sdk
{
	/// <summary>
	/// Default implementation of <see cref="IEqualityComparer{T}"/> used by the xUnit.net equality assertions.
	/// </summary>
	/// <typeparam name="T">The type that is being compared.</typeparam>
	class AssertEqualityComparer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T> : IEqualityComparer<T>
	{
		static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter<object>(new AssertEqualityComparer<object>());
		static readonly TypeInfo NullableTypeInfo = typeof(Nullable<>).GetTypeInfo();

		readonly Func<IEqualityComparer> innerComparerFactory;

		/// <summary>
		/// Initializes a new instance of the <see cref="AssertEqualityComparer{T}" /> class.
		/// </summary>
		/// <param name="innerComparer">The inner comparer to be used when the compared objects are enumerable.</param>
#if XUNIT_NULLABLE
		public AssertEqualityComparer(IEqualityComparer? innerComparer = null)
#else
		public AssertEqualityComparer(IEqualityComparer innerComparer = null)
#endif
		{
			// Use a thunk to delay evaluation of DefaultInnerComparer
			innerComparerFactory = () => innerComparer ?? DefaultInnerComparer;
		}

		/// <inheritdoc/>
		public bool Equals(
#if XUNIT_NULLABLE
			[AllowNull] T x,
			[AllowNull] T y)
#else
			T x,
			T y)
#endif
		{
			int? _;

			return Equals(x, y, out _);
		}

		/// <inheritdoc/>
		public bool Equals(
#if XUNIT_NULLABLE
			[AllowNull] T x,
			[AllowNull] T y,
#else
			T x,
			T y,
#endif
			out int? mismatchIndex)
		{
			mismatchIndex = null;
			var typeInfo = typeof(T).GetTypeInfo();

			// Null?
			if (x == null && y == null)
				return true;
			if (x == null || y == null)
				return false;

			// Implements IEquatable<T>?
			var equatable = x as IEquatable<T>;
			if (equatable != null)
				return equatable.Equals(y);

			// Implements IComparable<T>?
			var comparableGeneric = x as IComparable<T>;
			if (comparableGeneric != null)
			{
				try
				{
					return comparableGeneric.CompareTo(y) == 0;
				}
				catch
				{
					// Some implementations of IComparable<T>.CompareTo throw exceptions in
					// certain situations, such as if x can't compare against y.
					// If this happens, just swallow up the exception and continue comparing.
				}
			}

			// Implements IComparable?
			var comparable = x as IComparable;
			if (comparable != null)
			{
				try
				{
					return comparable.CompareTo(y) == 0;
				}
				catch
				{
					// Some implementations of IComparable.CompareTo throw exceptions in
					// certain situations, such as if x can't compare against y.
					// If this happens, just swallow up the exception and continue comparing.
				}
			}

			// Dictionaries?
			var dictionariesEqual = CheckIfDictionariesAreEqual(x, y);
			if (dictionariesEqual.HasValue)
				return dictionariesEqual.GetValueOrDefault();

			// Sets?
			var setsEqual = CheckIfSetsAreEqual(x, y, typeInfo);
			if (setsEqual.HasValue)
				return setsEqual.GetValueOrDefault();

			// Enumerable?
			var enumerablesEqual = CheckIfEnumerablesAreEqual(x, y, out mismatchIndex);
			if (enumerablesEqual.HasValue)
			{
				if (!enumerablesEqual.GetValueOrDefault())
				{
					return false;
				}

				// Array.GetEnumerator() flattens out the array, ignoring array ranks and lengths
				var xArray = x as Array;
				var yArray = y as Array;
				if (xArray != null && yArray != null)
				{
					// new object[2,1] != new object[2]
					if (xArray.Rank != yArray.Rank)
						return false;

					// new object[2,1] != new object[1,2]
					for (var i = 0; i < xArray.Rank; i++)
						if (xArray.GetLength(i) != yArray.GetLength(i))
							return false;
				}

				return true;
			}

			// Implements IStructuralEquatable?
			var structuralEquatable = x as IStructuralEquatable;
			if (structuralEquatable != null && structuralEquatable.Equals(y, new TypeErasedEqualityComparer(innerComparerFactory())))
				return true;

#if !XUNIT_AOT
			// Implements IEquatable<typeof(y)>?
			// Not supported on AOT due to MakeGenericType
			var iequatableY = typeof(IEquatable<>).MakeGenericType(y.GetType()).GetTypeInfo();
			if (iequatableY.IsAssignableFrom(x.GetType().GetTypeInfo()))
			{
				var equalsMethod = iequatableY.GetDeclaredMethod(nameof(IEquatable<T>.Equals));
				if (equalsMethod == null)
					return false;

#if XUNIT_NULLABLE
				return equalsMethod.Invoke(x, new object[] { y }) is true;
#else
				return (bool)equalsMethod.Invoke(x, new object[] { y });
#endif
			}
#endif

#if !XUNIT_AOT
			// Implements IComparable<typeof(y)>?
			// Not supported on AOT due to MakeGenericType
			var icomparableY = typeof(IComparable<>).MakeGenericType(y.GetType()).GetTypeInfo();
			if (icomparableY.IsAssignableFrom(x.GetType().GetTypeInfo()))
			{
				var compareToMethod = icomparableY.GetDeclaredMethod(nameof(IComparable<T>.CompareTo));
				if (compareToMethod == null)
					return false;

				try
				{
#if XUNIT_NULLABLE
					return compareToMethod.Invoke(x, new object[] { y }) is 0;
#else
					return (int)compareToMethod.Invoke(x, new object[] { y }) == 0;
#endif
				}
				catch
				{
					// Some implementations of IComparable.CompareTo throw exceptions in
					// certain situations, such as if x can't compare against y.
					// If this happens, just swallow up the exception and continue comparing.
				}
			}
#endif

			// Last case, rely on object.Equals
			return object.Equals(x, y);
		}

		bool? CheckIfEnumerablesAreEqual(
#if XUNIT_NULLABLE
			[AllowNull] T x,
			[AllowNull] T y,
#else
			T x,
			T y,
#endif
			out int? mismatchIndex)
		{
			mismatchIndex = null;

			var enumerableX = x as IEnumerable;
			var enumerableY = y as IEnumerable;

			if (enumerableX == null || enumerableY == null)
				return null;

			var enumeratorX = default(IEnumerator);
			var enumeratorY = default(IEnumerator);

			try
			{
				enumeratorX = enumerableX.GetEnumerator();
				enumeratorY = enumerableY.GetEnumerator();
				var equalityComparer = innerComparerFactory();

				mismatchIndex = 0;

				while (true)
				{
					var hasNextX = enumeratorX.MoveNext();
					var hasNextY = enumeratorY.MoveNext();

					if (!hasNextX || !hasNextY)
					{
						if (hasNextX == hasNextY)
						{
							mismatchIndex = null;
							return true;
						}

						return false;
					}

					if (!equalityComparer.Equals(enumeratorX.Current, enumeratorY.Current))
						return false;

					mismatchIndex++;
				}
			}
			finally
			{
				var asDisposable = enumeratorX as IDisposable;
				if (asDisposable != null)
					asDisposable.Dispose();
				asDisposable = enumeratorY as IDisposable;
				if (asDisposable != null)
					asDisposable.Dispose();
			}
		}

		bool? CheckIfDictionariesAreEqual(
#if XUNIT_NULLABLE
			[AllowNull] T x,
			[AllowNull] T y)
#else
			T x,
			T y)
#endif
		{
			var dictionaryX = x as IDictionary;
			var dictionaryY = y as IDictionary;

			if (dictionaryX == null || dictionaryY == null)
				return null;

			if (dictionaryX.Count != dictionaryY.Count)
				return false;

			var equalityComparer = innerComparerFactory();
			var dictionaryYKeys = new HashSet<object>(dictionaryY.Keys.Cast<object>());

			foreach (var key in dictionaryX.Keys.Cast<object>())
			{
				if (!dictionaryYKeys.Contains(key))
					return false;

				var valueX = dictionaryX[key];
				var valueY = dictionaryY[key];

				if (!equalityComparer.Equals(valueX, valueY))
					return false;

				dictionaryYKeys.Remove(key);
			}

			return dictionaryYKeys.Count == 0;
		}

#if !XUNIT_AOT
#if XUNIT_NULLABLE
		static MethodInfo? s_compareTypedSetsMethod;
#else
		static MethodInfo s_compareTypedSetsMethod;
#endif
#endif // !XUNIT_AOT

		bool? CheckIfSetsAreEqual(
#if XUNIT_NULLABLE
			[AllowNull] T x,
			[AllowNull] T y,
#else
			T x,
			T y,
#endif
			[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TypeInfo typeInfo)
		{
			if (!IsSet(typeInfo))
				return null;

			var enumX = x as IEnumerable;
			var enumY = y as IEnumerable;
			if (enumX == null || enumY == null)
				return null;

#if XUNIT_AOT
			// Can't use MakeGenericType in AOT
			return CompareUntypedSets(enumX, enumY);
#else
			Type elementType;
			if (typeof(T).GenericTypeArguments.Length != 1)
				elementType = typeof(object);
			else
				elementType = typeof(T).GenericTypeArguments[0];

			if (s_compareTypedSetsMethod == null)
			{
				s_compareTypedSetsMethod = GetType().GetTypeInfo().GetDeclaredMethod(nameof(CompareTypedSets));
				if (s_compareTypedSetsMethod == null)
					return false;
			}

			var method = s_compareTypedSetsMethod.MakeGenericMethod(new Type[] { elementType });
#if XUNIT_NULLABLE
			return method.Invoke(this, new object[] { enumX, enumY }) is true;
#else
			return (bool)method.Invoke(this, new object[] { enumX, enumY });
#endif // XUNIT_NULLABLE
#endif // XUNIT_AOT
		}

		static bool CompareUntypedSets(
			IEnumerable enumX,
			IEnumerable enumY)
		{
			var setX = new HashSet<object>(enumX.Cast<object>());
			var setY = new HashSet<object>(enumY.Cast<object>());

			return setX.SetEquals(setY);
		}

		bool CompareTypedSets<R>(
			IEnumerable enumX,
			IEnumerable enumY)
		{
			var setX = new HashSet<R>(enumX.Cast<R>());
			var setY = new HashSet<R>(enumY.Cast<R>());

			return setX.SetEquals(setY);
		}

		bool IsSet(
			[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TypeInfo typeInfo) =>
			typeInfo
				.ImplementedInterfaces
				.Select(i => i.GetTypeInfo())
				.Where(ti => ti.IsGenericType)
				.Select(ti => ti.GetGenericTypeDefinition())
				.Contains(typeof(ISet<>).GetGenericTypeDefinition());

		/// <inheritdoc/>
		public int GetHashCode(T obj)
		{
			throw new NotImplementedException();
		}

		private class TypeErasedEqualityComparer : IEqualityComparer
		{
			readonly IEqualityComparer innerComparer;

			public TypeErasedEqualityComparer(IEqualityComparer innerComparer)
			{
				this.innerComparer = innerComparer;
			}

#if !XUNIT_AOT
#if XUNIT_NULLABLE
			static MethodInfo? s_equalsMethod;
#else
			static MethodInfo s_equalsMethod;
#endif
#endif // XUNIT_AOT

			public new bool Equals(
#if XUNIT_NULLABLE
				object? x,
				object? y)
#else
				object x,
				object y)
#endif
			{
				if (x == null)
					return y == null;
				if (y == null)
					return false;

#if XUNIT_AOT
				// Can't use MakeGenericType, have to use object
				return EqualsGeneric(x, y);
#else
				// Delegate checking of whether two objects are equal to AssertEqualityComparer.
				// To get the best result out of AssertEqualityComparer, we attempt to specialize the
				// comparer for the objects that we are checking.
				// If the objects are the same, great! If not, assume they are objects.
				// This is more naive than the C# compiler which tries to see if they share any interfaces
				// etc. but that's likely overkill here as AssertEqualityComparer<object> is smart enough.
				Type objectType = x.GetType() == y.GetType() ? x.GetType() : typeof(object);

				// Lazily initialize and cache the EqualsGeneric<U> method.
				if (s_equalsMethod == null)
				{
					s_equalsMethod = typeof(TypeErasedEqualityComparer).GetTypeInfo().GetDeclaredMethod(nameof(EqualsGeneric));
					if (s_equalsMethod == null)
						return false;
				}

#if XUNIT_NULLABLE
				return s_equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y }) is true;
#else
				return (bool)s_equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y });
#endif // XUNIT_NULLABLE
#endif // XUNIT_AOT
			}

			bool EqualsGeneric<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] U>(
				U x,
				U y) =>
					new AssertEqualityComparer<U>(innerComparer: innerComparer).Equals(x, y);

			public int GetHashCode(object obj)
			{
				throw new NotImplementedException();
			}
		}
	}
}
