﻿// 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.Linq;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.Utilities;
using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer;

namespace Microsoft.CodeAnalysis.LanguageServer;

internal class LspServices : ILspServices
{
    private readonly ImmutableDictionary<Type, Lazy<ILspService, LspServiceMetadataView>> _lazyMefLspServices;

    /// <summary>
    /// A set of base services that apply to all roslyn lsp services.
    /// Unfortunately MEF doesn't provide a good way to export something for multiple contracts with metadata
    /// so these are manually created in <see cref="RoslynLanguageServer"/>.
    /// TODO - cleanup once https://github.com/dotnet/roslyn/issues/63555 is resolved.
    /// </summary>
    private readonly ImmutableDictionary<Type, ImmutableArray<Func<ILspServices, object>>> _baseServices;

    /// <summary>
    /// Gates access to <see cref="_servicesToDispose"/>.
    /// </summary>
    private readonly object _gate = new();
    private readonly HashSet<IDisposable> _servicesToDispose = new(ReferenceEqualityComparer.Instance);

    public LspServices(
        ImmutableArray<Lazy<ILspService, LspServiceMetadataView>> mefLspServices,
        ImmutableArray<Lazy<ILspServiceFactory, LspServiceMetadataView>> mefLspServiceFactories,
        WellKnownLspServerKinds serverKind,
        ImmutableDictionary<Type, ImmutableArray<Func<ILspServices, object>>> baseServices)
    {
        // Convert MEF exported service factories to the lazy LSP services that they create.
        var servicesFromFactories = mefLspServiceFactories.Select(lz => new Lazy<ILspService, LspServiceMetadataView>(() => lz.Value.CreateILspService(this, serverKind), lz.Metadata));

        var services = mefLspServices.Concat(servicesFromFactories);

        // Make sure that we only include services exported for the specified server kind (or NotSpecified).
        services = services.Where(lazyService => lazyService.Metadata.ServerKind == serverKind || lazyService.Metadata.ServerKind == WellKnownLspServerKinds.Any);

        // This will throw if the same service is registered twice
        _lazyMefLspServices = services.ToImmutableDictionary(lazyService => lazyService.Metadata.Type, lazyService => lazyService);

        // Bit cheaky, but lets make an this ILspService available on the base services to make constructors that take an ILspServices instance possible.
        _baseServices = baseServices.Add(typeof(ILspServices), ImmutableArray.Create<Func<ILspServices, object>>((_) => this));
    }

    public T GetRequiredService<T>() where T : notnull
    {
        var service = GetService<T>();
        Contract.ThrowIfNull(service, $"Missing required LSP service {typeof(T).FullName}");
        return service;
    }

    public T? GetService<T>()
    {
        T? service;

        // Check the base services first
        service = GetBaseServices<T>().SingleOrDefault();
        service ??= (T?)TryGetService(typeof(T));

        return service;
    }

    public IEnumerable<T> GetRequiredServices<T>()
    {
        var baseServices = GetBaseServices<T>();
        var mefServices = GetMefServices<T>();

        return baseServices != null ? mefServices.Concat(baseServices) : mefServices;
    }

    public object? TryGetService(Type type)
    {
        object? lspService;
        if (_lazyMefLspServices.TryGetValue(type, out var lazyService))
        {
            // If we are creating a stateful LSP service for the first time, we need to check
            // if it is disposable after creation and keep it around to dispose of on shutdown.
            // Stateless LSP services will be disposed of on MEF container disposal.
            var checkDisposal = !lazyService.Metadata.IsStateless && !lazyService.IsValueCreated;

            lspService = lazyService.Value;
            if (checkDisposal && lspService is IDisposable disposable)
            {
                lock (_gate)
                {
                    var res = _servicesToDispose.Add(disposable);
                }
            }

            return lspService;
        }

        lspService = null;
        return lspService;
    }

    public ImmutableArray<Type> GetRegisteredServices()
        => _lazyMefLspServices.Keys.ToImmutableArray();

    public bool SupportsGetRegisteredServices()
    {
        return true;
    }

    private IEnumerable<T> GetBaseServices<T>()
        => _baseServices.TryGetValue(typeof(T), out var baseServices)
            ? baseServices.Select(creatorFunc => (T)creatorFunc(this)).ToImmutableArray()
            : (IEnumerable<T>)ImmutableArray<T>.Empty;

    private IEnumerable<T> GetMefServices<T>()
    {
        if (typeof(T) == typeof(IMethodHandler))
        {
            // HACK: There is special handling for the IMethodHandler to make sure that its types remain lazy
            // Special case this to avoid providing them twice.
            yield break;
        }

        var allServices = GetRegisteredServices();
        foreach (var service in allServices)
        {
            var @interface = service.GetInterface(typeof(T).Name);
            if (@interface is not null)
            {
                var instance = TryGetService(service);
                if (instance is not null)
                {
                    yield return (T)instance;
                }
                else
                {
                    throw new Exception("Service failed to construct");
                }
            }
        }
    }

    public void Dispose()
    {
        ImmutableArray<IDisposable> disposableServices;
        lock (_gate)
        {
            disposableServices = _servicesToDispose.ToImmutableArray();
            _servicesToDispose.Clear();
        }

        foreach (var disposableService in disposableServices)
        {
            try
            {
                disposableService.Dispose();
            }
            catch (Exception ex) when (FatalError.ReportAndCatch(ex))
            {
            }
        }
    }
}
