// 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 Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements
{
    using static ConvertProgramAnalysis;

    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    internal sealed class ConvertToTopLevelStatementsDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    {
        public ConvertToTopLevelStatementsDiagnosticAnalyzer()
            : base(
                  IDEDiagnosticIds.UseTopLevelStatementsId,
                  EnforceOnBuildValues.UseTopLevelStatements,
                  CSharpCodeStyleOptions.PreferTopLevelStatements,
                  new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_top_level_statements), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
        {
        }

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

        protected override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(context =>
            {
                // can only suggest moving to top level statement on c# 9 or above.
                if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp9 ||
                    !IsApplication(context.Compilation))
                {
                    return;
                }

                context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit);
            });
        }

        private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context)
        {
            // Don't want to suggest moving if the user doesn't have a preference for top-level-statements.
            var option = context.GetCSharpAnalyzerOptions().PreferTopLevelStatements;
            if (!CanOfferUseTopLevelStatements(option, forAnalyzer: true))
                return;

            var cancellationToken = context.CancellationToken;
            var semanticModel = context.SemanticModel;
            var compilation = semanticModel.Compilation;
            var mainTypeName = GetMainTypeName(compilation);

            // Ok, the user does like top level statements.  Check if we can find a suitable hit in this type that
            // indicates we're on the entrypoint of the program.
            var root = (CompilationUnitSyntax)context.Node;
            var methodDeclarations = root.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or ClassDeclarationSyntax).OfType<MethodDeclarationSyntax>();
            foreach (var methodDeclaration in methodDeclarations)
            {
                if (IsProgramMainMethod(
                        semanticModel, methodDeclaration, mainTypeName, cancellationToken, out var canConvertToTopLevelStatement))
                {
                    if (canConvertToTopLevelStatement)
                    {
                        // Looks good.  Let the user know this type/method can be converted to a top level program.
                        var severity = option.Notification.Severity;
                        context.ReportDiagnostic(DiagnosticHelper.Create(
                            this.Descriptor,
                            GetUseTopLevelStatementsDiagnosticLocation(
                                methodDeclaration, isHidden: severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden),
                            severity,
                            ImmutableArray.Create(methodDeclaration.GetLocation()),
                            ImmutableDictionary<string, string?>.Empty));
                    }

                    // We found the main method, but it's not convertible, bail out as we have nothing else to do.
                    return;
                }
            }
        }
    }
}
