// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;

using Analyzer.Utilities.Extensions;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Testing.Platform;

using MSTest.Analyzers.Helpers;

namespace MSTest.Analyzers;

/// <summary>
/// MSTEST0042: <inheritdoc cref="Resources.DuplicateDataRowTitle"/>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DuplicateDataRowAnalyzer : DiagnosticAnalyzer
{
    internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
        DiagnosticIds.DuplicateDataRowRuleId,
        new LocalizableResourceString(nameof(Resources.DuplicateDataRowTitle), Resources.ResourceManager, typeof(Resources)),
        new LocalizableResourceString(nameof(Resources.DuplicateDataRowMessageFormat), Resources.ResourceManager, typeof(Resources)),
        null,
        Category.Usage,
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    /// <inheritdoc />
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

    /// <inheritdoc />
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        context.RegisterCompilationStartAction(context =>
        {
            if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute, out INamedTypeSymbol? dataRowAttribute))
            {
                context.RegisterSymbolAction(
                    context => AnalyzeSymbol(context, dataRowAttribute),
                    SymbolKind.Method);
            }
        });
    }

    private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol dataRowAttribute)
    {
        var methodSymbol = (IMethodSymbol)context.Symbol;
        var dataRowArguments = new Dictionary<ImmutableArray<TypedConstant>, int>(TypedConstantArrayComparer.Instance);

        ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
        for (int i = 0; i < attributes.Length; i++)
        {
            AttributeData attribute = attributes[i];
            if (!dataRowAttribute.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default))
            {
                continue;
            }

            if (dataRowArguments.TryGetValue(attribute.ConstructorArguments, out int existingIndex) &&
                attribute.ApplicationSyntaxReference is not null)
            {
                context.ReportDiagnostic(attribute.ApplicationSyntaxReference.CreateDiagnostic(Rule, context.CancellationToken, existingIndex, i));
                continue;
            }

            dataRowArguments[attribute.ConstructorArguments] = i;
        }
    }

    private sealed class TypedConstantArrayComparer : IEqualityComparer<ImmutableArray<TypedConstant>>
    {
        public static TypedConstantArrayComparer Instance { get; } = new();

        public bool Equals(ImmutableArray<TypedConstant> x, ImmutableArray<TypedConstant> y)
        {
            if (x.Length != y.Length)
            {
                return false;
            }

            for (int i = 0; i < x.Length; i++)
            {
                if (!AreTypedConstantEquals(x[i], y[i]))
                {
                    return false;
                }
            }

            return true;
        }

        private static bool AreTypedConstantEquals(TypedConstant typedConstant1, TypedConstant typedConstant2)
        {
            // If the Kind doesn't match or the Type doesn't match, they are not equal.
            if (typedConstant1.Kind != typedConstant2.Kind ||
                !SymbolEqualityComparer.Default.Equals(typedConstant1.Type, typedConstant2.Type))
            {
                return false;
            }

            // If IsNull is true and the Kind is array, Values will return default(ImmutableArray<TypedConstant>), not empty.
            // To avoid dealing with that, we do the quick IsNull checks first.
            // If both are nulls, then we are good, everything is equal.
            if (typedConstant1.IsNull && typedConstant2.IsNull)
            {
                return true;
            }

            // If only one is null, then we are not equal.
            if (typedConstant1.IsNull || typedConstant2.IsNull)
            {
                return false;
            }

            // If the kind is array (at this point we know both have the same Kind), we compare Values.
            // Accessing `Value` property for arrays will throw so we need to have explicit check to decide whether
            // we compare `Value` or `Values`.
            if (typedConstant1.Kind == TypedConstantKind.Array)
            {
                return TypedConstantArrayComparer.Instance.Equals(typedConstant1.Values, typedConstant2.Values);
            }

            if (typedConstant1.Kind == TypedConstantKind.Primitive)
            {
                // object.Equals(float.NegativeZero, 0.0f) will return true.
                // But we don't want to consider it as "equal" as the test case can yield different results.
                // Behavior difference between zero and negative zero can be observed via BitConverter or ToString.
                if (typedConstant1.Value is float float1 && typedConstant2.Value is float float2)
                {
                    // BitConverter.SingleToInt32Bits isn't available on netstandard2.0, so we use BitConverter.GetBytes instead.
                    return BitConverter.GetBytes(float1).SequenceEqual(BitConverter.GetBytes(float2));
                }
                else if (typedConstant1.Value is double double1 && typedConstant2.Value is double double2)
                {
                    return BitConverter.DoubleToInt64Bits(double1) == BitConverter.DoubleToInt64Bits(double2);
                }
            }

            // At this point, the type is matching and the kind is matching and is not array.
            return object.Equals(typedConstant1.Value, typedConstant2.Value);
        }

        public int GetHashCode(ImmutableArray<TypedConstant> obj)
        {
            var hashCode = default(RoslynHashCode);
            foreach (TypedConstant typedConstant in obj)
            {
                hashCode.Add(typedConstant.Kind);
                hashCode.Add(SymbolEqualityComparer.Default.GetHashCode(typedConstant.Type));

                if (!typedConstant.IsNull)
                {
                    if (typedConstant.Kind == TypedConstantKind.Array)
                    {
                        hashCode.Add(GetHashCode(typedConstant.Values));
                    }
                    else
                    {
                        AddTypedConstantValueHash(ref hashCode, typedConstant);
                    }
                }
            }

            return hashCode.ToHashCode();
        }

        private static void AddTypedConstantValueHash(ref RoslynHashCode hashCode, TypedConstant typedConstant)
        {
            if (typedConstant.Kind == TypedConstantKind.Primitive)
            {
                switch (typedConstant.Type?.SpecialType)
                {
                    case SpecialType.System_Single:
                        // BitConverter.SingleToInt32Bits isn't available on netstandard2.0, so we use BitConverter.GetBytes instead.
                        hashCode.Add(BitConverter.ToInt32(BitConverter.GetBytes((float)typedConstant.Value!), 0));
                        return;

                    case SpecialType.System_Double:
                        hashCode.Add(BitConverter.DoubleToInt64Bits((double)typedConstant.Value!));
                        return;
                }
            }

            hashCode.Add(typedConstant.Value);
        }
    }
}
