﻿// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Completion
{
    /// <summary>
    /// This <see cref="ArgumentProvider"/> attempts to locate a matching value in the context of a method invocation.
    /// </summary>
    internal abstract class AbstractContextVariableArgumentProvider : ArgumentProvider
    {
        protected abstract string ThisOrMeKeyword { get; }

        protected abstract bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken);

        public override async Task ProvideArgumentAsync(ArgumentContext context)
        {
            if (context.PreviousValue is not null)
            {
                // This argument provider does not attempt to replace arguments already in code.
                return;
            }

            var requireExactType = context.Parameter.Type.IsSpecialType()
                || context.Parameter.RefKind != RefKind.None;
            var symbols = context.SemanticModel.LookupSymbols(context.Position);

            // First try to find a local variable
            ISymbol? bestSymbol = null;
            string? bestSymbolName = null;
            CommonConversion bestConversion = default;
            foreach (var symbol in symbols)
            {
                ISymbol candidate;
                if (symbol.IsKind(SymbolKind.Parameter, out IParameterSymbol? parameter))
                    candidate = parameter;
                else if (symbol.IsKind(SymbolKind.Local, out ILocalSymbol? local))
                    candidate = local;
                else
                    continue;

                CheckCandidate(candidate);
            }

            if (bestSymbol is not null)
            {
                context.DefaultValue = bestSymbolName;
                return;
            }

            // Next try fields and properties of the current type
            foreach (var symbol in symbols)
            {
                ISymbol candidate;
                if (symbol.IsKind(SymbolKind.Field, out IFieldSymbol? field))
                    candidate = field;
                else if (symbol.IsKind(SymbolKind.Property, out IPropertySymbol? property))
                    candidate = property;
                else
                    continue;

                // Require a name match for primitive types
                if (candidate.GetSymbolType().IsSpecialType()
                    && !string.Equals(candidate.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                CheckCandidate(candidate);
            }

            if (bestSymbol is not null)
            {
                context.DefaultValue = bestSymbolName;
                return;
            }

            // Finally, if the invocation occurs in an instance context, check the current type ('this' or 'Me')
            var tree = context.SemanticModel.SyntaxTree;
            var targetToken = await tree.GetTouchingTokenAsync(context.Position, context.CancellationToken).ConfigureAwait(false);
            if (IsInstanceContext(tree, targetToken, context.SemanticModel, context.CancellationToken))
            {
                var enclosingSymbol = context.SemanticModel.GetEnclosingSymbol(targetToken.SpanStart, context.CancellationToken);
                while (enclosingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction } method)
                {
                    // It is allowed to reference the instance (`this`) within a local function or anonymous function,
                    // as long as the containing method allows it
                    enclosingSymbol = enclosingSymbol.ContainingSymbol;
                }

                if (enclosingSymbol is IMethodSymbol { ContainingType: { } containingType })
                {
                    CheckCandidate(containingType, ThisOrMeKeyword);
                }
            }

            if (bestSymbol is not null)
            {
                context.DefaultValue = bestSymbolName;
                return;
            }

            // Local functions
            void CheckCandidate(ISymbol candidate, string? overridingName = null)
            {
                if (candidate.GetSymbolType() is not { } symbolType)
                {
                    return;
                }

                if (requireExactType && !SymbolEqualityComparer.Default.Equals(context.Parameter.Type, symbolType))
                {
                    return;
                }

                var conversion = context.SemanticModel.Compilation.ClassifyCommonConversion(symbolType, context.Parameter.Type);
                if (!conversion.IsImplicit)
                {
                    return;
                }

                if (bestSymbol is not null && !IsNewConversionSameOrBetter(conversion))
                {
                    if (!IsNewConversionSameOrBetter(conversion))
                        return;

                    if (!IsNewNameSameOrBetter(candidate))
                        return;
                }

                bestSymbol = candidate;
                bestSymbolName = overridingName ?? bestSymbol.Name;
                bestConversion = conversion;
            }

            bool IsNewConversionSameOrBetter(CommonConversion conversion)
            {
                if (bestConversion.IsIdentity && !conversion.IsIdentity)
                    return false;

                if (bestConversion.IsImplicit && !conversion.IsImplicit)
                    return false;

                return true;
            }

            bool IsNewNameSameOrBetter(ISymbol symbol)
            {
                if (string.Equals(bestSymbol.Name, context.Parameter.Name))
                    return string.Equals(symbol.Name, context.Parameter.Name);

                if (string.Equals(bestSymbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase))
                    return string.Equals(symbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase);

                return true;
            }
        }
    }
}
