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

namespace ExpressionsParser {

	/// <summary>
	/// Generic "by value" comparer that uses IComparable and can compare arrays/lists.
	/// </summary>
	public class ValueComparer : IComparer, IComparer<object>, IValueComparer {

		internal readonly static ValueComparer _Instance = new ValueComparer();
		
		public static IValueComparer Instance {
			get {
				return _Instance;
			}
		}

		/// <summary>
		/// Gets or sets format provider used for Convert.ChangeType (InvariantCulture by default).
		/// </summary>
		public IFormatProvider FormatProvider { get; set; }

		/// <summary>
		/// Determines how ValueComparer handles comparison exceptions (by default is false: convert exceptions are thrown).
		/// </summary>
		public bool SuppressErrors { get; set; }

		/// <summary>
		/// Determines how ValueComparer handles comparison with nulls (default is "MinValue" mode).
		/// </summary>
		public NullComparisonMode NullComparison { get; set; }

		public ValueComparer() {
			FormatProvider = System.Globalization.CultureInfo.InvariantCulture;
			SuppressErrors = false;
		}

		private bool IsAssignableFrom(Type a, Type b) {
			#if NET40
			return a.IsAssignableFrom(b);
			#else
			return a.GetTypeInfo().IsAssignableFrom(b.GetTypeInfo() );
			#endif
		}

		int IComparer.Compare(object a, object b) {
			var res = CompareInternal(a, b);
			if (!res.HasValue)
				throw new ArgumentException(string.Format("Cannot compare {0} and {1}", a.GetType(), b.GetType()));
			return res.Value;
		}

		int IComparer<object>.Compare(object a, object b) {
			return ((IComparer)this).Compare(a, b);
		}

		public int? Compare(object a, object b) {
			try {
				if (NullComparison == NullComparisonMode.Sql)
					if (a == null || b == null)
						return null;
				return CompareInternal(a, b);
			} catch {
				if (SuppressErrors)
					return null;
				throw;
			}
		}

		protected virtual int? CompareInternal(object a, object b) {
			if (a == null && b == null)
				return 0;
			if (a == null && b != null)
				return -1;
			if (a != null && b == null)
				return 1;

			if ((a is IList) && (b is IList)) {
				IList aList = (IList)a;
				IList bList = (IList)b;
				if (aList.Count < bList.Count)
					return -1;
				if (aList.Count > bList.Count)
					return +1;
				for (int i = 0; i < aList.Count; i++) {
					int? r = Compare(aList[i], bList[i]);
					if (!r.HasValue || r != 0)
						return r;
				}
				// lists are equal
				return 0;
			}
			// test for quick compare if a type is assignable from b
			if (a is IComparable) {
				var aComp = (IComparable)a;
				// quick compare if types are fully compatible
				if (IsAssignableFrom( a.GetType(), b.GetType() ))
					return aComp.CompareTo(b);
			}
			if (b is IComparable) {
				var bComp = (IComparable)b;
				// quick compare if types are fully compatible
				if (IsAssignableFrom( b.GetType(), a.GetType() ))
					return -bComp.CompareTo(a);
			}

			// try to convert b to a and then compare
			if (a is IComparable) {
				var aComp = (IComparable)a;
				var bConverted = Convert.ChangeType(b, a.GetType(), FormatProvider);
				return aComp.CompareTo(bConverted);
			}
			// try to convert a to b and then compare
			if (b is IComparable) {
				var bComp = (IComparable)b;
				var aConverted =  Convert.ChangeType(a, b.GetType(), FormatProvider);
				return -bComp.CompareTo(aConverted);
			}

			return null;
		}

		public enum NullComparisonMode {

			/// <summary>
			/// Null compared as "MinValue" (affects less-than and greater-than comparisons). 
			/// </summary>
			/// <remarks>This is default behaviour expected for <see cref="IComparer"/> and described in MSDN.</remarks>
			MinValue = 0,

			/// <summary>
			/// Null cannot be compared to any other value (even if it is null). This is SQL-like nulls handling.
			/// </summary>
			Sql = 1,
		}

	}

}
