// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Reflection;
using System.Diagnostics;

#if FEATURE_ASSEMBLYLOADCONTEXT
using System.Runtime.Loader;
#endif
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;

#nullable disable

namespace Microsoft.Build.BackEnd.Logging
{
    /// <summary>
    /// This is a helper class to install an AssemblyResolver event handler in whatever AppDomain this class is created in.
    /// </summary>
    internal class TaskEngineAssemblyResolver
#if FEATURE_APPDOMAIN
        : MarshalByRefObject
#endif
    {
        /// <summary>
        /// This public default constructor is needed so that instances of this class can be created by NDP.
        /// </summary>
        internal TaskEngineAssemblyResolver()
        {
            // do nothing
        }

        /// <summary>
        /// Initializes the instance.
        /// </summary>
        /// <param name="taskAssemblyFileToResolve"></param>
        internal void Initialize(string taskAssemblyFileToResolve)
        {
            _taskAssemblyFile = taskAssemblyFileToResolve;
        }

        /// <summary>
        /// Installs an AssemblyResolve handler in the current AppDomain. This class can be created in any AppDomain, 
        /// so it's possible to create an AppDomain, create an instance of this class in it and use this method to install
        /// an event handler in that AppDomain. Since the event handler instance is stored internally, this method
        /// should only be called once before a corresponding call to RemoveHandler (not that it would make sense to do 
        /// anything else).
        /// </summary>
        internal void InstallHandler()
        {
#if FEATURE_APPDOMAIN
            Debug.Assert(_eventHandler == null, "The TaskEngineAssemblyResolver.InstallHandler method should only be called once!");

            _eventHandler = new ResolveEventHandler(ResolveAssembly);

            AppDomain.CurrentDomain.AssemblyResolve += _eventHandler;
#else
            _eventHandler = new Func<AssemblyLoadContext, AssemblyName, Assembly>(ResolveAssembly);

            AssemblyLoadContext.Default.Resolving += _eventHandler;
#endif
        }



        /// <summary>
        /// Removes the event handler.
        /// </summary>
        internal void RemoveHandler()
        {
            if (_eventHandler != null)
            {
#if FEATURE_APPDOMAIN
                AppDomain.CurrentDomain.AssemblyResolve -= _eventHandler;
#else
                AssemblyLoadContext.Default.Resolving -= _eventHandler;
#endif
                _eventHandler = null;
            }
            else
            {
                Debug.Assert(false, "There is no handler to remove.");
            }
        }


#if FEATURE_APPDOMAIN
        /// <summary>
        /// This is an assembly resolution handler necessary for fixing up types instantiated in different
        /// AppDomains and loaded with a Assembly.LoadFrom equivalent call. See comments in TaskEngine.ExecuteTask
        /// for more details.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        internal Assembly ResolveAssembly(object sender, ResolveEventArgs args)
#else
        private Assembly ResolveAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
#endif
        {
            // Is this our task assembly?
            if (_taskAssemblyFile != null)
            {
                if (FileSystems.Default.FileExists(_taskAssemblyFile))
                {
                    try
                    {
#if FEATURE_APPDOMAIN
                        AssemblyNameExtension taskAssemblyName = new AssemblyNameExtension(AssemblyName.GetAssemblyName(_taskAssemblyFile));
                        AssemblyNameExtension argAssemblyName = new AssemblyNameExtension(args.Name);

                        if (taskAssemblyName.Equals(argAssemblyName))
                        {
#if (!CLR2COMPATIBILITY)
                            return Assembly.UnsafeLoadFrom(_taskAssemblyFile);
#else
                            return Assembly.LoadFrom(_taskAssemblyFile);
#endif

                        }
#else // !FEATURE_APPDOMAIN
                        AssemblyNameExtension taskAssemblyName = new AssemblyNameExtension(AssemblyLoadContext.GetAssemblyName(_taskAssemblyFile));
                        AssemblyNameExtension argAssemblyName = new AssemblyNameExtension(assemblyName);
                        if (taskAssemblyName.Equals(argAssemblyName))
                        {
                            return AssemblyLoadContext.Default.LoadFromAssemblyPath(_taskAssemblyFile);
                        }
#endif
                    }
                    // any problems with the task assembly? return null.
                    catch (FileNotFoundException)
                    {
                        return null;
                    }
                    catch (BadImageFormatException)
                    {
                        return null;
                    }
                }
            }

            // otherwise, have a nice day.
            return null;
        }

#if FEATURE_APPDOMAIN
        /// <summary>
        /// Overridden to give this class infinite lease time. Otherwise we end up with a limited
        /// lease (5 minutes I think) and instances can expire if they take long time processing.
        /// </summary>
        [System.Security.SecurityCritical]
        public override object InitializeLifetimeService()
        {
            // null means infinite lease time
            return null;
        }


        // we have to store the event handler instance in case we have to remove it
        private ResolveEventHandler _eventHandler = null;
#else
        private Func<AssemblyLoadContext, AssemblyName, Assembly> _eventHandler = null;
#endif
        // path to the task assembly, but only if it's loaded using LoadFrom. If it's loaded with Load, this is null.
        private string _taskAssemblyFile = null;
    }
}
