// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Analyzers.UseCoalesceExpression
{
    internal abstract class AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer<
        TSyntaxKind,
        TExpressionSyntax,
        TStatementSyntax,
        TVariableDeclarator,
        TIfStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer
        where TSyntaxKind : struct
        where TExpressionSyntax : SyntaxNode
        where TStatementSyntax : SyntaxNode
        where TVariableDeclarator : SyntaxNode
        where TIfStatementSyntax : TStatementSyntax
    {
        protected AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer()
            : base(IDEDiagnosticIds.UseCoalesceExpressionForIfNullCheckDiagnosticId,
                   EnforceOnBuildValues.UseCoalesceExpression,
                   CodeStyleOptions2.PreferCoalesceExpression,
                   new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
                   new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
        {
        }

        protected abstract TSyntaxKind IfStatementKind { get; }
        protected abstract ISyntaxFacts SyntaxFacts { get; }

        protected abstract bool IsSingle(TVariableDeclarator declarator);
        protected abstract bool IsNullCheck(TExpressionSyntax condition, [NotNullWhen(true)] out TExpressionSyntax? checkedExpression);
        protected abstract bool HasElseBlock(TIfStatementSyntax ifStatement);

        protected abstract SyntaxNode GetDeclarationNode(TVariableDeclarator declarator);
        protected abstract TExpressionSyntax GetConditionOfIfStatement(TIfStatementSyntax ifStatement);
        protected abstract bool TryGetEmbeddedStatement(TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TStatementSyntax? whenTrueStatement);

        protected abstract TStatementSyntax? TryGetPreviousStatement(TIfStatementSyntax ifStatement);

        public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
            => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

        protected override void InitializeWorker(AnalysisContext context)
            => context.RegisterSyntaxNodeAction(AnalyzeSyntax, this.IfStatementKind);

        private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
        {
            var cancellationToken = context.CancellationToken;
            var ifStatement = (TIfStatementSyntax)context.Node;
            var semanticModel = context.SemanticModel;

            var option = context.GetAnalyzerOptions().PreferCoalesceExpression;
            if (!option.Value)
                return;

            var syntaxFacts = this.SyntaxFacts;
            var condition = GetConditionOfIfStatement(ifStatement);

            if (!IsNullCheck(condition, out var checkedExpression))
                return;

            var previousStatement = TryGetPreviousStatement(ifStatement);
            if (previousStatement is null)
                return;

            if (HasElseBlock(ifStatement))
                return;

            if (!TryGetEmbeddedStatement(ifStatement, out var whenTrueStatement))
                return;

            if (syntaxFacts.IsThrowStatement(whenTrueStatement))
            {
                if (!syntaxFacts.SupportsThrowExpression(ifStatement.SyntaxTree.Options))
                    return;

                var thrownExpression = syntaxFacts.GetExpressionOfThrowStatement(whenTrueStatement);
                if (thrownExpression is null)
                    return;
            }

            TExpressionSyntax? expressionToCoalesce;

            if (syntaxFacts.IsLocalDeclarationStatement(previousStatement))
            {
                // var v = Expr();
                // if (v == null)
                //    ...

                if (!AnalyzeLocalDeclarationForm(previousStatement, out expressionToCoalesce))
                    return;
            }
            else if (syntaxFacts.IsAnyAssignmentStatement(previousStatement))
            {
                // v = Expr();
                // if (v == null)
                //    ...
                if (!AnalyzeAssignmentForm(previousStatement, out expressionToCoalesce))
                    return;
            }
            else
            {
                return;
            }

            context.ReportDiagnostic(DiagnosticHelper.Create(
                Descriptor,
                ifStatement.GetFirstToken().GetLocation(),
                option.Notification.Severity,
                ImmutableArray.Create(
                    expressionToCoalesce.GetLocation(),
                    ifStatement.GetLocation(),
                    whenTrueStatement.GetLocation()),
                properties: null));

            bool AnalyzeLocalDeclarationForm(
                TStatementSyntax localDeclarationStatement,
                [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce)
            {
                expressionToCoalesce = null;

                // var v = Expr();
                // if (v == null)
                //    ...

                if (!syntaxFacts.IsIdentifierName(checkedExpression))
                    return false;

                var conditionIdentifier = syntaxFacts.GetIdentifierOfIdentifierName(checkedExpression).ValueText;

                var declarators = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
                if (declarators.Count != 1)
                    return false;

                var declarator = (TVariableDeclarator)declarators[0];
                if (!IsSingle(declarator))
                    return false;

                var equalsValue = syntaxFacts.GetInitializerOfVariableDeclarator(declarator);
                if (equalsValue is null)
                    return false;

                if (syntaxFacts.GetValueOfEqualsValueClause(equalsValue) is not TExpressionSyntax initializer)
                    return false;

                expressionToCoalesce = initializer;

                var variableName = syntaxFacts.GetIdentifierOfVariableDeclarator(declarator).ValueText;
                if (conditionIdentifier != variableName)
                    return false;

                // if 'Expr()' is a value type, we can't use `??` on it.
                var exprType = semanticModel.GetTypeInfo(initializer, cancellationToken).Type;
                if (exprType is null || exprType.IsNonNullableValueType())
                    return false;

                if (!IsLegalWhenTrueStatementForAssignment(out var whenPartToAnalyze))
                    return false;

                // Looks good.  However, make sure the when-true part doesn't access this symbol.  We can't merge
                // with the assignment then.
                var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(GetDeclarationNode(declarator), cancellationToken);
                foreach (var identifier in whenPartToAnalyze.DescendantNodesAndSelf())
                {
                    if (syntaxFacts.IsIdentifierName(identifier) &&
                        syntaxFacts.GetIdentifierOfIdentifierName(identifier).ValueText == localSymbol.Name)
                    {
                        var symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol();
                        if (Equals(localSymbol, symbol))
                            return false;
                    }
                }

                return true;

                bool IsLegalWhenTrueStatementForAssignment([NotNullWhen(true)] out SyntaxNode? whenPartToAnalyze)
                {
                    whenPartToAnalyze = whenTrueStatement;

                    // var v = Expr();
                    // if (v == null)
                    //    throw ...
                    //
                    // can always convert this to `var v = Expr() ?? throw
                    if (syntaxFacts.IsThrowStatement(whenTrueStatement))
                        return true;

                    // var v = Expr();
                    // if (v == null)
                    //    v = ...
                    //
                    // can convert if embedded statement is assigning to same variable
                    if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement))
                    {
                        syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var left, out var right);
                        if (syntaxFacts.IsIdentifierName(left))
                        {
                            whenPartToAnalyze = right;
                            var leftName = syntaxFacts.GetIdentifierOfIdentifierName(left).ValueText;
                            return leftName == variableName;
                        }
                    }

                    return false;
                }
            }

            bool AnalyzeAssignmentForm(
                TStatementSyntax assignmentStatement,
                [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce)
            {
                expressionToCoalesce = null;

                // expr = Expr();
                // if (expr == null)
                //    ...

                syntaxFacts.GetPartsOfAssignmentStatement(assignmentStatement, out var topAssignmentLeft, out var topAssignmentRight);
                if (!syntaxFacts.AreEquivalent(topAssignmentLeft, checkedExpression))
                    return false;

                expressionToCoalesce = topAssignmentRight as TExpressionSyntax;
                if (expressionToCoalesce is null)
                    return false;

                // expr = Expr();
                // if (expr == null)
                //    throw ...
                //
                // can always convert this to `var v = Expr() ?? throw
                if (syntaxFacts.IsThrowStatement(whenTrueStatement))
                    return true;

                // expr = Expr();
                // if (expr == null)
                //    expr = ...
                //
                // can convert if embedded statement is assigning to same variable
                if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement))
                {
                    syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var innerAssignmentLeft, out _);
                    return syntaxFacts.AreEquivalent(innerAssignmentLeft, checkedExpression);
                }

                return false;
            }
        }
    }
}
