// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Extensions
{
    internal static class BlockSyntaxExtensions
    {
        public static bool TryConvertToExpressionBody(
            this BlockSyntax? block,
            LanguageVersion languageVersion,
            ExpressionBodyPreference preference,
            CancellationToken cancellationToken,
            [NotNullWhen(true)] out ExpressionSyntax? expression,
            out SyntaxToken semicolonToken)
        {
            if (preference != ExpressionBodyPreference.Never &&
                block is { Statements: [var statement] } &&
                TryGetExpression(statement, languageVersion, out expression, out semicolonToken) &&
                MatchesPreference(expression, preference) &&
                HasAcceptableDirectiveShape(statement, block.CloseBraceToken))
            {
                // The close brace of the block may have important trivia on it (like 
                // comments or directives).  Preserve them on the semicolon when we
                // convert to an expression body.
                semicolonToken = semicolonToken.WithAppendedTrailingTrivia(
                    block.CloseBraceToken.LeadingTrivia.Where(t => !t.IsWhitespaceOrEndOfLine()));
                return true;
            }

            expression = null;
            semicolonToken = default;
            return false;

            static bool IsAnyCodeDirective(SyntaxTrivia trivia)
                => trivia.GetStructure() is ConditionalDirectiveTriviaSyntax;

            // We can have an ifdef'ed section around the statement, as long as each segment of the ifdef
            // contains an expression-statement or throw-statement.
            bool HasAcceptableDirectiveShape(StatementSyntax statement, SyntaxToken closeBrace)
            {
                var leadingDirectives = statement.GetLeadingTrivia().Where(IsAnyCodeDirective).ToImmutableArray();
                var closeBraceLeadingDirectives = block.CloseBraceToken.LeadingTrivia.Where(IsAnyCodeDirective).ToImmutableArray();

                if (leadingDirectives.Length == 0)
                {
                    // If we don't have any leading directives, our close brace token better not have any as well.
                    return closeBraceLeadingDirectives.Length == 0;
                }

                // Ok, we have some if/elif/else/endif pp directives above us.  If we're one of hte branches, and all
                // the rest of the branches are ok as well, we can convert this.

                if (leadingDirectives.Any(t => t.Kind() == SyntaxKind.EndIfDirectiveTrivia))
                    return false;

                var firstDirective = (DirectiveTriviaSyntax)leadingDirectives.First().GetStructure()!;
                var conditionalDirectives = firstDirective.GetMatchingConditionalDirectives(cancellationToken);

                // The sequence of conditionals have to all be within the method body.
                if (conditionalDirectives.First().SpanStart <= block.OpenBraceToken.SpanStart ||
                    conditionalDirectives.Last().Span.End >= block.CloseBraceToken.Span.End)
                {
                    return false;
                }

                // Last directive has to come after our statement.
                if (conditionalDirectives.Last().Span.End <= statement.Span.Start)
                    return false;

                // Now, check each part of the conditional chain
                foreach (var conditionalDirective in conditionalDirectives)
                {
                    var parentTrivia = conditionalDirective.ParentTrivia;
                    var parentToken = parentTrivia.Token;
                    var triviaIndex = parentToken.LeadingTrivia.IndexOf(parentTrivia);
                    if (triviaIndex + 1 < parentToken.LeadingTrivia.Count)
                    {
                        var nextTrivia = parentToken.LeadingTrivia[triviaIndex + 1];
                        if (nextTrivia.Kind() == SyntaxKind.DisabledTextTrivia)
                        {
                            // This was a conditional before a disabled section.  Parse out the disabled section and make
                            // sure it can legally become the body of a expression-bodied member.
                            var parsed = SyntaxFactory.ParseStatement(nextTrivia.ToFullString());
                            if (parsed.GetDiagnostics().Any(static d => d.Severity == DiagnosticSeverity.Error))
                                return false;
                        }
                    }
                }

                // Make sure there aren't any *new* pp directives before the close brace.
                foreach (var closeBraceDirective in closeBraceLeadingDirectives)
                {
                    if (!conditionalDirectives.Contains((DirectiveTriviaSyntax)closeBraceDirective.GetStructure()!))
                        return false;
                }

                return true;
            }
        }

        public static bool TryConvertToArrowExpressionBody(
            this BlockSyntax block,
            SyntaxKind declarationKind,
            LanguageVersion languageVersion,
            ExpressionBodyPreference preference,
            CancellationToken cancellationToken,
            [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression,
            out SyntaxToken semicolonToken)
        {
            // We can always use arrow-expression bodies in C# 7 or above.
            // We can also use them in C# 6, but only a select set of member kinds.
            var acceptableVersion =
                languageVersion >= LanguageVersion.CSharp7 ||
                (languageVersion >= LanguageVersion.CSharp6 && IsSupportedInCSharp6(declarationKind));

            if (acceptableVersion &&
                block.TryConvertToExpressionBody(languageVersion, preference, cancellationToken, out var expression, out semicolonToken))
            {
                arrowExpression = SyntaxFactory.ArrowExpressionClause(expression);

                var parent = block.GetRequiredParent();

                if (parent.Kind() == SyntaxKind.GetAccessorDeclaration)
                {
                    var comments = parent.GetLeadingTrivia().Where(t => !t.IsWhitespaceOrEndOfLine());
                    if (!comments.IsEmpty())
                    {
                        arrowExpression = arrowExpression.WithLeadingTrivia(
                            parent.GetLeadingTrivia());
                    }
                }

                return true;
            }

            arrowExpression = null;
            semicolonToken = default;
            return false;
        }

        private static bool IsSupportedInCSharp6(SyntaxKind declarationKind)
        {
            switch (declarationKind)
            {
                case SyntaxKind.ConstructorDeclaration:
                case SyntaxKind.DestructorDeclaration:
                case SyntaxKind.AddAccessorDeclaration:
                case SyntaxKind.RemoveAccessorDeclaration:
                case SyntaxKind.GetAccessorDeclaration:
                case SyntaxKind.SetAccessorDeclaration:
                    return false;
            }

            return true;
        }

        public static bool MatchesPreference(
            ExpressionSyntax expression, ExpressionBodyPreference preference)
        {
            if (preference == ExpressionBodyPreference.WhenPossible)
            {
                return true;
            }

            Contract.ThrowIfFalse(preference == ExpressionBodyPreference.WhenOnSingleLine);
            return CSharpSyntaxFacts.Instance.IsOnSingleLine(expression, fullSpan: false);
        }

        private static bool TryGetExpression(StatementSyntax firstStatement, LanguageVersion languageVersion, [NotNullWhen(true)] out ExpressionSyntax? expression, out SyntaxToken semicolonToken)
        {
            if (firstStatement is ExpressionStatementSyntax exprStatement)
            {
                expression = exprStatement.Expression;
                semicolonToken = exprStatement.SemicolonToken;
                return true;
            }
            else if (firstStatement is ReturnStatementSyntax returnStatement)
            {
                if (returnStatement.Expression != null)
                {
                    // If there are any comments or directives on the return keyword, move them to
                    // the expression.
                    expression = firstStatement.GetLeadingTrivia().Any(t => t.IsDirective || t.IsSingleOrMultiLineComment())
                        ? returnStatement.Expression.WithLeadingTrivia(returnStatement.GetLeadingTrivia())
                        : returnStatement.Expression;
                    semicolonToken = returnStatement.SemicolonToken;
                    return true;
                }
            }
            else if (firstStatement is ThrowStatementSyntax throwStatement)
            {
                if (languageVersion >= LanguageVersion.CSharp7 && throwStatement.Expression != null)
                {
                    expression = SyntaxFactory.ThrowExpression(throwStatement.ThrowKeyword, throwStatement.Expression);
                    semicolonToken = throwStatement.SemicolonToken;
                    return true;
                }
            }

            expression = null;
            semicolonToken = default;
            return false;
        }
    }
}
