// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.Collections
{
    /// <summary>
    /// This is a custom string comparer that has three advantages over the regular
    /// string comparer:
    /// 1) It can generate hash codes and perform equivalence operations on parts of a string rather than a whole
    /// 2) It uses "unsafe" pointers to maximize performance of those operations
    /// 3) It takes advantage of limitations on MSBuild Property/Item names to cheaply do case insensitive comparison.
    /// </summary>
    [Serializable]
    internal class MSBuildNameIgnoreCaseComparer : IConstrainedEqualityComparer<string>, IEqualityComparer<string>
    {
        /// <summary>
        /// The processor architecture on which we are running, but default it will be x86
        /// </summary>
        private static readonly NativeMethodsShared.ProcessorArchitectures s_runningProcessorArchitecture = NativeMethodsShared.ProcessorArchitecture;

        /// <summary>
        /// The default immutable comparer instance.
        /// </summary>
        internal static MSBuildNameIgnoreCaseComparer Default { get; } = new MSBuildNameIgnoreCaseComparer();

        public bool Equals(string x, string y)
        {
            return Equals(x, y, 0, y?.Length ?? 0);
        }

        public int GetHashCode(string obj)
        {
            return GetHashCode(obj, 0, obj?.Length ?? 0);
        }

        /// <summary>
        /// Performs the "Equals" operation on two MSBuild property, item or metadata names
        /// </summary>
        public bool Equals(string compareToString, string constrainedString, int start, int lengthToCompare)
        {
            if (lengthToCompare < 0)
            {
                ErrorUtilities.ThrowInternalError("Invalid lengthToCompare '{0}' {1} {2}", constrainedString, start, lengthToCompare);
            }

            if (start < 0 || start > (constrainedString?.Length ?? 0) - lengthToCompare)
            {
                ErrorUtilities.ThrowInternalError("Invalid start '{0}' {1} {2}", constrainedString, start, lengthToCompare);
            }

            if (ReferenceEquals(compareToString, constrainedString))
            {
                return true;
            }

            if (compareToString == null || constrainedString == null)
            {
                return false;
            }

            if (lengthToCompare != compareToString.Length)
            {
                return false;
            }

            if ((s_runningProcessorArchitecture != NativeMethodsShared.ProcessorArchitectures.IA64)
                && (s_runningProcessorArchitecture != NativeMethodsShared.ProcessorArchitectures.ARM))
            {
                // The use of unsafe here is quite a bit faster than the regular
                // mechanism in the BCL. This is because we can make assumptions
                // about the characters that are within the strings being compared
                // i.e. they are valid MSBuild property, item and metadata names
                unsafe
                {
                    fixed (char* px = compareToString)
                    {
                        fixed (char* py = constrainedString)
                        {
                            for (int i = 0; i < compareToString.Length; i++)
                            {
                                int chx = px[i];
                                int chy = py[i + start];
                                chx &= 0x00DF; // Extract the uppercase character
                                chy &= 0x00DF; // Extract the uppercase character

                                if (chx != chy)
                                {
                                    return false;
                                }
                            }
                        }
                    }
                }
            }
            else
            {
                return String.Compare(compareToString, 0, constrainedString, start, lengthToCompare, StringComparison.OrdinalIgnoreCase) == 0;
            }

            return true;
        }

        /// <summary>
        /// Getting a case insensitive hash code for the msbuild property, item or metadata name
        /// </summary>
        public int GetHashCode(string obj, int start, int length)
        {
            if (obj == null)
            {
                return 0; // per BCL convention
            }

            if ((s_runningProcessorArchitecture != NativeMethodsShared.ProcessorArchitectures.IA64)
                && (s_runningProcessorArchitecture != NativeMethodsShared.ProcessorArchitectures.ARM))
            {
                unsafe
                {
                    // This algorithm is based on the 32bit version from the CLR's string::GetHashCode
                    fixed (char* src = obj)
                    {
                        int hash1 = (5381 << 16) + 5381;

                        int hash2 = hash1;

                        char* src2 = src + start;
                        var pint = (int*)src2;

                        while (length > 0)
                        {
                            // We're only interested in uppercase ASCII characters
                            int val = pint[0] & 0x00DF00DF;

                            // When we reach the end of the string, we need to
                            // stop short when gathering our data to compute the
                            // hash code - we are only interested in the data within
                            // the string, and not the null terminator etc.
                            if (length == 1)
                            {
                                if (BitConverter.IsLittleEndian)
                                {
                                    val &= 0xFFFF;
                                }
                                else
                                {
                                    val &= unchecked((int)0xFFFF0000);
                                }
                            }

                            hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ val;
                            if (length <= 2)
                            {
                                break;
                            }

                            // Once again we're only interested in the uppercase ASCII characters
                            val = pint[1] & 0x00DF00DF;
                            if (length == 3)
                            {
                                if (BitConverter.IsLittleEndian)
                                {
                                    val &= 0xFFFF;
                                }
                                else
                                {
                                    val &= unchecked((int)0xFFFF0000);
                                }
                            }

                            hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ val;
                            pint += 2;
                            length -= 4;
                        }

                        return hash1 + (hash2 * 1566083941);
                    }
                }
            }
            else
            {
                return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Substring(start, length));
            }
        }
    }
}
