// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Diagnostics;

namespace System.Data
{
    /// <summary>
    /// This class implements IEqualityComparer using value based semantics
    /// when comparing DataRows.
    /// </summary>
    public static class DataRowComparer
    {
        /// <summary>
        /// Gets the singleton instance of the data row comparer.
        /// </summary>
        public static DataRowComparer<DataRow> Default { get { return DataRowComparer<DataRow>.Default; } }

        internal static bool AreEqual(object a, object b)
        {
            if (ReferenceEquals(a, b))
            {
                // same reference or (null, null) or (DBNull.Value, DBNull.Value)
                return true;
            }
            if (ReferenceEquals(a, null) || ReferenceEquals(a, DBNull.Value) ||
                ReferenceEquals(b, null) || ReferenceEquals(b, DBNull.Value))
            {
                // (null, non-null) or (null, DBNull.Value) or vice versa
                return false;
            }
            return (a.Equals(b) || (a.GetType().IsArray && CompareArray((Array)a, b as Array)));
        }

        private static bool AreElementEqual(object a, object b)
        {
            if (ReferenceEquals(a, b))
            {
                // same reference or (null, null) or (DBNull.Value, DBNull.Value)
                return true;
            }
            if (ReferenceEquals(a, null) || ReferenceEquals(a, DBNull.Value) ||
                ReferenceEquals(b, null) || ReferenceEquals(b, DBNull.Value))
            {
                // (null, non-null) or (null, DBNull.Value) or vice versa
                return false;
            }
            return a.Equals(b);
        }

        private static bool CompareArray(Array a, Array b)
        {
            if ((null == b) ||
                (1 != a.Rank) ||
                (1 != b.Rank) ||
                (a.Length != b.Length))
            {
                // automatically consider array's with Rank>1 not-equal
                return false;
            }

            int index1 = a.GetLowerBound(0);
            int index2 = b.GetLowerBound(0);
            if (a.GetType() == b.GetType() && (0 == index1) && (0 == index2))
            {
                switch (Type.GetTypeCode(a.GetType().GetElementType()))
                {
                    case TypeCode.Byte:
                        return CompareEquatableArray((byte[])a, (byte[])b);
                    case TypeCode.Int16:
                        return CompareEquatableArray((short[])a, (short[])b);
                    case TypeCode.Int32:
                        return CompareEquatableArray((int[])a, (int[])b);
                    case TypeCode.Int64:
                        return CompareEquatableArray((long[])a, (long[])b);
                    case TypeCode.String:
                        return CompareEquatableArray((string[])a, (string[])b);
                }
            }

            // Compare every element. But don't recurse if we have Array of array.
            int length = index1 + a.Length;
            for (; index1 < length; ++index1, ++index2)
            {
                if (!AreElementEqual(a.GetValue(index1), b.GetValue(index2)))
                {
                    return false;
                }
            }
            return true;
        }

        private static bool CompareEquatableArray<TElem>(TElem[] a, TElem[] b) where TElem : IEquatable<TElem>
        {
            Debug.Assert(!ReferenceEquals(a, b));
            Debug.Assert(a != null);
            Debug.Assert(b != null);
            Debug.Assert(a.Length == b.Length);

            for (int i = 0; i < a.Length; ++i)
            {
                if (!a[i].Equals(b[i]))
                {
                    return false;
                }
            }
            return true;
        }
    }

    /// <summary>
    /// This class implements IEqualityComparer using value based semantics
    /// when comparing DataRows.
    /// </summary>
    public sealed class DataRowComparer<TRow> : IEqualityComparer<TRow> where TRow : DataRow
    {
        /// <summary>
        /// Private constructor to prevent initialization outside of Default singleton instance.
        /// </summary>
        private DataRowComparer() { }

        private static DataRowComparer<TRow> s_instance = new DataRowComparer<TRow>();

        /// <summary>
        /// Gets the singleton instance of the data row comparer.
        /// </summary>
        public static DataRowComparer<TRow> Default { get { return s_instance; } }

        /// <summary>
        /// This method compares to DataRows by doing a column by column value based
        /// comparison.
        /// </summary>
        /// <param name="leftRow">The first input DataRow</param>
        /// <param name="rightRow">The second input DataRow</param>
        /// <returns>True if rows are equal, false if not.</returns>
        public bool Equals(TRow leftRow, TRow rightRow)
        {
            if (ReferenceEquals(leftRow, rightRow))
            {
                return true;
            }

            if (ReferenceEquals(leftRow, null) ||
                ReferenceEquals(rightRow, null))
            {
                return false;
            }

            if (leftRow.RowState == DataRowState.Deleted || rightRow.RowState == DataRowState.Deleted)
            {
                throw DataSetUtil.InvalidOperation(SR.DataSetLinq_CannotCompareDeletedRow);
            }

            int count = leftRow.Table.Columns.Count;
            if (count != rightRow.Table.Columns.Count)
            {
                return false;
            }

            for (int i = 0; i < count; ++i)
            {
                if (!DataRowComparer.AreEqual(leftRow[i], rightRow[i]))
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// This method retrieves a hash code for the source row.
        /// </summary>
        /// <param name="row">The source DataRow</param>
        /// <returns>HashCode for row based on values in the row.</returns>
        public int GetHashCode(TRow row)
        {
            DataSetUtil.CheckArgumentNull(row, nameof(row));

            if (row.RowState == DataRowState.Deleted)
            {
                throw DataSetUtil.InvalidOperation(SR.DataSetLinq_CannotCompareDeletedRow);
            }

            int hash = 0;
            Debug.Assert(row.Table != null);
            if (row.Table.Columns.Count > 0)
            {
                // if the row has at least one column, then use the first column value
                object value = row[0];

                Type valueType = value.GetType();
                if (valueType.IsArray)
                {
                    Array array = value as Array;

                    if (array.Rank > 1)
                    {
                        hash = value.GetHashCode();
                    }
                    else if (array.Length > 0)
                    {
                        hash = array.GetValue(array.GetLowerBound(0)).GetHashCode();
                    }
                }
                else
                {
                    ValueType vt = value as ValueType;

                    // have to unbox value types.
                    if (vt != null)
                    {
                        hash = vt.GetHashCode();
                    }
                    else
                    {
                        hash = value.GetHashCode();
                    }
                }
            }

            // if table has no columns, the hash code is 0
            return hash;
        }
    }
}
