﻿// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.IntroduceVariable;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable
{
    [ExportLanguageService(typeof(IIntroduceVariableService), LanguageNames.CSharp), Shared]
    internal partial class CSharpIntroduceVariableService :
        AbstractIntroduceVariableService<CSharpIntroduceVariableService, ExpressionSyntax, TypeSyntax, TypeDeclarationSyntax, QueryExpressionSyntax, NameSyntax>
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public CSharpIntroduceVariableService()
        {
        }

        protected override bool IsInNonFirstQueryClause(ExpressionSyntax expression)
        {
            var query = expression.GetAncestor<QueryExpressionSyntax>();
            if (query != null)
            {
                // Can't introduce for the first clause in a query.
                var fromClause = expression.GetAncestor<FromClauseSyntax>();
                if (fromClause == null || query.FromClause != fromClause)
                {
                    return true;
                }
            }

            return false;
        }

        protected override bool IsInFieldInitializer(ExpressionSyntax expression)
        {
            return expression.GetAncestorOrThis<VariableDeclaratorSyntax>()
                             .GetAncestorOrThis<FieldDeclarationSyntax>() != null;
        }

        protected override bool IsInParameterInitializer(ExpressionSyntax expression)
            => expression.GetAncestorOrThis<EqualsValueClauseSyntax>().IsParentKind(SyntaxKind.Parameter);

        protected override bool IsInConstructorInitializer(ExpressionSyntax expression)
            => expression.GetAncestorOrThis<ConstructorInitializerSyntax>() != null;

        protected override bool IsInAutoPropertyInitializer(ExpressionSyntax expression)
            => expression.GetAncestorOrThis<EqualsValueClauseSyntax>().IsParentKind(SyntaxKind.PropertyDeclaration);

        protected override bool IsInExpressionBodiedMember(ExpressionSyntax expression)
        {
            // walk up until we find a nearest enclosing block or arrow expression.
            for (SyntaxNode node = expression; node != null; node = node.Parent)
            {
                // If we are in an expression bodied member and if the expression has a block body, then,
                // act as if we're in a block context and not in an expression body context at all.
                if (node.IsKind(SyntaxKind.Block))
                {
                    return false;
                }
                else if (node.IsKind(SyntaxKind.ArrowExpressionClause))
                {
                    return true;
                }
            }

            return false;
        }

        protected override bool IsInAttributeArgumentInitializer(ExpressionSyntax expression)
        {
            // Don't call the base here.  We want to let the user extract a constant if they've
            // said "Goo(a = 10)"
            var attributeArgument = expression.GetAncestorOrThis<AttributeArgumentSyntax>();
            if (attributeArgument != null)
            {
                // Can't extract an attribute initializer if it contains an array initializer of any
                // sort.  Also, we can't extract if there's any typeof expression within it.
                if (!expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.ArrayCreationExpression) &&
                    !expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.TypeOfExpression))
                {
                    var attributeDecl = attributeArgument.GetAncestorOrThis<AttributeListSyntax>();

                    // Also can't extract an attribute initializer if the attribute is a global one.
                    if (!attributeDecl.IsParentKind(SyntaxKind.CompilationUnit))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Checks for conditions where we should not generate a variable for an expression
        /// </summary>
        protected override bool CanIntroduceVariableFor(ExpressionSyntax expression)
        {
            // (a) If that's the only expression in a statement.
            // Otherwise we'll end up with something like "v;" which is not legal in C#.
            if (expression.WalkUpParentheses().IsParentKind(SyntaxKind.ExpressionStatement))
            {
                return false;
            }

            // (b) For Null Literals, as AllOccurrences could introduce semantic errors.
            if (expression.IsKind(SyntaxKind.NullLiteralExpression))
            {
                return false;
            }

            // (c) For throw expressions.
            if (expression.IsKind(SyntaxKind.ThrowExpression))
            {
                return false;
            }

            return true;
        }

        protected override IEnumerable<SyntaxNode> GetContainingExecutableBlocks(ExpressionSyntax expression)
            => expression.GetAncestorsOrThis<BlockSyntax>();

        protected override IList<bool> GetInsertionIndices(TypeDeclarationSyntax destination, CancellationToken cancellationToken)
            => destination.GetInsertionIndices(cancellationToken);

        protected override bool CanReplace(ExpressionSyntax expression)
            => true;

        protected override bool IsExpressionInStaticLocalFunction(ExpressionSyntax expression)
        {
            var localFunction = expression.GetAncestor<LocalFunctionStatementSyntax>();
            return localFunction != null && localFunction.Modifiers.Any(SyntaxKind.StaticKeyword);
        }

        protected override TNode RewriteCore<TNode>(
            TNode node,
            SyntaxNode replacementNode,
            ISet<ExpressionSyntax> matches)
        {
            return (TNode)Rewriter.Visit(node, replacementNode, matches);
        }
    }
}
