﻿// 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.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Shell;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer
{
    internal class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged
    {
        private readonly AnalyzersFolderItem _analyzersFolder;
        private readonly IAnalyzersCommandHandler _commandHandler;
        private IReadOnlyCollection<AnalyzerReference> _analyzerReferences;
        private BulkObservableCollection<AnalyzerItem> _analyzerItems;

        public event PropertyChangedEventHandler PropertyChanged;

        public AnalyzerItemSource(AnalyzersFolderItem analyzersFolder, IAnalyzersCommandHandler commandHandler)
        {
            _analyzersFolder = analyzersFolder;
            _commandHandler = commandHandler;

            _analyzersFolder.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged;
        }

        private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
        {
            switch (e.Kind)
            {
                case WorkspaceChangeKind.SolutionAdded:
                case WorkspaceChangeKind.SolutionChanged:
                case WorkspaceChangeKind.SolutionReloaded:
                    UpdateAnalyzers();
                    break;

                case WorkspaceChangeKind.SolutionRemoved:
                case WorkspaceChangeKind.SolutionCleared:
                    _analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged;
                    break;

                case WorkspaceChangeKind.ProjectAdded:
                case WorkspaceChangeKind.ProjectReloaded:
                case WorkspaceChangeKind.ProjectChanged:
                    if (e.ProjectId == _analyzersFolder.ProjectId)
                    {
                        UpdateAnalyzers();
                    }

                    break;

                case WorkspaceChangeKind.ProjectRemoved:
                    if (e.ProjectId == _analyzersFolder.ProjectId)
                    {
                        _analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged;
                    }

                    break;
            }
        }

        private void UpdateAnalyzers()
        {
            if (_analyzerItems == null)
            {
                // The set of AnalyzerItems hasn't been realized yet. Just signal that HasItems
                // may have changed.

                NotifyPropertyChanged(nameof(HasItems));
                return;
            }

            var project = _analyzersFolder.Workspace
                            .CurrentSolution
                            .GetProject(_analyzersFolder.ProjectId);

            if (project != null &&
                project.AnalyzerReferences != _analyzerReferences)
            {
                _analyzerReferences = project.AnalyzerReferences;

                _analyzerItems.BeginBulkOperation();

                var itemsToRemove = _analyzerItems
                                        .Where(item => !_analyzerReferences.Contains(item.AnalyzerReference))
                                        .ToArray();

                var referencesToAdd = GetFilteredAnalyzers(_analyzerReferences, project)
                                        .Where(r => !_analyzerItems.Any(item => item.AnalyzerReference == r))
                                        .ToArray();

                foreach (var item in itemsToRemove)
                {
                    _analyzerItems.Remove(item);
                }

                foreach (var reference in referencesToAdd)
                {
                    _analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController));
                }

                var sorted = _analyzerItems.OrderBy(item => item.AnalyzerReference.Display).ToArray();
                for (var i = 0; i < sorted.Length; i++)
                {
                    _analyzerItems.Move(_analyzerItems.IndexOf(sorted[i]), i);
                }

                _analyzerItems.EndBulkOperation();

                NotifyPropertyChanged(nameof(HasItems));
            }
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public bool HasItems
        {
            get
            {
                if (_analyzerItems != null)
                {
                    return _analyzerItems.Count > 0;
                }

                var project = _analyzersFolder.Workspace
                                                .CurrentSolution
                                                .GetProject(_analyzersFolder.ProjectId);

                if (project != null)
                {
                    return project.AnalyzerReferences.Count > 0;
                }

                return false;
            }
        }

        public IEnumerable Items
        {
            get
            {
                if (_analyzerItems == null)
                {
                    _analyzerItems = new BulkObservableCollection<AnalyzerItem>();

                    var project = _analyzersFolder.Workspace
                                                .CurrentSolution
                                                .GetProject(_analyzersFolder.ProjectId);

                    if (project != null)
                    {
                        _analyzerReferences = project.AnalyzerReferences;
                        var initialSet = GetFilteredAnalyzers(_analyzerReferences, project)
                                            .OrderBy(ar => ar.Display)
                                            .Select(ar => new AnalyzerItem(_analyzersFolder, ar, _commandHandler.AnalyzerContextMenuController));
                        _analyzerItems.AddRange(initialSet);
                    }
                }

                Logger.Log(
                    FunctionId.SolutionExplorer_AnalyzerItemSource_GetItems,
                    KeyValueLogMessage.Create(m => m["Count"] = _analyzerItems.Count));

                return _analyzerItems;
            }
        }

        public object SourceItem
        {
            get
            {
                return _analyzersFolder;
            }
        }

        private ImmutableHashSet<string> GetAnalyzersWithLoadErrors()
        {
            if (_analyzersFolder.Workspace is VisualStudioWorkspaceImpl)
            {
                /*
                var vsProject = vsWorkspace.DeferredState?.ProjectTracker.GetProject(_analyzersFolder.ProjectId);
                var vsAnalyzersMap = vsProject?.GetProjectAnalyzersMap();

                if (vsAnalyzersMap != null)
                {
                    return vsAnalyzersMap.Where(kvp => kvp.Value.HasLoadErrors).Select(kvp => kvp.Key).ToImmutableHashSet();
                }
                */
            }

            return ImmutableHashSet<string>.Empty;
        }

        private ImmutableArray<AnalyzerReference> GetFilteredAnalyzers(IEnumerable<AnalyzerReference> analyzerReferences, Project project)
        {
            var analyzersWithLoadErrors = GetAnalyzersWithLoadErrors();

            // Filter out analyzer dependencies which have no diagnostic analyzers, but still retain the unresolved analyzers and analyzers with load errors.
            var builder = ArrayBuilder<AnalyzerReference>.GetInstance();
            foreach (var analyzerReference in analyzerReferences)
            {
                // Analyzer dependency:
                // 1. Must be an Analyzer file reference (we don't understand other analyzer dependencies).
                // 2. Mush have no diagnostic analyzers.
                // 3. Must have no source generators.
                // 4. Must have non-null full path.
                // 5. Must not have any assembly or analyzer load failures.
                if (analyzerReference is AnalyzerFileReference &&
                    analyzerReference.GetAnalyzers(project.Language).IsDefaultOrEmpty &&
                    analyzerReference.GetGenerators(project.Language).IsDefaultOrEmpty &&
                    analyzerReference.FullPath != null &&
                    !analyzersWithLoadErrors.Contains(analyzerReference.FullPath))
                {
                    continue;
                }

                builder.Add(analyzerReference);
            }

            return builder.ToImmutableAndFree();
        }
    }
}
