﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="EqualityComparisonExtension.cs" company="Hobsons">
//   Copyright (c) 2012 Hobsons EMS Technology
//   
//   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 
//   files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 
//   modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 
//   Software is furnished to do so, subject to the following conditions:
//     
//   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//    
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 
//   WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
//   COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
//   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
// <summary>
//   Helper class to assist equality comparison.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace Hobsons.CommonLibraries.EqualityHelper
{
    using System;
    using System.Collections;

    /// <summary>
    /// Helper class to assist equality comparison.
    /// </summary>
    /// <remarks>
    /// This equality comparison extension does not handle self reference very well.
    /// </remarks>
    public static class EqualityComparisonExtension
    {
        #region Public Methods and Operators

        /// <summary>
        /// Extension method to compare equality of two objects.
        /// </summary>
        /// <param name="object1">
        /// The first object.
        /// </param>
        /// <param name="object2">
        /// The second object.
        /// </param>
        /// <returns>
        /// True, if objects are equal; Otherwise, false.
        /// </returns>
        public static bool CompareEqualityWith(this object object1, object object2)
        {
            if (ReferenceEquals(object1, object2))
            {
                return true;
            }

            if (ReferenceEquals(object1, null) ^ ReferenceEquals(object2, null))
            {
                return false;
            }

            if (object1 != null)
            {
                var object1Type = object1.GetType();
                var object2Type = object2.GetType();

                if (object1Type != object2Type)
                {
                    return false;
                }

                if (IsTypeOfTimespan(object1Type))
                {
                    return CompareTimespan(object1, object2);
                }

                if (IsTypeOfIList(object1Type))
                {
                    return CompareIList(object1, object2);
                }

                if (IsTypeOfIDictionary(object1Type))
                {
                    return CompareIDictionary(object1, object2);
                }

                // if none of the above special case, we use default equals
                return object1.Equals(object2);
            }

            return false;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Compare <see cref="IDictionary"/> objects.
        /// </summary>
        /// <param name="object1">
        /// The first object to compare.
        /// </param>
        /// <param name="object2">
        /// The second object to compare.
        /// </param>
        /// <returns>
        /// True, if keys and values of two dictionary are sequential equal. Otherwise, false.
        /// </returns>
        private static bool CompareIDictionary(object object1, object object2)
        {
            var enumerableObject1 = object1 as IDictionary;
            var enumerableObject2 = object2 as IDictionary;

            if (enumerableObject1 == null)
            {
                throw new InvalidCastException("object1 is not assignable to IDictionary");
            }

            if (enumerableObject2 == null)
            {
                throw new InvalidCastException("object2 is not assignable to IDictionary");
            }

            if (enumerableObject1.Count != enumerableObject2.Count)
            {
                return false;
            }

            var enumerator1 = enumerableObject1.GetEnumerator();
            var enumerator2 = enumerableObject2.GetEnumerator();

            while (enumerator1.MoveNext() && enumerator2.MoveNext())
            {
                var keyCompareResult = enumerator1.Key.CompareEqualityWith(enumerator2.Key);

                var valueCompareResult = enumerator1.Value.CompareEqualityWith(enumerator2.Value);

                if (!keyCompareResult || !valueCompareResult)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Compare <see cref="IList"/> objects.
        /// </summary>
        /// <param name="object1">
        /// The first object to compare.
        /// </param>
        /// <param name="object2">
        /// The second object to compare.
        /// </param>
        /// <returns>
        /// True, if elements of two lists are sequential equal. Otherwise, false.
        /// </returns>
        private static bool CompareIList(object object1, object object2)
        {
            var enumerableObject1 = object1 as IList;
            var enumerableObject2 = object2 as IList;

            if (enumerableObject1 == null)
            {
                throw new InvalidCastException("object1 is not assignable to IList");
            }

            if (enumerableObject2 == null)
            {
                throw new InvalidCastException("object2 is not assignable to IList");
            }

            if (enumerableObject1.Count != enumerableObject2.Count)
            {
                return false;
            }

            var enumerator1 = enumerableObject1.GetEnumerator();
            var enumerator2 = enumerableObject2.GetEnumerator();

            while (enumerator1.MoveNext() && enumerator2.MoveNext())
            {
                var result = enumerator1.Current.CompareEqualityWith(enumerator2.Current);

                if (!result)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Compare <see cref="TimeSpan"/> objects.
        /// </summary>
        /// <param name="object1">
        /// The first object to compare.
        /// </param>
        /// <param name="object2">
        /// The second object to compare.
        /// </param>
        /// <returns>
        /// True, if ticks of two time span objects are the same. Otherwise, false.
        /// </returns>
        private static bool CompareTimespan(object object1, object object2)
        {
            return ((TimeSpan)object1).Ticks == ((TimeSpan)object2).Ticks;
        }

        /// <summary>
        /// Checks whether type is <see cref="IDictionary"/>.
        /// </summary>
        /// <param name="type">
        /// The object type.
        /// </param>
        /// <returns>
        /// True, if it is; Otherwise, false.
        /// </returns>
        private static bool IsTypeOfIDictionary(Type type)
        {
            return typeof(IDictionary).IsAssignableFrom(type);
        }

        /// <summary>
        /// Checks whether <see cref="IList"/> is assignable from type.
        /// </summary>
        /// <param name="type">
        /// The object type.
        /// </param>
        /// <returns>
        /// True, if it is; Otherwise, false.
        /// </returns>
        private static bool IsTypeOfIList(Type type)
        {
            return typeof(IList).IsAssignableFrom(type);
        }

        /// <summary>
        /// Checks whether type is timespan.
        /// </summary>
        /// <param name="type">
        /// The object type.
        /// </param>
        /// <returns>
        /// True, if it is; Otherwise, false.
        /// </returns>
        private static bool IsTypeOfTimespan(Type type)
        {
            return type == typeof(TimeSpan);
        }

        #endregion
    }
}