// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram;

using static CSharpSyntaxTokens;
using static SyntaxFactory;

internal static partial class ConvertProgramTransform
{
    public static async Task<Document> ConvertToProgramMainAsync(Document document, AccessibilityModifiersRequired accessibilityModifiersRequired, CancellationToken cancellationToken)
    {
        // While the analyze and refactoring check ensure we're in a well formed state for their needs, the 'new
        // template' code just calls directly into this if the user prefers Program.Main.  So check and make sure
        // this is actually something we can convert before proceeding.
        var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        if (root.IsTopLevelProgram())
        {
            var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);

            var mainMethod = compilation.GetTopLevelStatementsMethod();
            if (mainMethod is not null)
            {
                var oldClassDeclaration = root.Members.OfType<ClassDeclarationSyntax>().FirstOrDefault(IsProgramClass);

                var classDeclaration = await GenerateProgramClassAsync(
                    document, oldClassDeclaration, mainMethod, accessibilityModifiersRequired, cancellationToken).ConfigureAwait(false);

                var newRoot = root.RemoveNodes(root.Members.OfType<GlobalStatementSyntax>().Skip(1), SyntaxGenerator.DefaultRemoveOptions);
                if (oldClassDeclaration is not null)
                {
                    Contract.ThrowIfNull(newRoot);
                    newRoot = newRoot.RemoveNode(oldClassDeclaration, SyntaxGenerator.DefaultRemoveOptions);
                }

                Contract.ThrowIfNull(newRoot);

                var firstGlobalStatement = newRoot.Members.OfType<GlobalStatementSyntax>().Single();
                newRoot = newRoot.ReplaceNode(firstGlobalStatement, classDeclaration);

                return document.WithSyntaxRoot(newRoot);
            }
        }

        return document;
    }

    private static bool IsProgramClass(ClassDeclarationSyntax declaration)
    {
        return declaration.Identifier.ValueText == WellKnownMemberNames.TopLevelStatementsEntryPointTypeName &&
               declaration.Modifiers.Any(SyntaxKind.PartialKeyword);
    }

    private static async Task<ClassDeclarationSyntax> GenerateProgramClassAsync(
        Document document,
        ClassDeclarationSyntax? oldClassDeclaration,
        IMethodSymbol mainMethod,
        AccessibilityModifiersRequired accessibilityModifiersRequired,
        CancellationToken cancellationToken)
    {
        var programType = mainMethod.ContainingType;

        // Respect user settings on if they want explicit or implicit accessibility modifiers.
        var useDeclaredAccessibility = accessibilityModifiersRequired is AccessibilityModifiersRequired.ForNonInterfaceMembers or AccessibilityModifiersRequired.Always;

        var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var generator = document.GetRequiredLanguageService<SyntaxGenerator>();

        // See if we have an existing part in another file.  If so, we'll have to generate our declaration as partial.
        var hasExistingPart = programType.DeclaringSyntaxReferences.Any(static (d, cancellationToken) => d.GetSyntax(cancellationToken) is TypeDeclarationSyntax, cancellationToken);

        var method = (MethodDeclarationSyntax)generator.MethodDeclaration(
            mainMethod, WellKnownMemberNames.EntryPointMethodName,
            GenerateProgramMainStatements(root, out var leadingTrivia));
        method = method.WithReturnType(method.ReturnType.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation));
        method = (MethodDeclarationSyntax)generator.WithAccessibility(
            method, useDeclaredAccessibility ? mainMethod.DeclaredAccessibility : Accessibility.NotApplicable);

        // Workaround for simplification not being ready when we generate a new file.  Substitute System.String[]
        // with string[].
        if (method.ParameterList.Parameters is [{ Type: ArrayTypeSyntax arrayType }])
            method = method.ReplaceNode(arrayType.ElementType, PredefinedType(StringKeyword));

        if (oldClassDeclaration is null)
        {
            // If we dodn't have any suitable class declaration in the same file then generate it
            return FixupComments((ClassDeclarationSyntax)generator.ClassDeclaration(
                WellKnownMemberNames.TopLevelStatementsEntryPointTypeName,
                accessibility: useDeclaredAccessibility ? programType.DeclaredAccessibility : Accessibility.NotApplicable,
                modifiers: hasExistingPart ? DeclarationModifiers.Partial : DeclarationModifiers.None,
                members: [method]).WithLeadingTrivia(leadingTrivia));
        }
        else
        {
            // Otherwise just add new member and process leading trivia

            // Old class declaration is below top-level statements and is probably separated from them with a blank line (or several ones).
            // So we want to remove all leading line to make class declaration begin from the first line of the file after applying refactoring
            var oldTriviaWithoutBlankLines = oldClassDeclaration.GetLeadingTrivia().WithoutLeadingBlankLines();
            return oldClassDeclaration.WithMembers(oldClassDeclaration.Members.Add(method))
                                      .WithLeadingTrivia(oldTriviaWithoutBlankLines.Union(leadingTrivia));
        }
    }

    private static ImmutableArray<StatementSyntax> GenerateProgramMainStatements(
        CompilationUnitSyntax root, out SyntaxTriviaList triviaToMove)
    {
        using var _ = ArrayBuilder<StatementSyntax>.GetInstance(out var statements);

        triviaToMove = default;
        var first = true;
        foreach (var globalStatement in root.Members.OfType<GlobalStatementSyntax>())
        {
            // Remove leading trivia from first statement.  We'll move it to the Program type. Any directly attached
            // comments though stay attached to the first statement.
            var statement = globalStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation);
            if (first)
            {
                first = false;

                triviaToMove = statement.GetLeadingTrivia();
                while (triviaToMove is [.., SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia), SyntaxTrivia(SyntaxKind.EndOfLineTrivia)])
                    triviaToMove = [.. triviaToMove.Take(triviaToMove.Count - 2)];

                var commentsToPreserve = TriviaList(statement.GetLeadingTrivia().Skip(triviaToMove.Count));
                statements.Add(FixupComments(statement.WithLeadingTrivia(commentsToPreserve)));
            }
            else
            {
                statements.Add(statement);
            }
        }

        return statements.ToImmutableAndClear();
    }

    private static TSyntaxNode FixupComments<TSyntaxNode>(TSyntaxNode node) where TSyntaxNode : SyntaxNode
    {
        // Remove comment explaining top level statements as it isn't relevant if the user switches back to full
        // Program.Main form.
        var leadingTrivia = node.GetLeadingTrivia();
        var comment = leadingTrivia.FirstOrNull(
            c => c.Kind() is SyntaxKind.SingleLineCommentTrivia && c.ToString().Contains("https://aka.ms/new-console-template"));
        if (comment == null)
            return node;

        var commentIndex = leadingTrivia.IndexOf(comment.Value);
        leadingTrivia = leadingTrivia.RemoveAt(commentIndex);

        while (commentIndex < leadingTrivia.Count && leadingTrivia[commentIndex].Kind() is SyntaxKind.EndOfLineTrivia)
            leadingTrivia = leadingTrivia.RemoveAt(commentIndex);

        return node.WithLeadingTrivia(leadingTrivia);
    }
}
