﻿// 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.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices.Implementation.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel
{
    /// <summary>
    /// Cache FileCodeModel instances for a given project (we are using WeakReference for now, 
    /// so that we can more or less match the semantics of the former native implementation, which 
    /// offered reference equality until all instances were collected by the GC)
    /// </summary>
    internal sealed partial class CodeModelProjectCache
    {
        private readonly ProjectId _projectId;
        private readonly ICodeModelInstanceFactory _codeModelInstanceFactory;

        private readonly Dictionary<string, CacheEntry> _cache = new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
        private readonly object _cacheGate = new object();

        private EnvDTE.CodeModel? _rootCodeModel;
        private bool _zombied;

        internal CodeModelState State { get; }

        internal CodeModelProjectCache(
            IThreadingContext threadingContext,
            ProjectId projectId,
            ICodeModelInstanceFactory codeModelInstanceFactory,
            ProjectCodeModelFactory projectFactory,
            IServiceProvider serviceProvider,
            Microsoft.CodeAnalysis.Host.LanguageServices languageServices,
            VisualStudioWorkspace workspace)
        {
            State = new CodeModelState(threadingContext, serviceProvider, languageServices, workspace, projectFactory);
            _projectId = projectId;
            _codeModelInstanceFactory = codeModelInstanceFactory;
        }

        /// <summary>
        /// Look for an existing instance of FileCodeModel in our cache.
        /// Return null if there is no active FCM for "fileName".
        /// </summary>
        private CacheEntry? GetCacheEntry(string fileName)
        {
            lock (_cacheGate)
            {
                if (_cache.TryGetValue(fileName, out var cacheEntry))
                {
                    return cacheEntry;
                }
            }

            return null;
        }

        public ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> GetOrCreateFileCodeModel(string filePath)
        {
            // First try
            {
                var cacheEntry = GetCacheEntry(filePath);
                if (cacheEntry != null)
                {
                    var comHandle = cacheEntry.Value.ComHandle;
                    if (comHandle != null)
                    {
                        return comHandle.Value;
                    }
                }
            }

            // This ultimately ends up calling GetOrCreateFileCodeModel(fileName, parent) with the correct "parent" object
            // through the project system.
            var newFileCodeModel = (EnvDTE80.FileCodeModel2)_codeModelInstanceFactory.TryCreateFileCodeModelThroughProjectSystem(filePath);
            return new ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>(newFileCodeModel);
        }

        public ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>? GetComHandleForFileCodeModel(string filePath)
        {
            var cacheEntry = GetCacheEntry(filePath);

            return cacheEntry?.ComHandle;
        }

        public ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> GetOrCreateFileCodeModel(string filePath, object? parent)
        {
            // First try
            {
                var cacheEntry = GetCacheEntry(filePath);
                if (cacheEntry != null)
                {
                    var comHandle = cacheEntry.Value.ComHandle;
                    if (comHandle != null)
                    {
                        return comHandle.Value;
                    }
                }
            }

            // Check that we know about this file!
            var solution = State.Workspace.CurrentSolution;
            var documentId = solution.GetDocumentIdsWithFilePath(filePath).Where(id => id.ProjectId == _projectId).FirstOrDefault();
            if (documentId == null || solution.GetDocument(documentId) == null)
            {
                // Matches behavior of native (C#) implementation
                throw Exceptions.ThrowENotImpl();
            }

            // Create object (outside of lock)
            var newFileCodeModel = FileCodeModel.Create(State, parent, documentId, isSourceGeneratorOutput: false, new TextManagerAdapter());
            var newCacheEntry = new CacheEntry(newFileCodeModel);

            // Second try (object might have been added by another thread at this point!)
            lock (_cacheGate)
            {
                var cacheEntry = GetCacheEntry(filePath);
                if (cacheEntry != null)
                {
                    var comHandle = cacheEntry.Value.ComHandle;
                    if (comHandle != null)
                    {
                        return comHandle.Value;
                    }
                }

                // Note: Using the indexer here (instead of "Add") is relevant since the old
                //       WeakReference entry is likely still in the cache (with a Null target, of course)
                _cache[filePath] = newCacheEntry;

                return newFileCodeModel;
            }
        }

        public EnvDTE.CodeModel GetOrCreateRootCodeModel(EnvDTE.Project parent)
        {
            if (_zombied)
            {
                Debug.Fail("Cannot access root code model after code model was shutdown!");
                throw Exceptions.ThrowEUnexpected();
            }

            _rootCodeModel ??= RootCodeModel.Create(State, parent, _projectId);

            return _rootCodeModel;
        }

        public IEnumerable<ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>> GetFileCodeModelInstances()
        {
            var result = new List<ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>>();

            lock (_cacheGate)
            {
                foreach (var cacheEntry in _cache.Values)
                {
                    var comHandle = cacheEntry.ComHandle;
                    if (comHandle != null)
                    {
                        result.Add(comHandle.Value);
                    }
                }
            }

            return result;
        }

        public void OnProjectClosed()
        {
            var instances = GetFileCodeModelInstances();

            lock (_cacheGate)
            {
                _cache.Clear();
            }

            foreach (var instance in instances)
            {
                instance.Object.Shutdown();
            }

            _zombied = true;
        }

        public void OnSourceFileRemoved(string fileName)
        {
            ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>? comHandle = null;

            lock (_cacheGate)
            {
                if (_cache.TryGetValue(fileName, out var cacheEntry))
                {
                    comHandle = cacheEntry.ComHandle;
                    _cache.Remove(fileName);
                }
            }

            comHandle?.Object.Shutdown();
        }

        public void OnSourceFileRenaming(string oldFileName, string newFileName)
        {
            ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>? comHandleToRename = null;
            ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>? comHandleToShutDown = null;

            lock (_cacheGate)
            {
                if (_cache.TryGetValue(oldFileName, out var cacheEntry))
                {
                    comHandleToRename = cacheEntry.ComHandle;

                    _cache.Remove(oldFileName);

                    if (comHandleToRename != null)
                    {
                        // We might already have a code model for this new filename. This can happen if
                        // we were to rename Goo.cs to Goocs, which will call this method, and then rename
                        // it back, which does not call this method. This results in both Goo.cs and Goocs
                        // being in the cache. We could fix that "correctly", but the zombied Goocs code model
                        // is pretty broken, so there's no point in trying to reuse it.
                        if (_cache.TryGetValue(newFileName, out cacheEntry))
                        {
                            comHandleToShutDown = cacheEntry.ComHandle;
                        }

                        _cache.Add(newFileName, cacheEntry);
                    }
                }
            }

            comHandleToShutDown?.Object.Shutdown();
            comHandleToRename?.Object.OnRename(newFileName);
        }
    }
}
