﻿// 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.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Storage;
using Microsoft.VisualStudio.RpcContracts.Caching;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Storage
{
    /// <summary>
    /// Cache of our own internal roslyn storage keys to the equivalent platform cloud cache keys.  Cloud cache keys can
    /// store a lot of date in them (like their 'dimensions' dictionary.  We don't want to continually recreate these as
    /// we read/write date to the db.
    /// </summary>
    internal class ProjectContainerKeyCache
    {
        private static readonly ImmutableSortedDictionary<string, string?> EmptyDimensions = ImmutableSortedDictionary.Create<string, string?>(StringComparer.Ordinal);

        /// <summary>
        /// Container key explicitly for the project itself.
        /// </summary>
        public readonly CacheContainerKey? ProjectContainerKey;

        /// <summary>
        /// Cache from document green nodes to the container keys we've computed for it. We can avoid computing these
        /// container keys when called repeatedly for the same documents.
        /// </summary>
        /// <remarks>
        /// We can use a normal Dictionary here instead of a <see cref="ConditionalWeakTable{TKey, TValue}"/> as
        /// instances of <see cref="ProjectContainerKeyCache"/> are always owned in a context where the <see
        /// cref="ProjectState"/> is alive.  As that instance is alive, all <see cref="TextDocumentState"/>s the project
        /// points at will be held alive strongly too.
        /// </remarks>
        private readonly Dictionary<TextDocumentState, CacheContainerKey?> _documentToContainerKey = new();
        private readonly Func<TextDocumentState, CacheContainerKey?> _documentToContainerKeyCallback;

        public ProjectContainerKeyCache(string relativePathBase, ProjectKey projectKey)
        {
            ProjectContainerKey = CreateProjectContainerKey(relativePathBase, projectKey);

            _documentToContainerKeyCallback = ds => CreateDocumentContainerKey(relativePathBase, DocumentKey.ToDocumentKey(projectKey, ds));
        }

        public CacheContainerKey? GetDocumentContainerKey(TextDocumentState state)
        {
            lock (_documentToContainerKey)
                return _documentToContainerKey.GetOrAdd(state, _documentToContainerKeyCallback);
        }

        public static CacheContainerKey? CreateProjectContainerKey(
            string relativePathBase, ProjectKey projectKey)
        {
            // Creates a container key for this project.  The container key is a mix of the project's name, relative
            // file path (to the solution), and optional parse options.

            // If we don't have a valid solution path, we can't store anything.
            if (string.IsNullOrEmpty(relativePathBase))
                return null;

            // We have to have a file path for this project
            if (RoslynString.IsNullOrEmpty(projectKey.FilePath))
                return null;

            // The file path has to be relative to the base path the DB is associated with (either the solution-path or
            // repo-path).
            var relativePath = PathUtilities.GetRelativePath(relativePathBase, projectKey.FilePath!);
            if (relativePath == projectKey.FilePath)
                return null;

            var dimensions = EmptyDimensions
                .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.Name)}", projectKey.Name)
                .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.FilePath)}", relativePath)
                .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.ParseOptionsChecksum)}", projectKey.ParseOptionsChecksum.ToString());

            return new CacheContainerKey("Roslyn.Project", dimensions);
        }

        public static CacheContainerKey? CreateDocumentContainerKey(
            string relativePathBase,
            DocumentKey documentKey)
        {
            // See if we can get a project key for this info.  If not, we def can't get a doc key.
            var projectContainerKey = CreateProjectContainerKey(relativePathBase, documentKey.Project);
            if (projectContainerKey == null)
                return null;

            // We have to have a file path for this document
            if (string.IsNullOrEmpty(documentKey.FilePath))
                return null;

            // The file path has to be relative to the base path the DB is associated with (either the solution-path or
            // repo-path).
            var relativePath = PathUtilities.GetRelativePath(relativePathBase, documentKey.FilePath!);
            if (relativePath == documentKey.FilePath)
                return null;

            var dimensions = projectContainerKey.Value.Dimensions
                .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.Name)}", documentKey.Name)
                .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.FilePath)}", relativePath);

            return new CacheContainerKey("Roslyn.Document", dimensions);
        }
    }
}
