// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast
{
    internal abstract partial class AbstractAddExplicitCastCodeFixProvider<TExpressionSyntax> where TExpressionSyntax : SyntaxNode
    {
        protected abstract class Fixer<TArgumentSyntax, TArgumentListSyntax, TInvocationSyntax>
            where TArgumentSyntax : SyntaxNode
            where TArgumentListSyntax : SyntaxNode
            where TInvocationSyntax : SyntaxNode
        {
            private readonly AbstractAddExplicitCastCodeFixProvider<TExpressionSyntax> _provider;

            protected Fixer(AbstractAddExplicitCastCodeFixProvider<TExpressionSyntax> provider)
                => _provider = provider;

            protected abstract TExpressionSyntax GetExpressionOfArgument(TArgumentSyntax argument);
            protected abstract TArgumentSyntax GenerateNewArgument(TArgumentSyntax oldArgument, ITypeSymbol conversionType);
            protected abstract TArgumentListSyntax GenerateNewArgumentList(TArgumentListSyntax oldArgumentList, ArrayBuilder<TArgumentSyntax> newArguments);
            protected abstract SeparatedSyntaxList<TArgumentSyntax> GetArgumentsOfArgumentList(TArgumentListSyntax argumentList);
            protected abstract SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, TArgumentListSyntax newArgumentList);

            /// <summary>
            /// Collect all the available cast pairs, format is (target argument expression, potential conversion type)
            /// </summary>
            /// <param name="targetArgument"> The argument that need to be cast</param>
            /// <param name="argumentList"> The argument list that contains the target argument to be cast </param>
            /// <param name="invocationNode"> The invocation node that is the parent of "argumentList"</param>
            /// <returns>
            /// Return all the available cast pairs, format is (target argument expression, potential conversion type)
            /// </returns>
            public ImmutableArray<(TExpressionSyntax, ITypeSymbol)> GetPotentialConversionTypes(
                SemanticModel semanticModel,
                SyntaxNode root,
                TArgumentSyntax targetArgument,
                TArgumentListSyntax argumentList,
                TInvocationSyntax invocationNode,
                CancellationToken cancellationToken)
            {
                // Implicit downcast appears on the argument of invocation node,
                // get all candidate functions and extract potential conversion types 
                var symbolInfo = semanticModel.GetSymbolInfo(invocationNode, cancellationToken);
                using var _ = ArrayBuilder<ISymbol>.GetInstance(out var candidateSymbols);
                if (symbolInfo.Symbol != null) // BC42016: the only candidate symbol is symbolInfo.Symbol
                {
                    candidateSymbols.Add(symbolInfo.Symbol);
                }
                else
                {
                    candidateSymbols.AddRange(symbolInfo.CandidateSymbols);
                }

                using var __ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes);
                foreach (var candidateSymbol in candidateSymbols.OfType<IMethodSymbol>())
                {
                    if (CanArgumentTypesBeConvertedToParameterTypes(
                            semanticModel, root, argumentList, candidateSymbol.Parameters,
                            targetArgument, cancellationToken, out var targetArgumentConversionType)
                        && GetExpressionOfArgument(targetArgument) is TExpressionSyntax argumentExpression)
                    {
                        mutablePotentialConversionTypes.Add((argumentExpression, targetArgumentConversionType));
                    }
                }

                // Sort the potential conversion types by inheritance distance, so that
                // operations are in order and user can choose least specific types(more accurate)
                mutablePotentialConversionTypes.Sort(new InheritanceDistanceComparer<TExpressionSyntax>(semanticModel));

                return mutablePotentialConversionTypes.ToImmutable();
            }

            /// <summary>
            /// Test if all argument types can be converted to corresponding parameter types.
            /// </summary>
            /// For example:
            /// class Base { }
            /// class Derived1 : Base { }
            /// class Derived2 : Base { }
            /// class Derived3 : Base { }
            /// void DoSomething(int i, Derived1 d) { }
            /// void DoSomething(string s, Derived2 d) { }
            /// void DoSomething(int i, Derived3 d) { }
            /// 
            /// Base b;
            /// DoSomething(1, [||]b);
            ///
            /// *void DoSomething(string s, Derived2 d) { }* is not the perfect match candidate function for
            /// *DoSomething(1, [||]b)* because int and string are not ancestor-descendant relationship. Thus,
            /// Derived2 is not a potential conversion type.
            /// 
            /// <param name="argumentList"> The argument list of invocation expression</param>
            /// <param name="parameters"> The parameters of function</param>
            /// <param name="targetArgument">The argument need to be cast.</param>
            /// <param name="targetArgumentConversionType"> Output the corresponding parameter type of
            /// "targetArgument" if function returns true</param>
            /// <returns>
            /// True, if arguments and parameters match perfectly.
            /// "targetArgumentConversionType" outputs the corresponding parameter type of "targetArgument"
            /// False, otherwise.
            /// </returns>
            public bool CanArgumentTypesBeConvertedToParameterTypes(
                SemanticModel semanticModel,
                SyntaxNode root,
                TArgumentListSyntax argumentList,
                ImmutableArray<IParameterSymbol> parameters,
                TArgumentSyntax targetArgument,
                CancellationToken cancellationToken,
                [NotNullWhen(true)] out ITypeSymbol? targetArgumentConversionType)
            {
                targetArgumentConversionType = null;

                // No conversion happens under this case
                if (parameters.Length == 0)
                    return false;

                var syntaxFacts = _provider.SyntaxFacts;

                var arguments = GetArgumentsOfArgumentList(argumentList);
                using var _ = ArrayBuilder<TArgumentSyntax>.GetInstance(out var newArguments);

                for (var i = 0; i < arguments.Count; i++)
                {
                    // Parameter index cannot out of its range, #arguments is larger than #parameter only if 
                    // the last parameter with keyword params
                    var parameterIndex = Math.Min(i, parameters.Length - 1);

                    // If the argument has a name, get the corresponding parameter index
                    if (syntaxFacts.GetNameForArgument(arguments[i]) is string name
                        && name != string.Empty
                        && !FindCorrespondingParameterByName(name, parameters, ref parameterIndex))
                    {
                        return false;
                    }

                    // The argument is either in order with parameters, or have a matched name with parameters.
                    var argumentExpression = GetExpressionOfArgument(arguments[i]);
                    if (argumentExpression == null)
                    {
                        // argumentExpression is null when it is an omitted argument in VB .NET
                        newArguments.Add(arguments[i]);
                        continue;
                    }

                    var parameterType = parameters[parameterIndex].Type;
                    if (parameters[parameterIndex].IsParams
                        && parameterType is IArrayTypeSymbol paramsType
                        && _provider.ClassifyConversion(semanticModel, argumentExpression, paramsType.ElementType).Exists)
                    {
                        newArguments.Add(GenerateNewArgument(arguments[i], paramsType.ElementType));
                        if (arguments[i].Equals(targetArgument))
                            targetArgumentConversionType = paramsType.ElementType;
                    }
                    else if (_provider.ClassifyConversion(semanticModel, argumentExpression, parameterType).Exists)
                    {
                        newArguments.Add(GenerateNewArgument(arguments[i], parameterType));
                        if (arguments[i].Equals(targetArgument))
                            targetArgumentConversionType = parameterType;
                    }
                    else if (syntaxFacts.IsDeclarationExpression(argumentExpression)
                        && semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type is ITypeSymbol argumentType
                        && semanticModel.Compilation.ClassifyCommonConversion(argumentType, parameterType).IsIdentity)
                    {
                        // Direct conversion from a declaration expression to a type is unspecified, thus we classify the
                        // conversion from the type of declaration expression to the parameter type
                        // An example for this case:
                        // void Goo(out int i) { i = 1; }
                        // Goo([|out var i|]);
                        // "var i" is a declaration expression
                        // 
                        // In addition, since this case is with keyword "out", the type of declaration expression and the
                        // parameter type must be identical in order to match.
                        newArguments.Add(arguments[i]);
                    }
                    else
                    {
                        return false;
                    }
                }

                return targetArgumentConversionType != null
                    && IsInvocationExpressionWithNewArgumentsApplicable(
                        semanticModel, root, argumentList, newArguments, targetArgument);
            }

            /// <summary>
            /// Check whether the invocation expression with new arguments is applicable.
            /// </summary>
            /// <param name="oldArgumentList" >old argumentList node</param>
            /// <param name="newArguments"> new arguments that are cast by corresponding parameter types</param>
            /// <param name="targetNode"> The node needs to be cast.</param>
            /// <returns>
            /// Return true if the invocation expression with new arguments is applicable.
            /// Otherwise, return false
            /// </returns>
            private bool IsInvocationExpressionWithNewArgumentsApplicable(SemanticModel semanticModel,
                SyntaxNode root,
                TArgumentListSyntax oldArgumentList,
                ArrayBuilder<TArgumentSyntax> newArguments,
                SyntaxNode targetNode)
            {
                var newRoot = root.ReplaceNode(oldArgumentList, GenerateNewArgumentList(oldArgumentList, newArguments));
                if (newRoot.FindNode(targetNode.Span).GetAncestorOrThis<TArgumentListSyntax>() is TArgumentListSyntax newArgumentList)
                {
                    var symbolInfo = GetSpeculativeSymbolInfo(semanticModel, newArgumentList);
                    return symbolInfo.Symbol != null;
                }

                return false;
            }
        }
    }
}
