// 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.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.GenerateFromMembers;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PickMembers;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.GenerateEqualsAndGetHashCodeFromMembers;

using static GenerateFromMembersHelpers;

[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic,
    Name = PredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers), Shared]
[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers,
                Before = PredefinedCodeRefactoringProviderNames.AddConstructorParametersFromMembers)]
[SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")]
internal sealed partial class GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider(IPickMembersService? pickMembersService)
    : CodeRefactoringProvider
{
    public const string GenerateOperatorsId = nameof(GenerateOperatorsId);
    public const string ImplementIEquatableId = nameof(ImplementIEquatableId);

    private const string EqualsName = nameof(object.Equals);
    private const string GetHashCodeName = nameof(object.GetHashCode);

    private readonly IPickMembersService? _pickMembersService_forTestingPurposes = pickMembersService;

    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider()
        : this(pickMembersService: null)
    {
    }

    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, textSpan, cancellationToken) = context;
        if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles)
        {
            return;
        }

        var actions = await GenerateEqualsAndGetHashCodeFromMembersAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
        context.RegisterRefactorings(actions);

        if (actions.IsDefaultOrEmpty && textSpan.IsEmpty)
        {
            await HandleNonSelectionAsync(context).ConfigureAwait(false);
        }
    }

    private async Task HandleNonSelectionAsync(CodeRefactoringContext context)
    {
        var (document, textSpan, cancellationToken) = context;

        var helpers = document.GetRequiredLanguageService<IRefactoringHelpersService>();
        var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

        // We offer the refactoring when the user is either on the header of a class/struct,
        // or if they're between any members of a class/struct and are on a blank line.
        if (!helpers.IsOnTypeHeader(root, textSpan.Start, out var typeDeclaration) &&
            !helpers.IsBetweenTypeMembers(sourceText, root, textSpan.Start, out typeDeclaration))
        {
            return;
        }

        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

        // Only supported on classes/structs.
        var containingType = semanticModel.GetDeclaredSymbol(typeDeclaration) as INamedTypeSymbol;
        if (containingType?.TypeKind is not TypeKind.Class and not TypeKind.Struct)
            return;

        // No overrides in static classes.
        if (containingType.IsStatic)
            return;

        // Find all the possible instance fields/properties.  If there are any, then
        // show a dialog to the user to select the ones they want.
        var viableMembers = containingType
            .GetBaseTypesAndThis()
            .Reverse()
            .SelectAccessibleMembers<ISymbol>(containingType)
            .WhereAsArray(IsReadableInstanceFieldOrProperty);

        if (viableMembers.Length == 0)
            return;

        GetExistingMemberInfo(
            containingType, out var hasEquals, out var hasGetHashCode);

        var globalOptions = document.Project.Solution.Services.GetService<ILegacyGlobalOptionsWorkspaceService>();
        if (globalOptions == null)
            return;

        var actions = await CreateActionsAsync(
            document, typeDeclaration, containingType, viableMembers,
            hasEquals, hasGetHashCode, withDialog: true, globalOptions, cancellationToken).ConfigureAwait(false);

        context.RegisterRefactorings(actions, textSpan);
    }

    private static bool HasOperators(INamedTypeSymbol containingType)
        => HasOperator(containingType, WellKnownMemberNames.EqualityOperatorName) ||
           HasOperator(containingType, WellKnownMemberNames.InequalityOperatorName);

    private static bool HasOperator(INamedTypeSymbol containingType, string operatorName)
        => containingType.GetMembers(operatorName)
                         .OfType<IMethodSymbol>()
                         .Any(m => m.MethodKind == MethodKind.UserDefinedOperator &&
                                   m.Parameters.Length == 2 &&
                                   containingType.Equals(m.Parameters[0].Type) &&
                                   containingType.Equals(m.Parameters[1].Type));

    private static bool CanImplementIEquatable(
        SemanticModel semanticModel, INamedTypeSymbol containingType,
        [NotNullWhen(true)] out INamedTypeSymbol? constructedType)
    {
        // A ref struct can never implement an interface, therefore never add IEquatable to the selection
        // options if the type is a ref struct.
        if (!containingType.IsRefLikeType)
        {
            var equatableTypeOpt = semanticModel.Compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName!);
            if (equatableTypeOpt != null)
            {
                constructedType = equatableTypeOpt.Construct(containingType);

                // A ref struct can never implement an interface, therefore never add IEquatable to the selection
                // options if the type is a ref struct.
                return !containingType.AllInterfaces.Contains(constructedType);
            }
        }

        constructedType = null;
        return false;
    }

    private static void GetExistingMemberInfo(INamedTypeSymbol containingType, out bool hasEquals, out bool hasGetHashCode)
    {
        hasEquals = containingType
            .GetMembers(EqualsName)
            .Any(m => m is IMethodSymbol { Parameters.Length: 1, IsStatic: false, IsImplicitlyDeclared: false });

        hasGetHashCode = containingType
            .GetMembers(GetHashCodeName)
            .Any(m => m is IMethodSymbol { Parameters.Length: 0, IsStatic: false, IsImplicitlyDeclared: false });
    }

    public async Task<ImmutableArray<CodeAction>> GenerateEqualsAndGetHashCodeFromMembersAsync(
        Document document,
        TextSpan textSpan,
        CancellationToken cancellationToken)
    {
        using (Logger.LogBlock(FunctionId.Refactoring_GenerateFromMembers_GenerateEqualsAndGetHashCode, cancellationToken))
        {
            var info = await GetSelectedMemberInfoAsync(document, textSpan, allowPartialSelection: false, cancellationToken).ConfigureAwait(false);
            if (info != null &&
                info.SelectedMembers.All(IsReadableInstanceFieldOrProperty))
            {
                if (info.ContainingType != null && info.ContainingType.TypeKind != TypeKind.Interface)
                {
                    GetExistingMemberInfo(
                        info.ContainingType, out var hasEquals, out var hasGetHashCode);

                    var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
                    var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                    var typeDeclaration = syntaxFacts.GetContainingTypeDeclaration(root, textSpan.Start);
                    RoslynDebug.AssertNotNull(typeDeclaration);

                    return await CreateActionsAsync(
                        document, typeDeclaration, info.ContainingType, info.SelectedMembers,
                        hasEquals, hasGetHashCode, withDialog: false, globalOptions: null, cancellationToken).ConfigureAwait(false);
                }
            }

            return default;
        }
    }

    private async Task<ImmutableArray<CodeAction>> CreateActionsAsync(
        Document document, SyntaxNode typeDeclaration, INamedTypeSymbol containingType, ImmutableArray<ISymbol> selectedMembers,
        bool hasEquals, bool hasGetHashCode, bool withDialog, ILegacyGlobalOptionsWorkspaceService? globalOptions, CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<Task<CodeAction>>.GetInstance(out var tasks);

        if (!hasEquals && !hasGetHashCode)
        {
            // if we don't have either Equals or GetHashCode then offer:
            //  "Generate Equals" and
            //  "Generate Equals and GetHashCode"
            //
            // Don't bother offering to just "Generate GetHashCode" as it's very unlikely
            // the user would need to bother just generating that member without also
            // generating 'Equals' as well.
            tasks.Add(CreateCodeActionAsync(
                document, typeDeclaration, containingType, selectedMembers, globalOptions,
                generateEquals: true, generateGetHashCode: false, withDialog, cancellationToken));
            tasks.Add(CreateCodeActionAsync(
                document, typeDeclaration, containingType, selectedMembers, globalOptions,
                generateEquals: true, generateGetHashCode: true, withDialog, cancellationToken));
        }
        else if (!hasEquals)
        {
            tasks.Add(CreateCodeActionAsync(
                document, typeDeclaration, containingType, selectedMembers, globalOptions,
                generateEquals: true, generateGetHashCode: false, withDialog, cancellationToken));
        }
        else if (!hasGetHashCode)
        {
            tasks.Add(CreateCodeActionAsync(
                document, typeDeclaration, containingType, selectedMembers, globalOptions,
                generateEquals: false, generateGetHashCode: true, withDialog, cancellationToken));
        }

        var codeActions = await Task.WhenAll(tasks).ConfigureAwait(false);
        return [.. codeActions];

    }

    private Task<CodeAction> CreateCodeActionAsync(
        Document document, SyntaxNode typeDeclaration, INamedTypeSymbol containingType, ImmutableArray<ISymbol> members,
        ILegacyGlobalOptionsWorkspaceService? globalOptions,
        bool generateEquals, bool generateGetHashCode, bool withDialog, CancellationToken cancellationToken)
    {
        if (withDialog)
        {
            // We can't create dialog code action if globalOptions is null
            Contract.ThrowIfNull(globalOptions);
            return CreateCodeActionWithDialogAsync(document, typeDeclaration, containingType, members, globalOptions, generateEquals, generateGetHashCode, cancellationToken);
        }
        else
        {
            return CreateCodeActionWithoutDialogAsync(document, typeDeclaration, containingType, members, generateEquals, generateGetHashCode, cancellationToken);
        }
    }

    private async Task<CodeAction> CreateCodeActionWithDialogAsync(
        Document document, SyntaxNode typeDeclaration, INamedTypeSymbol containingType, ImmutableArray<ISymbol> members,
        ILegacyGlobalOptionsWorkspaceService globalOptions,
        bool generateEquals, bool generateGetHashCode, CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

        using var _ = ArrayBuilder<PickMembersOption>.GetInstance(out var pickMembersOptions);

        if (CanImplementIEquatable(semanticModel, containingType, out var equatableTypeOpt))
        {
            var value = globalOptions.GetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable(document.Project.Language);

            var displayName = equatableTypeOpt.ToDisplayString(new SymbolDisplayFormat(
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
                genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters));

            pickMembersOptions.Add(new PickMembersOption(
                ImplementIEquatableId,
                string.Format(FeaturesResources.Implement_0, displayName),
                value));
        }

        if (!HasOperators(containingType))
        {
            var value = globalOptions.GetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators(document.Project.Language);

            pickMembersOptions.Add(new PickMembersOption(
                GenerateOperatorsId,
                FeaturesResources.Generate_operators,
                value));
        }

        return new GenerateEqualsAndGetHashCodeWithDialogCodeAction(
            this, document, typeDeclaration, containingType, members, pickMembersOptions.ToImmutable(), globalOptions, generateEquals, generateGetHashCode);
    }

    private static async Task<CodeAction> CreateCodeActionWithoutDialogAsync(
        Document document, SyntaxNode typeDeclaration, INamedTypeSymbol containingType, ImmutableArray<ISymbol> members,
        bool generateEquals, bool generateGetHashCode, CancellationToken cancellationToken)
    {
        var implementIEquatable = false;
        var generateOperators = false;

        if (generateEquals && containingType.TypeKind == TypeKind.Struct)
        {
            // if we're generating equals for a struct, then also add IEquatable<S> support as
            // well as operators (as long as the struct does not already have them).
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            implementIEquatable = CanImplementIEquatable(semanticModel, containingType, out _);
            generateOperators = !HasOperators(containingType);
        }

        return new GenerateEqualsAndGetHashCodeAction(
            document, typeDeclaration, containingType, members,
            generateEquals, generateGetHashCode, implementIEquatable, generateOperators);
    }
}
