﻿// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation
{
    [Export(typeof(AnalyzerDependencyCheckingService))]
    internal sealed class AnalyzerDependencyCheckingService
    {
        /// <summary>
        /// Object given as key for <see cref="HostDiagnosticUpdateSource.UpdateDiagnosticsForProject(ProjectId, object, IEnumerable{DiagnosticData})"/>.
        /// </summary>
        private static readonly object s_dependencyConflictErrorId = new();
        private static readonly IIgnorableAssemblyList s_systemPrefixList = new IgnorableAssemblyNamePrefixList("System");
        private static readonly IIgnorableAssemblyList s_codeAnalysisPrefixList = new IgnorableAssemblyNamePrefixList("Microsoft.CodeAnalysis");
        private static readonly IIgnorableAssemblyList s_explicitlyIgnoredAssemblyList = new IgnorableAssemblyIdentityList(GetExplicitlyIgnoredAssemblyIdentities());
        private static readonly IIgnorableAssemblyList s_assembliesIgnoredByNameList = new IgnorableAssemblyNameList(ImmutableHashSet.Create("mscorlib"));
        private static readonly IBindingRedirectionService s_bindingRedirectionService = new BindingRedirectionService();

        private readonly VisualStudioWorkspace _workspace;
        private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource;

        /// <summary>
        /// Object given to synchronize access to the mutable fields in this class.
        /// </summary>
        private readonly object _gate = new();
        private CancellationTokenSource _cancellationTokenSource = new();

        /// <summary>
        /// The most recently started analysis task; if we start a new analysis we will cancel the previous one and start the next one
        /// as a continuation of this task to ensure any notification to <see cref="_hostDiagnosticUpdateSource"/> was done first.
        /// </summary>
        private Task _task = Task.CompletedTask;
        private ImmutableHashSet<string> _previousAnalyzerPaths = ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);

        private static readonly DiagnosticDescriptor s_missingAnalyzerReferenceRule = new(
            id: IDEDiagnosticIds.MissingAnalyzerReferenceId,
            title: ServicesVSResources.MissingAnalyzerReference,
            messageFormat: ServicesVSResources.Analyzer_assembly_0_depends_on_1_but_it_was_not_found_Analyzers_may_not_run_correctly_unless_the_missing_assembly_is_added_as_an_analyzer_reference_as_well,
            category: FeaturesResources.Roslyn_HostError,
            defaultSeverity: DiagnosticSeverity.Warning,
            isEnabledByDefault: true);

        private static readonly DiagnosticDescriptor s_analyzerDependencyConflictRule = new(
            id: IDEDiagnosticIds.AnalyzerDependencyConflictId,
            title: ServicesVSResources.AnalyzerDependencyConflict,
            messageFormat: ServicesVSResources.Analyzer_assemblies_0_and_1_both_have_identity_2_but_different_contents_Only_one_will_be_loaded_and_analyzers_using_these_assemblies_may_not_run_correctly,
            category: FeaturesResources.Roslyn_HostError,
            defaultSeverity: DiagnosticSeverity.Warning,
            isEnabledByDefault: true);

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public AnalyzerDependencyCheckingService(
            VisualStudioWorkspace workspace,
            HostDiagnosticUpdateSource hostDiagnosticUpdateSource)
        {
            _workspace = workspace;
            _hostDiagnosticUpdateSource = hostDiagnosticUpdateSource;
        }

        public void ReanalyzeSolutionForConflicts()
        {
            var solution = _workspace.CurrentSolution;
            var currentAnalyzerPaths = solution
                .Projects
                .SelectMany(p => p.AnalyzerReferences)
                .OfType<AnalyzerFileReference>()
                .Select(a => a.FullPath)
                .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);

            lock (_gate)
            {
                // If we've already started analysis for this set of analyzers, no reason to start over
                if (currentAnalyzerPaths.SetEquals(_previousAnalyzerPaths))
                {
                    return;
                }

                _cancellationTokenSource.Cancel();
                _cancellationTokenSource = new CancellationTokenSource();
                _previousAnalyzerPaths = currentAnalyzerPaths;

                // Capturing cancellationToken here so the right instance is passed into the delegates below
                var cancellationToken = _cancellationTokenSource.Token;

                // We are explicitly relying on SafeContinueWith including LazyCancellation as a continuation option here
                _task = _task.SafeContinueWith(_ =>
                {
                    AnalyzeAndReportConflictsInSolution(solution, currentAnalyzerPaths, _hostDiagnosticUpdateSource, cancellationToken);
                },
                cancellationToken, TaskScheduler.Default);
            }
        }

        // Method is static to prevent accidental use of mutable state in this class
        private static void AnalyzeAndReportConflictsInSolution(
            Solution solution,
            ImmutableHashSet<string> currentAnalyzerPaths,
            HostDiagnosticUpdateSource hostDiagnosticUpdateSource,
            CancellationToken cancellationToken)
        {
            var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Select(assembly => AssemblyIdentity.FromAssemblyDefinition(assembly));
            var loadedAssembliesList = new IgnorableAssemblyIdentityList(loadedAssemblies);

            var ignorableAssemblyLists = new[] { s_systemPrefixList, s_codeAnalysisPrefixList, s_explicitlyIgnoredAssemblyList, s_assembliesIgnoredByNameList, loadedAssembliesList };

            cancellationToken.ThrowIfCancellationRequested();

            var results = AnalyzerDependencyChecker.ComputeDependencyConflicts(currentAnalyzerPaths, ignorableAssemblyLists, s_bindingRedirectionService, cancellationToken);

            var builder = ImmutableArray.CreateBuilder<DiagnosticData>();

            var conflicts = results.Conflicts;
            var missingDependencies = results.MissingDependencies;

            foreach (var project in solution.Projects)
            {
                builder.Clear();

                // If our analysis has been cancelled, it means another request has been queued behind us; thus it's OK to stop
                // doing the analysis now and let that other one fix up any stale results.
                cancellationToken.ThrowIfCancellationRequested();

                var analyzerFilePaths = new HashSet<string>(
                    project.AnalyzerReferences
                           .OfType<AnalyzerFileReference>()
                           .Select(f => f.FullPath),
                    StringComparer.OrdinalIgnoreCase);

                foreach (var conflict in conflicts)
                {
                    if (analyzerFilePaths.Contains(conflict.AnalyzerFilePath1) ||
                        analyzerFilePaths.Contains(conflict.AnalyzerFilePath2))
                    {
                        var messageArguments = new string[] { conflict.AnalyzerFilePath1, conflict.AnalyzerFilePath2, conflict.Identity.ToString() };
                        if (DiagnosticData.TryCreate(s_analyzerDependencyConflictRule, messageArguments, project, out var diagnostic))
                        {
                            builder.Add(diagnostic);
                        }
                    }
                }

                foreach (var missingDependency in missingDependencies)
                {
                    if (analyzerFilePaths.Contains(missingDependency.AnalyzerPath))
                    {
                        var messageArguments = new string[] { missingDependency.AnalyzerPath, missingDependency.DependencyIdentity.ToString() };
                        if (DiagnosticData.TryCreate(s_missingAnalyzerReferenceRule, messageArguments, project, out var diagnostic))
                        {
                            builder.Add(diagnostic);
                        }
                    }
                }

                hostDiagnosticUpdateSource.UpdateDiagnosticsForProject(project.Id, s_dependencyConflictErrorId, builder.ToImmutable());
            }

            foreach (var conflict in conflicts)
            {
                LogConflict(conflict);
            }

            foreach (var missingDependency in missingDependencies)
            {
                LogMissingDependency(missingDependency);
            }
        }

        private static void LogConflict(AnalyzerDependencyConflict conflict)
        {
            Logger.Log(
                FunctionId.AnalyzerDependencyCheckingService_LogConflict,
                KeyValueLogMessage.Create(m =>
                {
                    m["Identity"] = conflict.Identity.ToString();
                    m["Analyzer1"] = conflict.AnalyzerFilePath1;
                    m["Analyzer2"] = conflict.AnalyzerFilePath2;
                }));
        }

        private static void LogMissingDependency(MissingAnalyzerDependency missingDependency)
        {
            Logger.Log(
                FunctionId.AnalyzerDependencyCheckingService_LogMissingDependency,
                KeyValueLogMessage.Create(m =>
                {
                    m["Analyzer"] = missingDependency.AnalyzerPath;
                    m["Identity"] = missingDependency.DependencyIdentity;
                }));
        }

        private static IEnumerable<AssemblyIdentity> GetExplicitlyIgnoredAssemblyIdentities()
        {
            // Microsoft.VisualBasic.dll
            var list = new List<AssemblyIdentity>();
            AddAssemblyIdentity(list, "Microsoft.VisualBasic, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
            AddAssemblyIdentity(list, "Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

            return list;
        }

        private static void AddAssemblyIdentity(List<AssemblyIdentity> list, string dllName)
        {
            if (!AssemblyIdentity.TryParseDisplayName(dllName, out var identity))
            {
                return;
            }

            list.Add(identity);
        }

        private sealed class BindingRedirectionService : IBindingRedirectionService
        {
            public AssemblyIdentity ApplyBindingRedirects(AssemblyIdentity originalIdentity)
            {
                var redirectedAssemblyName = AppDomain.CurrentDomain.ApplyPolicy(originalIdentity.ToString());
                if (AssemblyIdentity.TryParseDisplayName(redirectedAssemblyName, out var redirectedAssemblyIdentity))
                {
                    return redirectedAssemblyIdentity;
                }

                return originalIdentity;
            }
        }
    }
}
