using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace RoslynPad.Roslyn.CodeFixes
{
    [Export(typeof(ICodeFixService))]
    [Shared]
    internal sealed class CodeFixService : ICodeFixService
    {
        private readonly Internal.CodeFixService _inner;

        [ImportingConstructor]
        public CodeFixService(Internal.CodeFixService inner) => _inner = inner;

        public async Task<IEnumerable<CodeFixCollection>> GetFixesAsync(Document document, TextSpan textSpan, bool includeSuppressionFixes,
            CancellationToken cancellationToken)
        {
            var result = await _inner.GetFixesAsync(document, textSpan, includeSuppressionFixes, cancellationToken).ConfigureAwait(false);
            return result.Select(x => new CodeFixCollection(x)).ToImmutableArray();
        }

        public CodeFixProvider? GetSuppressionFixer(string language, IEnumerable<string> diagnosticIds) =>
            _inner.GetSuppressionFixer(language, diagnosticIds);
    }

    // from EditorFeatures
    namespace Internal
    {
        using DiagnosticId = String;
        using LanguageKind = String;

        [Export]
        [Shared]
        internal class CodeFixService
        {
            private static readonly Func<string, List<CodeFixProvider>> s_createList = _ => new List<CodeFixProvider>();

            private readonly ConditionalWeakTable<AnalyzerReference, ProjectCodeFixProvider> _analyzerReferenceToFixersMap;

            private readonly ImmutableDictionary<string, Lazy<ImmutableArray<IConfigurationFixProvider>>> _configurationProvidersMap;
            private readonly ConditionalWeakTable<AnalyzerReference, ProjectCodeFixProvider>.CreateValueCallback _createProjectCodeFixProvider;
            private readonly IDiagnosticAnalyzerService _diagnosticService;

            private readonly ImmutableDictionary<string, Lazy<ImmutableDictionary<CodeFixProvider, int>>> _fixerPriorityMap;

            private readonly ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, ImmutableDictionary<string, List<CodeFixProvider>>>
                _projectFixersMap;

            private readonly ImmutableDictionary<string, Lazy<ImmutableDictionary<string, ImmutableArray<CodeFixProvider>>>> _workspaceFixersMap;

            private ImmutableDictionary<object, FixAllProviderInfo?> _fixAllProviderMap;

            // Shared by project fixers and workspace fixers.
            private ImmutableDictionary<CodeFixProvider, ImmutableArray<string>> _fixerToFixableIdsMap =
                ImmutableDictionary<CodeFixProvider, ImmutableArray<string>>.Empty;

            [ImportingConstructor]
            public CodeFixService(
                IDiagnosticAnalyzerService service,
                [ImportMany] IEnumerable<Lazy<CodeFixProvider, CodeChangeProviderMetadata>> fixers,
                [ImportMany] IEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>> configurationProviders)
            {
                _diagnosticService = service;
                var fixersPerLanguageMap = fixers.ToPerLanguageMapWithMultipleLanguages();
                var configurationProvidersPerLanguageMap = configurationProviders.ToPerLanguageMapWithMultipleLanguages();

                _workspaceFixersMap = GetFixerPerLanguageMap(fixersPerLanguageMap, null);
                _configurationProvidersMap = GetConfigurationProvidersPerLanguageMap(configurationProvidersPerLanguageMap);

                // REVIEW: currently, fixer's priority is statically defined by the fixer itself. might considering making it more dynamic or configurable.
                _fixerPriorityMap = GetFixerPriorityPerLanguageMap(fixersPerLanguageMap);

                // Per-project fixers
                _projectFixersMap = new ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, ImmutableDictionary<string, List<CodeFixProvider>>>();
                _analyzerReferenceToFixersMap = new ConditionalWeakTable<AnalyzerReference, ProjectCodeFixProvider>();
                _createProjectCodeFixProvider = r => new ProjectCodeFixProvider(r);
                _fixAllProviderMap = ImmutableDictionary<object, FixAllProviderInfo?>.Empty;
            }

            public async Task<Microsoft.CodeAnalysis.CodeFixes.FirstDiagnosticResult> GetMostSevereFixableDiagnosticAsync(
                Document document, TextSpan range, CancellationToken cancellationToken)
            {
                if (document == null || !document.IsOpen())
                {
                    return default;
                }

                using var diagnostics = SharedPools.Default<ArrayBuilder<DiagnosticData>>().GetPooledObject();
                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

                var linkedToken = linkedTokenSource.Token;

                // This flag is used by SuggestedActionsSource to track what solution is was
                // last able to get "full results" for.
                var isFullResult = await _diagnosticService.TryAppendDiagnosticsForSpanAsync(
                    document, range, diagnostics.Object, cancellationToken: linkedToken).ConfigureAwait(false);

                var errorDiagnostics = diagnostics.Object.Where(d => d.Severity == DiagnosticSeverity.Error);
                var otherDiagnostics = diagnostics.Object.Where(d => d.Severity != DiagnosticSeverity.Error);

                // Kick off a task that will determine there's an Error Diagnostic with a fixer
                var errorDiagnosticsTask = Task.Run(
                    () => GetFirstDiagnosticWithFixAsync(document, errorDiagnostics, range, linkedToken),
                    linkedToken);

                // Kick off a task that will determine if any non-Error Diagnostic has a fixer
                var otherDiagnosticsTask = Task.Run(
                    () => GetFirstDiagnosticWithFixAsync(document, otherDiagnostics, range, linkedToken),
                    linkedToken);

                // If the error diagnostics task happens to complete with a non-null result before
                // the other diagnostics task, we can cancel the other task.
                var diagnostic = await errorDiagnosticsTask.ConfigureAwait(false)
                                 ?? await otherDiagnosticsTask.ConfigureAwait(false);
                linkedTokenSource.Cancel();

                return new Microsoft.CodeAnalysis.CodeFixes.FirstDiagnosticResult(!isFullResult,
                    diagnostic != null,
                    diagnostic);
            }

            private async Task<DiagnosticData?> GetFirstDiagnosticWithFixAsync(
                Document document,
                IEnumerable<DiagnosticData> severityGroup,
                TextSpan range,
                CancellationToken cancellationToken)
            {
                foreach (var diagnostic in severityGroup)
                {
                    if (!range.IntersectsWith(diagnostic.GetTextSpan()))
                    {
                        continue;
                    }

                    if (await ContainsAnyFixAsync(document, diagnostic, cancellationToken).ConfigureAwait(false))
                    {
                        return diagnostic;
                    }
                }

                return null;
            }

            public Task<ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection>> GetFixesAsync(Document document, TextSpan range,
                bool includeConfigurationFixes, CancellationToken cancellationToken) =>
                GetFixesAsync(document, range, includeConfigurationFixes, false, cancellationToken);

            public async Task<ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection>> GetFixesAsync(Document document, TextSpan range,
                bool includeConfigurationFixes, bool isBlocking, CancellationToken cancellationToken)
            {
                // REVIEW: this is the first and simplest design. basically, when ctrl+. is pressed, it asks diagnostic service to give back
                // current diagnostics for the given span, and it will use that to get fixes. internally diagnostic service will either return cached information
                // (if it is up-to-date) or synchronously do the work at the spot.
                //
                // this design's weakness is that each side don't have enough information to narrow down works to do. it will most likely always do more works than needed.
                // sometimes way more than it is needed. (compilation)

                // group diagnostics by their diagnostics span
                // invariant: later code gathers & runs CodeFixProviders for diagnostics with one identical diagnostics span (that gets set later as CodeFixCollection's TextSpan)
                Dictionary<TextSpan, List<DiagnosticData>>? aggregatedDiagnostics = null;
                foreach (var diagnostic in await _diagnosticService.GetDiagnosticsForSpanAsync(document, range,
                             includeSuppressedDiagnostics: includeConfigurationFixes, cancellationToken: cancellationToken).ConfigureAwait(false))
                {
                    if (diagnostic.IsSuppressed)
                    {
                        continue;
                    }

                    cancellationToken.ThrowIfCancellationRequested();

                    aggregatedDiagnostics ??= new Dictionary<TextSpan, List<DiagnosticData>>();
                    aggregatedDiagnostics.GetOrAdd(diagnostic.GetTextSpan(), _ => new List<DiagnosticData>()).Add(diagnostic);
                }

                if (aggregatedDiagnostics == null)
                {
                    return ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection>.Empty;
                }

                // append fixes for all diagnostics with the same diagnostics span
                using var resultDisposer = ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection>.GetInstance(out var result);
                foreach (var spanAndDiagnostic in aggregatedDiagnostics)
                {
                    await AppendFixesAsync(
                        document, spanAndDiagnostic.Key, spanAndDiagnostic.Value, false, isBlocking,
                        result, cancellationToken).ConfigureAwait(false);
                }

                if (result.Count > 0)
                {
                    // sort the result to the order defined by the fixers
                    var priorityMap = _fixerPriorityMap[document.Project.Language].Value;
                    result.Sort((d1, d2) => GetValue(d1).CompareTo(GetValue(d2)));

                    int GetValue(Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection c)
                    {
                        return priorityMap!.TryGetValue((CodeFixProvider)c.Provider, out var value) ? value : int.MaxValue;
                    }
                }

                if (includeConfigurationFixes)
                {
                    foreach (var spanAndDiagnostic in aggregatedDiagnostics)
                    {
                        await AppendConfigurationsAsync(
                            document, spanAndDiagnostic.Key, spanAndDiagnostic.Value,
                            result, cancellationToken).ConfigureAwait(false);
                    }
                }

                return result.ToImmutable();
            }

            public async Task<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection?> GetDocumentFixAllForIdInSpanAsync(Document document,
                TextSpan range, string diagnosticId, CancellationToken cancellationToken)
            {
                var diagnostics = (await _diagnosticService
                        .GetDiagnosticsForSpanAsync(document, range, diagnosticId, false, cancellationToken: cancellationToken).ConfigureAwait(false))
                    .ToList();
                if (diagnostics.Count == 0)
                {
                    return null;
                }

                using var resultDisposer = ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection>.GetInstance(out var result);
                await AppendFixesAsync(document, range, diagnostics, true, false, result, cancellationToken).ConfigureAwait(false);

                // TODO: Just get the first fix for now until we have a way to config user's preferred fix
                // https://github.com/dotnet/roslyn/issues/27066
                return result.ToImmutable().FirstOrDefault();
            }

            public async Task<Document> ApplyCodeFixesForSpecificDiagnosticIdAsync(Document document, string diagnosticId,
                IProgressTracker progressTracker, CancellationToken cancellationToken)
            {
                var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var textSpan = new TextSpan(0, tree!.Length);

                var fixCollection = await GetDocumentFixAllForIdInSpanAsync(
                    document, textSpan, diagnosticId, cancellationToken).ConfigureAwait(false);
                if (fixCollection == null)
                {
                    return document;
                }

                var fixAllService = document.Project.Solution.Workspace.Services.GetService<IFixAllGetFixesService>();
                var solution = await fixAllService!.GetFixAllChangedSolutionAsync(
                    new FixAllContext(fixCollection.FixAllState, progressTracker, cancellationToken)).ConfigureAwait(false);

                return solution.GetDocument(document.Id)!;
            }

            private async Task AppendFixesAsync(
                Document document,
                TextSpan span,
                IEnumerable<DiagnosticData> diagnostics,
                bool fixAllForInSpan,
                bool isBlocking,
                ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection> result,
                CancellationToken cancellationToken)
            {
                var hasAnySharedFixer = _workspaceFixersMap.TryGetValue(document.Project.Language, out var fixerMap);

                var projectFixersMap = GetProjectFixers(document.Project);
                var hasAnyProjectFixer = projectFixersMap.Any();

                if (!hasAnySharedFixer && !hasAnyProjectFixer)
                {
                    return;
                }

                var allFixers = new List<CodeFixProvider>();

                // gather CodeFixProviders for all distinct diagnostics found for current span
                foreach (var diagnosticId in diagnostics.Select(d => d.Id).Distinct())
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    if (hasAnySharedFixer && fixerMap!.Value.TryGetValue(diagnosticId, out var workspaceFixers))
                    {
                        allFixers.AddRange(workspaceFixers);
                    }

                    if (hasAnyProjectFixer && projectFixersMap.TryGetValue(diagnosticId, out var projectFixers))
                    {
                        allFixers.AddRange(projectFixers);
                    }
                }

                var extensionManager = document.Project.Solution.Workspace.Services.GetService<IExtensionManager>();

                // run each CodeFixProvider to gather individual CodeFixes for reported diagnostics
                foreach (var fixer in allFixers.Distinct())
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    await AppendFixesOrConfigurationsAsync(
                        document, span, diagnostics, fixAllForInSpan, result, fixer,
                        d => GetFixableDiagnosticIds(fixer, extensionManager).Contains(d.Id),
                        dxs =>
                        {
                            if (fixAllForInSpan)
                            {
                                var primaryDiagnostic = dxs.First();
                                return GetCodeFixesAsync(document, primaryDiagnostic.Location.SourceSpan, fixer, isBlocking,
                                    ImmutableArray.Create(primaryDiagnostic), cancellationToken);
                            }

                            return GetCodeFixesAsync(document, span, fixer, isBlocking, dxs, cancellationToken);
                        },
                        cancellationToken).ConfigureAwait(false);

                    // Just need the first result if we are doing fix all in span
                    if (fixAllForInSpan && result.Any())
                    {
                        return;
                    }
                }
            }

            private async Task<ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFix>> GetCodeFixesAsync(
                Document document, TextSpan span, CodeFixProvider fixer, bool isBlocking,
                ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
            {
                using var fixesDisposer = ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFix>.GetInstance(out var fixes);
                var context = new CodeFixContext(document, span, diagnostics,
                    // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs?
                    (action, applicableDiagnostics) =>
                    {
                        // Serialize access for thread safety - we don't know what thread the fix provider will call this delegate from.
                        lock (fixes)
                        {
                            fixes.Add(new Microsoft.CodeAnalysis.CodeFixes.CodeFix(document.Project, action, applicableDiagnostics));
                        }
                    },
                    false,
                    isBlocking,
                    cancellationToken);

                var task = fixer.RegisterCodeFixesAsync(context) ?? Task.CompletedTask;
                await task.ConfigureAwait(false);
                return fixes.ToImmutable();
            }

            private async Task AppendConfigurationsAsync(
                Document document, TextSpan diagnosticsSpan, IEnumerable<DiagnosticData> diagnostics,
                ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection> result, CancellationToken cancellationToken)
            {
                if (!_configurationProvidersMap.TryGetValue(document.Project.Language, out var lazyConfigurationProviders) ||
                    lazyConfigurationProviders.Value == null)
                {
                    return;
                }

                // append CodeFixCollection for each CodeFixProvider
                foreach (var provider in lazyConfigurationProviders.Value)
                {
                    await AppendFixesOrConfigurationsAsync(
                        document, diagnosticsSpan, diagnostics, false, result, provider,
                        d => provider.IsFixableDiagnostic(d),
                        dxs => provider.GetFixesAsync(
                            document, diagnosticsSpan, dxs, cancellationToken),
                        cancellationToken).ConfigureAwait(false);
                }
            }

            private async Task AppendFixesOrConfigurationsAsync<TCodeFixProvider>(
                Document document,
                TextSpan fixesSpan,
                IEnumerable<DiagnosticData> diagnosticsWithSameSpan,
                bool fixAllForInSpan,
                ArrayBuilder<Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection> result,
                TCodeFixProvider fixer,
                Func<Diagnostic, bool> hasFix,
                Func<ImmutableArray<Diagnostic>, Task<ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFix>>> getFixes,
                CancellationToken cancellationToken)
            {
                var allDiagnostics =
                    await diagnosticsWithSameSpan.OrderByDescending(d => d.Severity)
                        .ToDiagnosticsAsync(document.Project, cancellationToken).ConfigureAwait(false);
                var diagnostics = allDiagnostics.WhereAsArray(hasFix);
                if (diagnostics.Length <= 0)
                {
                    // this can happen for suppression case where all diagnostics can't be suppressed
                    return;
                }

                var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService<IExtensionManager>();
                var fixes = await extensionManager.PerformFunctionAsync(fixer!,
                    () => getFixes(diagnostics),
                    ImmutableArray<Microsoft.CodeAnalysis.CodeFixes.CodeFix>.Empty).ConfigureAwait(false);

                if (fixes.IsDefaultOrEmpty)
                {
                    return;
                }

                // If the fix provider supports fix all occurrences, then get the corresponding FixAllProviderInfo and fix all context.
                var fixAllProviderInfo = extensionManager.PerformFunction(fixer!,
                    () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, fixer!, FixAllProviderInfo.Create), null!);

                FixAllState? fixAllState = null;
                var supportedScopes = ImmutableArray<FixAllScope>.Empty;
                if (fixAllProviderInfo != null)
                {
                    var codeFixProvider = fixer as CodeFixProvider ??
                                          new WrapperCodeFixProvider((IConfigurationFixProvider)fixer!, diagnostics.Select(d => d.Id));

                    var diagnosticIds = diagnostics.Where(fixAllProviderInfo.CanBeFixed)
                        .Select(d => d.Id)
                        .ToImmutableHashSet();

                    var diagnosticProvider = fixAllForInSpan
                        ? new FixAllPredefinedDiagnosticProvider(allDiagnostics)
                        : (FixAllContext.DiagnosticProvider)new FixAllDiagnosticProvider(this, diagnosticIds);

                    fixAllState = new FixAllState(
                        fixAllProviderInfo.FixAllProvider,
                        document,
                        codeFixProvider,
                        FixAllScope.Document,
                        null,
                        diagnosticIds,
                        diagnosticProvider);

                    supportedScopes = fixAllProviderInfo.SupportedScopes;
                }

                var codeFix = new Microsoft.CodeAnalysis.CodeFixes.CodeFixCollection(
                    fixer, fixesSpan, fixes, fixAllState,
                    supportedScopes, diagnostics.First());
                result.Add(codeFix);
            }

            /// <summary> Looks explicitly for an <see cref="AbstractSuppressionCodeFixProvider" />.</summary>
            public CodeFixProvider? GetSuppressionFixer(string language, IEnumerable<string> diagnosticIds)
            {
                if (!_configurationProvidersMap.TryGetValue(language, out var lazyConfigurationProviders) ||
                    lazyConfigurationProviders.Value.IsDefault)
                {
                    return null;
                }

                // Explicitly looks for an AbstractSuppressionCodeFixProvider
                var fixer = lazyConfigurationProviders.Value.OfType<AbstractSuppressionCodeFixProvider>().FirstOrDefault();
                if (fixer == null)
                {
                    return null;
                }

                return new WrapperCodeFixProvider(fixer, diagnosticIds);
            }

            private async Task<IEnumerable<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, ImmutableHashSet<string> diagnosticIds,
                CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(document);
                var solution = document.Project.Solution;
                var diagnostics = await _diagnosticService
                    .GetDiagnosticsForIdsAsync(solution, null, document.Id, diagnosticIds, cancellationToken: cancellationToken)
                    .ConfigureAwait(false);
                Contract.ThrowIfFalse(diagnostics.All(d => d.DocumentId != null));
                return await diagnostics.ToDiagnosticsAsync(document.Project, cancellationToken).ConfigureAwait(false);
            }

            private async Task<IEnumerable<Diagnostic>> GetProjectDiagnosticsAsync(Project project, bool includeAllDocumentDiagnostics,
                ImmutableHashSet<string> diagnosticIds, CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(project);

                if (includeAllDocumentDiagnostics)
                {
                    // Get all diagnostics for the entire project, including document diagnostics.
                    var diagnostics = await _diagnosticService
                        .GetDiagnosticsForIdsAsync(project.Solution, project.Id, diagnosticIds: diagnosticIds, cancellationToken: cancellationToken)
                        .ConfigureAwait(false);
                    return await diagnostics.ToDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    // Get all no-location diagnostics for the project, doesn't include document diagnostics.
                    var diagnostics = await _diagnosticService
                        .GetProjectDiagnosticsForIdsAsync(project.Solution, project.Id, diagnosticIds, cancellationToken: cancellationToken)
                        .ConfigureAwait(false);
                    Contract.ThrowIfFalse(diagnostics.All(d => d.DocumentId == null));
                    return await diagnostics.ToDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false);
                }
            }

            private async Task<bool> ContainsAnyFixAsync(
                Document document, DiagnosticData diagnostic, CancellationToken cancellationToken)
            {
                var workspaceFixers = ImmutableArray<CodeFixProvider>.Empty;
                var hasAnySharedFixer = _workspaceFixersMap.TryGetValue(document.Project.Language, out var fixerMap) &&
                                        fixerMap.Value.TryGetValue(diagnostic.Id, out workspaceFixers);
                var hasAnyProjectFixer = GetProjectFixers(document.Project).TryGetValue(diagnostic.Id, out var projectFixers);

                var hasConfigurationFixer =
                    _configurationProvidersMap.TryGetValue(document.Project.Language, out var lazyConfigurationProviders) &&
                    !lazyConfigurationProviders.Value.IsDefaultOrEmpty;

                if (!hasAnySharedFixer && !hasAnyProjectFixer && !hasConfigurationFixer)
                {
                    return false;
                }

                var allFixers = ImmutableArray<CodeFixProvider>.Empty;
                if (hasAnySharedFixer)
                {
                    allFixers = workspaceFixers;
                }

                if (hasAnyProjectFixer)
                {
                    allFixers = allFixers.AddRange(projectFixers!);
                }

                var dx = await diagnostic.ToDiagnosticAsync(document.Project, cancellationToken).ConfigureAwait(false);

                if (hasConfigurationFixer)
                {
                    foreach (var lazyConfigurationProvider in lazyConfigurationProviders!.Value)
                    {
                        if (lazyConfigurationProvider.IsFixableDiagnostic(dx))
                        {
                            return true;
                        }
                    }
                }

                var fixes = new List<Microsoft.CodeAnalysis.CodeFixes.CodeFix>();
                var context = new CodeFixContext(document, dx,

                    // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs?
                    (action, applicableDiagnostics) =>
                    {
                        // Serialize access for thread safety - we don't know what thread the fix provider will call this delegate from.
                        lock (fixes)
                        {
                            fixes.Add(new Microsoft.CodeAnalysis.CodeFixes.CodeFix(document.Project, action, applicableDiagnostics));
                        }
                    },
                    false,
                    cancellationToken);

                var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService<IExtensionManager>();

                // we do have fixer. now let's see whether it actually can fix it
                foreach (var fixer in allFixers)
                {
                    await extensionManager.PerformActionAsync(fixer, () => fixer.RegisterCodeFixesAsync(context) ?? Task.CompletedTask)
                        .ConfigureAwait(false);
                    if (fixes.Count > 0)
                    {
                        return true;
                    }
                }

                return false;
            }

            private ImmutableArray<string> GetFixableDiagnosticIds(CodeFixProvider fixer, IExtensionManager? extensionManager)
            {
                // If we are passed a null extension manager it means we do not have access to a document so there is nothing to
                // show the user.  In this case we will log any exceptions that occur, but the user will not see them.
                if (extensionManager != null)
                {
                    return extensionManager.PerformFunction(
                        fixer,
                        () => ImmutableInterlocked.GetOrAdd(ref _fixerToFixableIdsMap, fixer, f => GetAndTestFixableDiagnosticIds(f)),
                        ImmutableArray<string>.Empty);
                }

                try
                {
                    return ImmutableInterlocked.GetOrAdd(ref _fixerToFixableIdsMap, fixer, f => GetAndTestFixableDiagnosticIds(f));
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch
                {
                    return ImmutableArray<string>.Empty;
                }
            }

            private static ImmutableArray<string> GetAndTestFixableDiagnosticIds(CodeFixProvider codeFixProvider)
            {
                var ids = codeFixProvider.FixableDiagnosticIds;
                if (ids.IsDefault)
                {
                    throw new InvalidOperationException(
                        string.Format(
                            WorkspacesResources._0_returned_an_uninitialized_ImmutableArray,
                            codeFixProvider.GetType().Name + "." + nameof(CodeFixProvider.FixableDiagnosticIds)));
                }

                return ids;
            }

            private ImmutableDictionary<string, Lazy<ImmutableDictionary<string, ImmutableArray<CodeFixProvider>>>> GetFixerPerLanguageMap(
                Dictionary<string, List<Lazy<CodeFixProvider, CodeChangeProviderMetadata>>> fixersPerLanguage,
                IExtensionManager? extensionManager)
            {
                var fixerMap = ImmutableDictionary.Create<string, Lazy<ImmutableDictionary<string, ImmutableArray<CodeFixProvider>>>>();
                foreach (var languageKindAndFixers in fixersPerLanguage)
                {
                    var lazyMap = new Lazy<ImmutableDictionary<string, ImmutableArray<CodeFixProvider>>>(() =>
                    {
                        var mutableMap = new Dictionary<string, List<CodeFixProvider>>();

                        foreach (var fixer in languageKindAndFixers.Value)
                        {
                            foreach (var id in GetFixableDiagnosticIds(fixer.Value, extensionManager))
                            {
                                if (string.IsNullOrWhiteSpace(id))
                                {
                                    continue;
                                }

                                var list = mutableMap.GetOrAdd(id, s_createList);
                                list.Add(fixer.Value);
                            }
                        }

                        var immutableMap = ImmutableDictionary.CreateBuilder<string, ImmutableArray<CodeFixProvider>>();
                        foreach (var diagnosticIdAndFixers in mutableMap)
                        {
                            immutableMap.Add(diagnosticIdAndFixers.Key, diagnosticIdAndFixers.Value.AsImmutableOrEmpty());
                        }

                        return immutableMap.ToImmutable();
                    }, true);

                    fixerMap = fixerMap.Add(languageKindAndFixers.Key, lazyMap);
                }

                return fixerMap;
            }

            private static ImmutableDictionary<string, Lazy<ImmutableArray<IConfigurationFixProvider>>> GetConfigurationProvidersPerLanguageMap(
                Dictionary<string, List<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>>> configurationProvidersPerLanguage)
            {
                var configurationFixerMap = ImmutableDictionary.Create<string, Lazy<ImmutableArray<IConfigurationFixProvider>>>();
                foreach (var languageKindAndFixers in configurationProvidersPerLanguage)
                {
                    var lazyConfigurationFixers =
                        new Lazy<ImmutableArray<IConfigurationFixProvider>>(() => GetConfigurationFixProviders(languageKindAndFixers.Value));
                    configurationFixerMap = configurationFixerMap.Add(languageKindAndFixers.Key, lazyConfigurationFixers);
                }

                return configurationFixerMap;

                static ImmutableArray<IConfigurationFixProvider> GetConfigurationFixProviders(
                    List<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>> languageKindAndFixers)
                {
                    using var builderDisposer = ArrayBuilder<IConfigurationFixProvider>.GetInstance(out var builder);
                    var orderedLanguageKindAndFixers = ExtensionOrderer.Order(languageKindAndFixers);
                    foreach (var languageKindAndFixersValue in orderedLanguageKindAndFixers)
                    {
                        builder.Add(languageKindAndFixersValue.Value);
                    }

                    return builder.ToImmutable();
                }
            }

            private static ImmutableDictionary<string, Lazy<ImmutableDictionary<CodeFixProvider, int>>> GetFixerPriorityPerLanguageMap(
                Dictionary<string, List<Lazy<CodeFixProvider, CodeChangeProviderMetadata>>> fixersPerLanguage)
            {
                var languageMap = ImmutableDictionary.CreateBuilder<string, Lazy<ImmutableDictionary<CodeFixProvider, int>>>();
                foreach (var languageAndFixers in fixersPerLanguage)
                {
                    var lazyMap = new Lazy<ImmutableDictionary<CodeFixProvider, int>>(() =>
                    {
                        var priorityMap = ImmutableDictionary.CreateBuilder<CodeFixProvider, int>();

                        var fixers = ExtensionOrderer.Order(languageAndFixers.Value);
                        for (var i = 0; i < fixers.Count; i++)
                        {
                            priorityMap.Add(fixers[i].Value, i);
                        }

                        return priorityMap.ToImmutable();
                    }, true);

                    languageMap.Add(languageAndFixers.Key, lazyMap);
                }

                return languageMap.ToImmutable();
            }

            private ImmutableDictionary<string, List<CodeFixProvider>> GetProjectFixers(Project project) =>
                _projectFixersMap.GetValue(project.AnalyzerReferences, pId => ComputeProjectFixers(project));

            private ImmutableDictionary<string, List<CodeFixProvider>> ComputeProjectFixers(Project project)
            {
                var extensionManager = project.Solution.Workspace.Services.GetService<IExtensionManager>();
                ImmutableDictionary<string, List<CodeFixProvider>>.Builder? builder = null;
                foreach (var reference in project.AnalyzerReferences)
                {
                    var projectCodeFixerProvider = _analyzerReferenceToFixersMap.GetValue(reference, _createProjectCodeFixProvider);
                    foreach (var fixer in projectCodeFixerProvider.GetFixers(project.Language))
                    {
                        var fixableIds = GetFixableDiagnosticIds(fixer, extensionManager!);
                        foreach (var id in fixableIds)
                        {
                            if (string.IsNullOrWhiteSpace(id))
                            {
                                continue;
                            }

                            builder ??= ImmutableDictionary.CreateBuilder<string, List<CodeFixProvider>>();
                            var list = builder.GetOrAdd(id, s_createList);
                            list.Add(fixer);
                        }
                    }
                }

                if (builder == null)
                {
                    return ImmutableDictionary<string, List<CodeFixProvider>>.Empty;
                }

                return builder.ToImmutable();
            }

            private class ProjectCodeFixProvider
            {
                private readonly AnalyzerReference _reference;
                private ImmutableDictionary<string, ImmutableArray<CodeFixProvider>> _fixersPerLanguage;

                public ProjectCodeFixProvider(AnalyzerReference reference)
                {
                    _reference = reference;
                    _fixersPerLanguage = ImmutableDictionary<string, ImmutableArray<CodeFixProvider>>.Empty;
                }

                public ImmutableArray<CodeFixProvider> GetFixers(string language) =>
                    ImmutableInterlocked.GetOrAdd(ref _fixersPerLanguage, language, CreateFixers);

                private ImmutableArray<CodeFixProvider> CreateFixers(string language)
                {
                    // check whether the analyzer reference knows how to return fixers directly.
                    if (_reference is ICodeFixProviderFactory codeFixProviderFactory)
                    {
                        return codeFixProviderFactory.GetFixers();
                    }

                    // otherwise, see whether we can pick it up from reference itself
                    if (_reference is not AnalyzerFileReference analyzerFileReference)
                    {
                        return ImmutableArray<CodeFixProvider>.Empty;
                    }

                    var builder = ArrayBuilder<CodeFixProvider>.GetInstance();

                    try
                    {
                        var analyzerAssembly = analyzerFileReference.GetAssembly();
                        var typeInfos = analyzerAssembly.DefinedTypes;

                        foreach (var typeInfo in typeInfos)
                        {
                            if (typeInfo.IsSubclassOf(typeof(CodeFixProvider)))
                            {
                                try
                                {
                                    var attribute = typeInfo.GetCustomAttribute<ExportCodeFixProviderAttribute>();
                                    if (attribute != null)
                                    {
                                        if ((attribute.Languages == null ||
                                             attribute.Languages.Length == 0 ||
                                             attribute.Languages.Contains(language))
                                            && Activator.CreateInstance(typeInfo.AsType()) is CodeFixProvider provider)
                                        {
                                            builder.Add(provider);
                                        }
                                    }
                                }
                                catch
                                {
                                    // ignored
                                }
                            }
                        }
                    }
                    catch
                    {
                        // REVIEW: is the below message right?
                        // NOTE: We could report "unable to load analyzer" exception here but it should have been already reported by DiagnosticService.
                    }

                    return builder.ToImmutableAndFree();
                }
            }

            private class FixAllDiagnosticProvider : FixAllContext.DiagnosticProvider
            {
                private readonly CodeFixService _codeFixService;
                private readonly ImmutableHashSet<string> _diagnosticIds;

                public FixAllDiagnosticProvider(CodeFixService codeFixService, ImmutableHashSet<string> diagnosticIds)
                {
                    _codeFixService = codeFixService;
                    _diagnosticIds = diagnosticIds;
                }

                public override Task<IEnumerable<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken)
                    => _codeFixService.GetDocumentDiagnosticsAsync(document, _diagnosticIds, cancellationToken);

                public override Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken)
                    => _codeFixService.GetProjectDiagnosticsAsync(project, true, _diagnosticIds, cancellationToken);

                public override Task<IEnumerable<Diagnostic>> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken)
                    => _codeFixService.GetProjectDiagnosticsAsync(project, false, _diagnosticIds, cancellationToken);
            }

            private class FixAllPredefinedDiagnosticProvider : FixAllContext.DiagnosticProvider
            {
                private readonly ImmutableArray<Diagnostic> _diagnostics;

                public FixAllPredefinedDiagnosticProvider(ImmutableArray<Diagnostic> diagnostics) => _diagnostics = diagnostics;

                public override Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken)
                    => Task.FromResult<IEnumerable<Diagnostic>>(_diagnostics);

                public override Task<IEnumerable<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken)
                    => Task.FromResult<IEnumerable<Diagnostic>>(_diagnostics);

                public override Task<IEnumerable<Diagnostic>> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken)
                    => SpecializedTasks.EmptyEnumerable<Diagnostic>();
            }
        }
    }
}
