﻿// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindSymbols.Finders;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ChangeSignature
{
    /// <summary>
    /// For ChangeSignature, FAR on a delegate invoke method must cascade to BeginInvoke, 
    /// cascade through method group conversions, and discover implicit invocations that do not
    /// mention the string "Invoke" or the delegate type itself. This implementation finds these
    /// symbols by binding most identifiers and invocation expressions in the solution. 
    /// </summary>
    /// <remarks>
    /// TODO: Rewrite this to track backward through references instead of binding everything
    /// </remarks>
    internal class DelegateInvokeMethodReferenceFinder : AbstractReferenceFinder<IMethodSymbol>
    {
        public static readonly IReferenceFinder DelegateInvokeMethod = new DelegateInvokeMethodReferenceFinder();

        protected override bool CanFind(IMethodSymbol symbol)
            => symbol.MethodKind == MethodKind.DelegateInvoke;

        protected override async ValueTask<ImmutableArray<ISymbol>> DetermineCascadedSymbolsAsync(
            IMethodSymbol symbol,
            Solution solution,
            FindReferencesSearchOptions options,
            CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<ISymbol>.GetInstance(out var result);

            var beginInvoke = symbol.ContainingType.GetMembers(WellKnownMemberNames.DelegateBeginInvokeName).FirstOrDefault();
            if (beginInvoke != null)
                result.Add(beginInvoke);

            // All method group references
            foreach (var project in solution.Projects)
            {
                foreach (var document in project.Documents)
                {
                    var changeSignatureService = document.GetRequiredLanguageService<AbstractChangeSignatureService>();
                    var cascaded = await changeSignatureService.DetermineCascadedSymbolsFromDelegateInvokeAsync(
                        symbol, document, cancellationToken).ConfigureAwait(false);
                    result.AddRange(cascaded);
                }
            }

            return result.ToImmutable();
        }

        protected override Task<ImmutableArray<Document>> DetermineDocumentsToSearchAsync(
            IMethodSymbol symbol,
            HashSet<string>? globalAliases,
            Project project,
            IImmutableSet<Document>? documents,
            FindReferencesSearchOptions options,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(project.Documents.ToImmutableArray());
        }

        protected override async ValueTask<ImmutableArray<FinderLocation>> FindReferencesInDocumentAsync(
            IMethodSymbol methodSymbol,
            FindReferencesDocumentState state,
            FindReferencesSearchOptions options,
            CancellationToken cancellationToken)
        {
            // FAR on the Delegate type and use those results to find Invoke calls

            var syntaxFacts = state.SyntaxFacts;

            var root = state.Root;
            var nodes = root.DescendantNodes();

            using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var convertedAnonymousFunctions);
            foreach (var node in nodes)
            {
                if (!syntaxFacts.IsAnonymousFunctionExpression(node))
                    continue;

                var convertedType = (ISymbol?)state.SemanticModel.GetTypeInfo(node, cancellationToken).ConvertedType;
                if (convertedType != null)
                {
                    convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, state.Solution, cancellationToken).ConfigureAwait(false)
                        ?? convertedType;
                }

                if (convertedType == methodSymbol.ContainingType)
                    convertedAnonymousFunctions.Add(node);
            }

            var invocations = nodes.Where(syntaxFacts.IsInvocationExpression)
                .Where(e => state.SemanticModel.GetSymbolInfo(e, cancellationToken).Symbol?.OriginalDefinition == methodSymbol);

            return invocations.Concat(convertedAnonymousFunctions).SelectAsArray(
                node => new FinderLocation(
                    node,
                    new ReferenceLocation(
                        state.Document,
                        alias: null,
                        node.GetLocation(),
                        isImplicit: false,
                        GetSymbolUsageInfo(node, state, cancellationToken),
                        GetAdditionalFindUsagesProperties(node, state),
                        CandidateReason.None)));
        }
    }
}
