﻿// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices.Implementation.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel
{
    /// <summary>
    /// The root type that is held by a project to provide CodeModel support.
    /// </summary>
    internal sealed class ProjectCodeModel : IProjectCodeModel
    {
        private readonly NonReentrantLock _guard = new NonReentrantLock();
        private readonly IThreadingContext _threadingContext;
        private readonly ProjectId _projectId;
        private readonly ICodeModelInstanceFactory _codeModelInstanceFactory;
        private readonly VisualStudioWorkspace _visualStudioWorkspace;
        private readonly IServiceProvider _serviceProvider;
        private readonly ProjectCodeModelFactory _projectCodeModelFactory;

        private CodeModelProjectCache _codeModelCache;

        public ProjectCodeModel(
            IThreadingContext threadingContext,
            ProjectId projectId,
            ICodeModelInstanceFactory codeModelInstanceFactory,
            VisualStudioWorkspace visualStudioWorkspace,
            IServiceProvider serviceProvider,
            ProjectCodeModelFactory projectCodeModelFactory)
        {
            _threadingContext = threadingContext;
            _projectId = projectId;
            _codeModelInstanceFactory = codeModelInstanceFactory;
            _visualStudioWorkspace = visualStudioWorkspace;
            _serviceProvider = serviceProvider;
            _projectCodeModelFactory = projectCodeModelFactory;
        }

        public void OnProjectClosed()
        {
            _codeModelCache?.OnProjectClosed();
            _projectCodeModelFactory.OnProjectClosed(_projectId);
        }

        private CodeModelProjectCache GetCodeModelCache()
        {
            Contract.ThrowIfNull(_projectId);
            Contract.ThrowIfNull(_visualStudioWorkspace);

            using (_guard.DisposableWait())
            {
                if (_codeModelCache == null)
                {
                    var workspaceProject = _visualStudioWorkspace.CurrentSolution.GetProject(_projectId);

                    if (workspaceProject != null)
                    {
                        _codeModelCache = new CodeModelProjectCache(_threadingContext, _projectId, _codeModelInstanceFactory, _projectCodeModelFactory, _serviceProvider, workspaceProject.Services, _visualStudioWorkspace);
                    }
                }

                return _codeModelCache;
            }
        }

        internal IEnumerable<ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>> GetCachedFileCodeModelInstances()
            => GetCodeModelCache().GetFileCodeModelInstances();

        internal bool TryGetCachedFileCodeModel(string fileName, out ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> fileCodeModelHandle)
        {
            var handle = GetCodeModelCache()?.GetComHandleForFileCodeModel(fileName);

            fileCodeModelHandle = handle != null
                ? handle.Value
                : default;

            return handle != null;
        }

        /// <summary>
        /// Gets or creates a <see cref="FileCodeModel"/> for the given file name. Because we don't have
        /// a parent object, this will call back to the project system to provide us the parent object.
        /// </summary>
        public ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> GetOrCreateFileCodeModel(string filePath)
            => GetCodeModelCache().GetOrCreateFileCodeModel(filePath);

        public ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> GetOrCreateFileCodeModel(string filePath, object parent)
            => GetCodeModelCache().GetOrCreateFileCodeModel(filePath, parent);

        public EnvDTE.CodeModel GetOrCreateRootCodeModel(EnvDTE.Project parent)
            => GetCodeModelCache().GetOrCreateRootCodeModel(parent);

        public void OnSourceFileRemoved(string fileName)
        {
            // This uses the field directly. If we haven't yet created the CodeModelProjectCache, then we most definitely
            // don't have any source files we need to zombie when they go away. There's no reason to create a cache in that case.
            _codeModelCache?.OnSourceFileRemoved(fileName);
        }

        public void OnSourceFileRenaming(string filePath, string newFilePath)
        {
            // This uses the field directly. If we haven't yet created the CodeModelProjectCache, then we most definitely
            // don't have any source files we need to handle a rename for. There's no reason to create a cache in that case.
            _codeModelCache?.OnSourceFileRenaming(filePath, newFilePath);
        }

        EnvDTE.FileCodeModel IProjectCodeModel.GetOrCreateFileCodeModel(string filePath, object parent)
            => this.GetOrCreateFileCodeModel(filePath, parent).Handle;

        public EnvDTE.FileCodeModel CreateFileCodeModel(SourceGeneratedDocument sourceGeneratedDocument)
        {
            // Unlike for "regular" documents, we make no effort to cache these between callers or hold them for longer lifetimes with
            // events.
            return FileCodeModel.Create(GetCodeModelCache().State, parent: null, sourceGeneratedDocument.Id, isSourceGeneratorOutput: true, new TextManagerAdapter()).Handle;
        }
    }
}
