// 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;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.SyncNamespaces;

internal abstract class AbstractSyncNamespacesService<TSyntaxKind, TNamespaceSyntax>
    : ISyncNamespacesService
    where TSyntaxKind : struct
    where TNamespaceSyntax : SyntaxNode
{
    public abstract AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<TSyntaxKind, TNamespaceSyntax> DiagnosticAnalyzer { get; }
    public abstract bool IsHostAnalyzer { get; }
    public abstract AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; }

    /// <inheritdoc/>
    public async Task<Solution> SyncNamespacesAsync(
        ImmutableArray<Project> projects,
        IProgress<CodeAnalysisProgress> progressTracker,
        CancellationToken cancellationToken)
    {
        // all projects must be of the same language
        Debug.Assert(projects.All(project => project.Language == projects[0].Language));

        var solution = projects[0].Solution;
        var diagnosticAnalyzers = ImmutableArray.Create<DiagnosticAnalyzer>(DiagnosticAnalyzer);
        var diagnosticsByProject = await GetDiagnosticsByProjectAsync(projects, diagnosticAnalyzers, IsHostAnalyzer, cancellationToken).ConfigureAwait(false);

        // If no diagnostics are reported, then there is nothing to fix.
        if (diagnosticsByProject.Values.All(diagnostics => diagnostics.IsEmpty))
        {
            return solution;
        }

        var fixAllContext = await GetFixAllContextAsync(
            solution, CodeFixProvider, diagnosticsByProject, progressTracker, cancellationToken).ConfigureAwait(false);
        var fixAllProvider = CodeFixProvider.GetFixAllProvider();
        RoslynDebug.AssertNotNull(fixAllProvider);

        return await ApplyCodeFixAsync(fixAllProvider, fixAllContext, cancellationToken).ConfigureAwait(false);
    }

    private static async Task<ImmutableDictionary<Project, ImmutableArray<Diagnostic>>> GetDiagnosticsByProjectAsync(
        ImmutableArray<Project> projects,
        ImmutableArray<DiagnosticAnalyzer> diagnosticAnalyzers,
        bool isHostAnalyzer,
        CancellationToken cancellationToken)
    {
        var builder = ImmutableDictionary.CreateBuilder<Project, ImmutableArray<Diagnostic>>();

        foreach (var project in projects)
        {
            var diagnostics = await GetDiagnosticsAsync(project, diagnosticAnalyzers, isHostAnalyzer, cancellationToken).ConfigureAwait(false);
            builder.Add(project, diagnostics);
        }

        return builder.ToImmutable();
    }

    private static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(
        Project project,
        ImmutableArray<DiagnosticAnalyzer> diagnosticAnalyzers,
        bool isHostAnalyzer,
        CancellationToken cancellationToken)
    {
        var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
        RoslynDebug.AssertNotNull(compilation);

        var analyzerOptions = new CompilationWithAnalyzersOptions(
            isHostAnalyzer ? project.HostAnalyzerOptions : project.AnalyzerOptions,
            onAnalyzerException: null,
            concurrentAnalysis: true,
            logAnalyzerExecutionTime: false,
            reportSuppressedDiagnostics: false);
        var analyzerCompilation = compilation.WithAnalyzers(diagnosticAnalyzers, analyzerOptions);

        return await analyzerCompilation.GetAnalyzerDiagnosticsAsync(cancellationToken).ConfigureAwait(false);
    }

    private static async Task<FixAllContext> GetFixAllContextAsync(
        Solution solution,
        CodeFixProvider codeFixProvider,
        ImmutableDictionary<Project, ImmutableArray<Diagnostic>> diagnosticsByProject,
        IProgress<CodeAnalysisProgress> progressTracker,
        CancellationToken cancellationToken)
    {
        var diagnosticProvider = new DiagnosticProvider(diagnosticsByProject);

        var firstDiagnostic = diagnosticsByProject
            .SelectMany(kvp => kvp.Value)
            .FirstOrDefault();
        RoslynDebug.AssertNotNull(firstDiagnostic?.Location?.SourceTree);

        var document = solution.GetRequiredDocument(firstDiagnostic.Location.SourceTree);

        // This will allow us access to the equivalence key
        CodeAction? action = null;
        var context = new CodeFixContext(
            document,
            firstDiagnostic.Location.SourceSpan,
            [firstDiagnostic],
            (a, _) => action ??= a,
            cancellationToken);
        await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false);

        return new FixAllContext(
            new FixAllState(
                fixAllProvider: NoOpFixAllProvider.Instance,
                diagnosticSpan: firstDiagnostic.Location.SourceSpan,
                document,
                document.Project,
                codeFixProvider,
                FixAllScope.Solution,
                codeActionEquivalenceKey: action?.EquivalenceKey, // FixAllState supports null equivalence key. This should still be supported.
                diagnosticIds: codeFixProvider.FixableDiagnosticIds,
                fixAllDiagnosticProvider: diagnosticProvider),
            progressTracker,
            cancellationToken);
    }

    private static async Task<Solution> ApplyCodeFixAsync(
        FixAllProvider fixAllProvider,
        FixAllContext fixAllContext,
        CancellationToken cancellationToken)
    {
        var fixAllAction = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false);
        RoslynDebug.AssertNotNull(fixAllAction);

        var operations = await fixAllAction.GetOperationsAsync(
            fixAllContext.Solution, fixAllContext.Progress, cancellationToken).ConfigureAwait(false);
        var applyChangesOperation = operations.OfType<ApplyChangesOperation>().SingleOrDefault();
        RoslynDebug.AssertNotNull(applyChangesOperation);

        return applyChangesOperation.ChangedSolution;
    }

    private sealed class DiagnosticProvider : FixAllContext.DiagnosticProvider
    {
        private static readonly Task<IEnumerable<Diagnostic>> EmptyDiagnosticResult = Task.FromResult(Enumerable.Empty<Diagnostic>());

        private readonly ImmutableDictionary<Project, ImmutableArray<Diagnostic>> _diagnosticsByProject;

        internal DiagnosticProvider(ImmutableDictionary<Project, ImmutableArray<Diagnostic>> diagnosticsByProject)
        {
            _diagnosticsByProject = diagnosticsByProject;
        }

        public override Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken)
        {
            return GetProjectDiagnosticsAsync(project, cancellationToken);
        }

        public override async Task<IEnumerable<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken)
        {
            var projectDiagnostics = await GetProjectDiagnosticsAsync(document.Project, cancellationToken).ConfigureAwait(false);
            return projectDiagnostics
                .WhereAsArray(diagnostic => diagnostic.Location.SourceTree?.FilePath == document.FilePath);
        }

        public override Task<IEnumerable<Diagnostic>> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken)
        {
            return _diagnosticsByProject.TryGetValue(project, out var diagnostics)
                ? Task.FromResult<IEnumerable<Diagnostic>>(diagnostics)
                : EmptyDiagnosticResult;
        }
    }
}
