// 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.Composition;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Design;
using Microsoft.VisualStudio.Shell.Interop;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;

[ExportWorkspaceServiceFactory(typeof(IFrameworkAssemblyPathResolver), ServiceLayer.Host), Shared]
internal sealed class VisualStudioFrameworkAssemblyPathResolverFactory : IWorkspaceServiceFactory
{
    private readonly IThreadingContext _threadingContext;
    private readonly IServiceProvider _serviceProvider;

    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public VisualStudioFrameworkAssemblyPathResolverFactory(IThreadingContext threadingContext, SVsServiceProvider serviceProvider)
    {
        _threadingContext = threadingContext;
        _serviceProvider = serviceProvider;
    }

    public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
        => new Service(_threadingContext, workspaceServices.Workspace as VisualStudioWorkspace, _serviceProvider);

    private sealed class Service(IThreadingContext threadingContext, VisualStudioWorkspace? workspace, IServiceProvider serviceProvider) : IFrameworkAssemblyPathResolver
    {
        private readonly IThreadingContext _threadingContext = threadingContext;
        private readonly VisualStudioWorkspace? _workspace = workspace;
        private readonly IServiceProvider _serviceProvider = serviceProvider;

        public string? ResolveAssemblyPath(
            ProjectId projectId,
            string assemblyName,
            string? fullyQualifiedTypeName)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            var assembly = ResolveAssembly(projectId, assemblyName);
            if (assembly != null)
            {
                // Codebase specifies where the assembly is on disk.  However, it's in 
                // full URI format (i.e. file://c:/...). This will allow us to get the 
                // actual local in the normal path format.
                if (Uri.TryCreate(assembly.CodeBase, UriKind.RelativeOrAbsolute, out var uri) &&
                    CanResolveType(assembly, fullyQualifiedTypeName))
                {
                    return uri.LocalPath;
                }
            }

            return null;
        }

        private static bool CanResolveType(Assembly assembly, string? fullyQualifiedTypeName)
        {
            if (fullyQualifiedTypeName == null)
            {
                // nothing to resolve.
                return true;
            }

            // We only get a type name without generic indicators.  So try to few different
            // generic versions of the type name in case any of those hit.  it's highly 
            // unlikely we'd find something with more than 4 generic parameters, so only try
            // up that point.
            for (var i = 0; i < 5; i++)
            {
                var name = i == 0
                    ? fullyQualifiedTypeName
                    : fullyQualifiedTypeName + "`" + i;

                try
                {
                    var type = assembly.GetType(name, throwOnError: false);
                    if (type != null)
                    {
                        return true;
                    }
                }
                catch (FileNotFoundException)
                {
                }
                catch (FileLoadException)
                {
                }
                catch (BadImageFormatException)
                {
                }
            }

            return false;
        }

        private Assembly? ResolveAssembly(ProjectId projectId, string assemblyName)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            if (_workspace == null)
            {
                return null;
            }

            var hierarchy = _workspace.GetHierarchy(projectId);
            if (hierarchy == null ||
                !hierarchy.TryGetProperty((__VSHPROPID)__VSHPROPID4.VSHPROPID_TargetFrameworkMoniker, out string? targetMoniker) ||
                targetMoniker == null)
            {
                return null;
            }

            try
            {
                // Below we use the DesignTimeAssemblyResolver functionality of VS to 
                // determine if we can resolve the specified assembly name in the context
                // of this project.  However, this service does not do the right thing
                // in UWP apps.  Specifically, it *will* resolve the assembly to a 
                // reference assembly, even though that's never what we want.  In order
                // to deal with that, we put in this little check where we do not allow
                // reference assembly resolution if the projects TargetFrameworkMoniker
                // is ".NETCore, Version=5.0" or greater.

                var frameworkName = new FrameworkName(targetMoniker);
                if (StringComparer.OrdinalIgnoreCase.Equals(frameworkName.Identifier, ".NETCore") &&
                    frameworkName.Version >= new Version(major: 5, minor: 0))
                {
                    return null;
                }
            }
            catch (ArgumentException)
            {
                // Something wrong with our TFM.  We don't have enough information to 
                // properly resolve this assembly name.
                return null;
            }

            try
            {
                var frameworkProvider = new VsTargetFrameworkProvider(
                    (IVsFrameworkMultiTargeting)_serviceProvider.GetService(typeof(SVsFrameworkMultiTargeting)),
                    targetMoniker,
                    (IVsSmartOpenScope)_serviceProvider.GetService(typeof(SVsSmartOpenScope)));
                return frameworkProvider.GetReflectionAssembly(new AssemblyName(assemblyName));
            }
            catch (InvalidOperationException)
            {
                // VsTargetFrameworkProvider throws InvalidOperationException in the 
                // some cases (like when targeting packs are missing).  In that case
                // we can't resolve this path.
                return null;
            }
        }
    }
}
