// 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.Diagnostics;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp
{
    partial class Binder
    {
        internal sealed class CapturedParametersFinder : IdentifierUsedAsValueFinder
        {
            private readonly SynthesizedPrimaryConstructor _primaryConstructor;
            private readonly HashSet<string> _namesToCheck;
            private readonly ArrayBuilder<ParameterSymbol> _captured;

            private CapturedParametersFinder(SynthesizedPrimaryConstructor primaryConstructor, HashSet<string> namesToCheck, ArrayBuilder<ParameterSymbol> captured)
            {
                this._primaryConstructor = primaryConstructor;
                this._namesToCheck = namesToCheck;
                this._captured = captured;
            }

            public static IReadOnlyDictionary<ParameterSymbol, FieldSymbol> GetCapturedParameters(SynthesizedPrimaryConstructor primaryConstructor)
            {
                var namesToCheck = PooledHashSet<string>.GetInstance();
                addParameterNames(namesToCheck);

                if (namesToCheck.Count == 0)
                {
                    namesToCheck.Free();
                    return SpecializedCollections.EmptyReadOnlyDictionary<ParameterSymbol, FieldSymbol>();
                }

                var captured = ArrayBuilder<ParameterSymbol>.GetInstance(primaryConstructor.Parameters.Length);

                var finder = new CapturedParametersFinder(primaryConstructor, namesToCheck, captured);

                var containingType = primaryConstructor.ContainingType;

                foreach (SourceMemberMethodSymbol sourceMethod in containingType.GetMethodsPossiblyCapturingPrimaryConstructorParameters())
                {
                    Binder? bodyBinder;
                    CSharpSyntaxNode? syntaxNode;

                    getBodyBinderAndSyntax(sourceMethod, out bodyBinder, out syntaxNode);
                    if (bodyBinder is null)
                    {
                        continue;
                    }

                    Debug.Assert(syntaxNode is not null);

                    bool keepChecking = checkParameterReferencesInMethodBody(syntaxNode, bodyBinder);
                    if (!keepChecking)
                    {
                        break;
                    }
                }

                finder.Free();
                namesToCheck.Free();

                if (captured.Count == 0)
                {
                    captured.Free();
                    return SpecializedCollections.EmptyReadOnlyDictionary<ParameterSymbol, FieldSymbol>();
                }

                var result = new Dictionary<ParameterSymbol, FieldSymbol>(ReferenceEqualityComparer.Instance);

                foreach (var parameter in captured)
                {
                    result.Add(parameter,
                               new SynthesizedPrimaryConstructorParameterBackingFieldSymbol(parameter,
                                                                                            GeneratedNames.MakePrimaryConstructorParameterFieldName(parameter.Name),
                                                                                            isReadOnly: containingType.IsReadOnly));
                }

                captured.Free();
                return result;

                void addParameterNames(PooledHashSet<string> namesToCheck)
                {
                    foreach (var parameter in primaryConstructor.Parameters)
                    {
                        if (parameter.Name.Length != 0)
                        {
                            namesToCheck.Add(parameter.Name);
                        }
                    }
                }

                void getBodyBinderAndSyntax(SourceMemberMethodSymbol sourceMethod, out Binder? bodyBinder, out CSharpSyntaxNode? syntaxNode)
                {
                    bodyBinder = null;
                    syntaxNode = null;

                    bodyBinder = sourceMethod.TryGetBodyBinder();

                    if (bodyBinder is null)
                    {
                        return;
                    }

                    syntaxNode = sourceMethod.SyntaxNode;
                }

                bool checkParameterReferencesInMethodBody(CSharpSyntaxNode syntaxNode, Binder bodyBinder)
                {
                    switch (syntaxNode)
                    {
                        case ConstructorDeclarationSyntax s:
                            return finder.CheckIdentifiersInNode(s.Initializer, bodyBinder) &&
                                   finder.CheckIdentifiersInNode(s.Body, bodyBinder) &&
                                   finder.CheckIdentifiersInNode(s.ExpressionBody, bodyBinder);

                        case BaseMethodDeclarationSyntax s:
                            return finder.CheckIdentifiersInNode(s.Body, bodyBinder) &&
                                   finder.CheckIdentifiersInNode(s.ExpressionBody, bodyBinder);

                        case AccessorDeclarationSyntax s:
                            return finder.CheckIdentifiersInNode(s.Body, bodyBinder) &&
                                   finder.CheckIdentifiersInNode(s.ExpressionBody, bodyBinder);

                        case ArrowExpressionClauseSyntax s:
                            return finder.CheckIdentifiersInNode(s, bodyBinder);

                        default:
                            throw ExceptionUtilities.UnexpectedValue(syntaxNode);
                    }
                }
            }

            protected override bool IsIdentifierOfInterest(IdentifierNameSyntax id)
            {
                return _namesToCheck.Contains(id.Identifier.ValueText);
            }

            protected override bool CheckAndClearLookupResult(Binder enclosingBinder, IdentifierNameSyntax id, LookupResult lookupResult)
            {
                if (lookupResult.IsMultiViable)
                {
                    bool? isInsideNameof = null;
                    bool detectedCapture = false;

                    foreach (var candidate in lookupResult.Symbols)
                    {
                        if (candidate is ParameterSymbol parameter && parameter.ContainingSymbol == (object)_primaryConstructor)
                        {
                            isInsideNameof ??= enclosingBinder.IsInsideNameof;

                            if (isInsideNameof.GetValueOrDefault())
                            {
                                break;
                            }
                            else if (lookupResult.IsSingleViable)
                            {
                                Debug.Assert(lookupResult.SingleSymbolOrDefault == (object)parameter);

                                // Check for left of potential color color member access 
                                if (IsTypeOrValueReceiver(enclosingBinder, id, parameter.Type, out SyntaxNode? memberAccessNode, out string? memberName, out int targetMemberArity, out bool invoked))
                                {
                                    lookupResult.Clear();
                                    if (TreatAsInstanceMemberAccess(enclosingBinder, parameter.Type, memberAccessNode, memberName, targetMemberArity, invoked, lookupResult))
                                    {
                                        _captured.Add(parameter);
                                        detectedCapture = true;
                                    }

                                    // We cleared the lookupResult and the candidate list within it.
                                    // Do not attempt to continue the enclosing foreach
                                    break;
                                }
                            }

                            _captured.Add(parameter);
                            detectedCapture = true;
                        }
                    }

                    if (detectedCapture)
                    {
                        _namesToCheck.Remove(id.Identifier.ValueText);

                        if (_namesToCheck.Count == 0)
                        {
                            return false;
                        }
                    }
                }

                lookupResult.Clear();
                return true;
            }
        }
    }
}
