// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.DotNet.GenAPI.SyntaxRewriter
{
    /// <summary>
    /// Represents a <see cref="CSharpSyntaxVisitor{TResult}"/> which descends an entire <see cref="CSharpSyntaxNode"/> graph and
    /// modify visited constructor, method declarations SyntaxNodes in depth-first order.
    /// Rewrites body with default implementation details.
    /// </summary>
    public class BodyBlockCSharpSyntaxRewriter(string? _exceptionMessage) : CSharpSyntaxRewriter
    {
        /// <inheritdoc />
        public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
        {
            // visit subtree first to normalize type names.
            if (base.VisitConstructorDeclaration(node) is not ConstructorDeclarationSyntax rs)
                return null;

            // if there is at least one reference parameter - generate non empty body.
            if (node.ParameterList.Parameters.Any(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.OutKeyword))))
            {
                rs = rs.WithBody(GetThrowNullBody());
            }
            else
            {
                rs = rs.WithBody(GetEmptyBody());
            }

            return rs;
        }

        /// <inheritdoc />
        public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
        {
            // visit subtree first to normalize type names.
            if (base.VisitMethodDeclaration(node) is not MethodDeclarationSyntax rs)
                return null;

            if (rs.Modifiers.Any(token => token.IsKind(SyntaxKind.AbstractKeyword)) || rs.Body is null)
            {
                return rs;
            }

            if (rs.ExpressionBody is not null)
            {
                rs = rs.WithExpressionBody(null);
            }

            if (!(rs.ReturnType is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword)) ||
                // if there is at least one reference parameter - generate non empty body.
                node.ParameterList.Parameters.Any(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.OutKeyword))))
            {
                rs = rs.WithBody(GetThrowNullBody());
            }
            else
            {
                rs = rs.WithBody(GetEmptyBody());
            }

            return rs.WithParameterList(rs.ParameterList.WithTrailingTrivia(SyntaxFactory.Space));
        }

        /// <inheritdoc />
        public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node)
        {
            // visit subtree first to normalize type names.
            return base.VisitOperatorDeclaration(node) is OperatorDeclarationSyntax rs ?
                rs.WithBody(GetThrowNullBody()).WithParameterList(rs.ParameterList.WithTrailingTrivia(SyntaxFactory.Space)) :
                null;
        }

        /// <inheritdoc />
        public override SyntaxNode? VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node)
        {
            return base.VisitConversionOperatorDeclaration(node) is ConversionOperatorDeclarationSyntax rs ?
                rs.WithBody(GetThrowNullBody()).WithParameterList(rs.ParameterList.WithTrailingTrivia(SyntaxFactory.Space)) :
                null;
        }

        /// <inheritdoc />
        public override SyntaxNode? VisitAccessorDeclaration(AccessorDeclarationSyntax node)
        {
            switch (node.Kind())
            {
                case SyntaxKind.GetAccessorDeclaration:
                case SyntaxKind.InitAccessorDeclaration:
                case SyntaxKind.SetAccessorDeclaration:
                    {
                        var accessorListSyntax = (AccessorListSyntax?)node.Parent;
                        if (accessorListSyntax?.Parent == null) break;

                        if (accessorListSyntax?.Parent is IndexerDeclarationSyntax indexerDeclarationSyntax)
                        {
                            var typeDeclarationSyntax = (TypeDeclarationSyntax?)indexerDeclarationSyntax.Parent;

                            if (indexerDeclarationSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AbstractKeyword)) ||
                                (typeDeclarationSyntax != null && typeDeclarationSyntax.Keyword.IsKind(SyntaxKind.InterfaceKeyword)))
                            {
                                return node.WithSemicolonToken(node.SemicolonToken);
                            }

                            return ProcessPropertyDeclarationSyntax(node);
                        }
                        else if (accessorListSyntax?.Parent is PropertyDeclarationSyntax propertyDeclarationSyntax)
                        {
                            var typeDeclarationSyntax = (TypeDeclarationSyntax?)propertyDeclarationSyntax.Parent;

                            if (propertyDeclarationSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AbstractKeyword)) ||
                                (typeDeclarationSyntax != null && typeDeclarationSyntax.Keyword.IsKind(SyntaxKind.InterfaceKeyword)))
                            {
                                return node.WithSemicolonToken(node.SemicolonToken);
                            }

                            return ProcessPropertyDeclarationSyntax(node);
                        }
                    }
                    break;
            }

            return base.VisitAccessorDeclaration(node);
        }

        private static BlockSyntax GetEmptyBody()
        {
            BlockSyntax node = GetMethodBodyFromText(SyntaxFactory.Space.ToString());
            return node.WithOpenBraceToken(node.OpenBraceToken.WithTrailingTrivia(SyntaxFactory.Space));
        }

        private BlockSyntax GetThrowNullBody() =>
            _exceptionMessage is not null ?
                GetMethodBodyFromText($"throw new PlatformNotSupportedException(\"{_exceptionMessage}\");") :
                GetMethodBodyFromText("throw null;");

        private AccessorDeclarationSyntax? ProcessPropertyDeclarationSyntax(AccessorDeclarationSyntax node)
        {
            if (node.Kind() == SyntaxKind.GetAccessorDeclaration)
            {
                node = node.WithBody(GetThrowNullBody());
            }
            else if (node.Kind() is SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration)
            {
                node = node.WithBody(GetEmptyBody());
            }

            return node.WithSemicolonToken(default)
                .WithKeyword(node.Keyword.WithTrailingTrivia(SyntaxFactory.Space));
        }

        private static BlockSyntax GetMethodBodyFromText(string text) =>
            SyntaxFactory.Block(SyntaxFactory.ParseStatement(text))
                .WithTrailingTrivia(SyntaxFactory.Space);
    }
}
