﻿// 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;
using System.Diagnostics;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Compares assembly identities. 
    /// Derived types may implement platform specific unification and portability policies.
    /// </summary>
    public class AssemblyIdentityComparer
    {
        public static AssemblyIdentityComparer Default { get; } = new AssemblyIdentityComparer();

        public static StringComparer SimpleNameComparer
        {
            get { return StringComparer.OrdinalIgnoreCase; }
        }

        public static StringComparer CultureComparer
        {
            get { return StringComparer.OrdinalIgnoreCase; }
        }

        internal AssemblyIdentityComparer()
        {
        }

        /// <summary>
        /// A set of possible outcomes of <see cref="AssemblyIdentity"/> comparison.
        /// </summary>
        public enum ComparisonResult
        {
            /// <summary>
            /// Reference doesn't match definition.
            /// </summary>
            NotEquivalent = 0,

            /// <summary>
            /// Strongly named reference matches strongly named definition (strong identity is identity with public key or token),
            /// Or weak reference matches weak definition.
            /// </summary>
            Equivalent = 1,

            /// <summary>
            /// Reference matches definition except for version (reference version is lower or higher than definition version).
            /// </summary>
            EquivalentIgnoringVersion = 2
        }

        /// <summary>
        /// Compares assembly reference name (possibly partial) with definition identity.
        /// </summary>
        /// <param name="referenceDisplayName">Partial or full assembly display name.</param>
        /// <param name="definition">Full assembly display name.</param>
        /// <returns>True if the reference name matches the definition identity.</returns>
        public bool ReferenceMatchesDefinition(string referenceDisplayName, AssemblyIdentity definition)
        {
            return Compare(reference: null, referenceDisplayName, definition, unificationApplied: out _, ignoreVersion: false) != ComparisonResult.NotEquivalent;
        }

        /// <summary>
        /// Compares assembly reference identity with definition identity.
        /// </summary>
        /// <param name="reference">Reference assembly identity.</param>
        /// <param name="definition">Full assembly display name.</param>
        /// <returns>True if the reference identity matches the definition identity.</returns>
        public bool ReferenceMatchesDefinition(AssemblyIdentity reference, AssemblyIdentity definition)
        {
            return Compare(reference, referenceDisplayName: null, definition, unificationApplied: out _, ignoreVersion: false) != ComparisonResult.NotEquivalent;
        }

        /// <summary>
        /// Compares reference assembly identity with definition identity and returns their relationship.
        /// </summary>
        /// <param name="reference">Reference identity.</param>
        /// <param name="definition">Definition identity.</param>
        public ComparisonResult Compare(AssemblyIdentity reference, AssemblyIdentity definition)
        {
            return Compare(reference, referenceDisplayName: null, definition, unificationApplied: out _, ignoreVersion: true);
        }

        // internal for testing
        internal ComparisonResult Compare(AssemblyIdentity? reference, string? referenceDisplayName, AssemblyIdentity definition, out bool unificationApplied, bool ignoreVersion)
        {
            Debug.Assert((reference is not null) ^ (referenceDisplayName != null));
            unificationApplied = false;
            AssemblyIdentityParts parts;

            if (reference is not null)
            {
                // fast path
                bool? eq = TriviallyEquivalent(reference, definition);
                if (eq.HasValue)
                {
                    return eq.GetValueOrDefault() ? ComparisonResult.Equivalent : ComparisonResult.NotEquivalent;
                }

                parts = AssemblyIdentityParts.Name | AssemblyIdentityParts.Version | AssemblyIdentityParts.Culture | AssemblyIdentityParts.PublicKeyToken;
            }
            else
            {
                if (!AssemblyIdentity.TryParseDisplayName(referenceDisplayName!, out reference, out parts) ||
                    reference.ContentType != definition.ContentType)
                {
                    return ComparisonResult.NotEquivalent;
                }
            }

            Debug.Assert(reference.ContentType == definition.ContentType);

            bool isDefinitionFxAssembly;
            if (!ApplyUnificationPolicies(ref reference, ref definition, parts, out isDefinitionFxAssembly))
            {
                return ComparisonResult.NotEquivalent;
            }

            if (ReferenceEquals(reference, definition))
            {
                return ComparisonResult.Equivalent;
            }

            bool compareCulture = (parts & AssemblyIdentityParts.Culture) != 0;
            bool comparePublicKeyToken = (parts & AssemblyIdentityParts.PublicKeyOrToken) != 0;

            if (!definition.IsStrongName)
            {
                if (reference.IsStrongName)
                {
                    return ComparisonResult.NotEquivalent;
                }

                if (!AssemblyIdentity.IsFullName(parts))
                {
                    if (!SimpleNameComparer.Equals(reference.Name, definition.Name))
                    {
                        return ComparisonResult.NotEquivalent;
                    }

                    if (compareCulture && !CultureComparer.Equals(reference.CultureName, definition.CultureName))
                    {
                        return ComparisonResult.NotEquivalent;
                    }

                    // version is ignored

                    return ComparisonResult.Equivalent;
                }

                isDefinitionFxAssembly = false;
            }

            if (!SimpleNameComparer.Equals(reference.Name, definition.Name))
            {
                return ComparisonResult.NotEquivalent;
            }

            if (compareCulture && !CultureComparer.Equals(reference.CultureName, definition.CultureName))
            {
                return ComparisonResult.NotEquivalent;
            }

            if (comparePublicKeyToken && !AssemblyIdentity.KeysEqual(reference, definition))
            {
                return ComparisonResult.NotEquivalent;
            }

            bool hasSomeVersionParts = (parts & AssemblyIdentityParts.Version) != 0;
            bool hasPartialVersion = (parts & AssemblyIdentityParts.Version) != AssemblyIdentityParts.Version;

            // If any version parts were specified then compare the versions. The comparison fails if some version parts are missing.
            if (definition.IsStrongName &&
                hasSomeVersionParts &&
                (hasPartialVersion || reference.Version != definition.Version))
            {
                // Note:
                // System.Numerics.Vectors, Version=4.0 is an FX assembly
                // System.Numerics.Vectors, Version=4.1+ is not an FX assembly
                //
                // It seems like a bug in Fusion: it only determines whether the definition is an FX assembly 
                // and calculates the result based upon that, regardless of whether the reference is an FX assembly or not.
                // We do replicate the behavior.
                //
                // As a result unification is asymmetric when comparing the above identities.
                if (isDefinitionFxAssembly)
                {
                    unificationApplied = true;
                    return ComparisonResult.Equivalent;
                }

                if (ignoreVersion)
                {
                    return ComparisonResult.EquivalentIgnoringVersion;
                }

                return ComparisonResult.NotEquivalent;
            }

            return ComparisonResult.Equivalent;
        }

        private static bool? TriviallyEquivalent(AssemblyIdentity x, AssemblyIdentity y)
        {
            // Identities from different binding models never match.
            if (x.ContentType != y.ContentType)
            {
                return false;
            }

            // Can't compare if identity might get retargeted.
            if (x.IsRetargetable || y.IsRetargetable)
            {
                return null;
            }

            return AssemblyIdentity.MemberwiseEqual(x, y);
        }

        internal virtual bool ApplyUnificationPolicies(ref AssemblyIdentity reference, ref AssemblyIdentity definition, AssemblyIdentityParts referenceParts, out bool isDefinitionFxAssembly)
        {
            isDefinitionFxAssembly = false;
            return true;
        }
    }
}

