// 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.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(StringComparer.OrdinalIgnoreCase);
    private readonly object _cacheGate = new();

    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);
    }
}
