// 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.Collections.Generic;
#if FEATURE_APPDOMAIN
using System.Threading;
#endif
using System.Reflection;

using Microsoft.Build.BackEnd;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.CommandLine
{
    /// <summary>
    /// Class for executing a task in an AppDomain
    /// </summary>
    [Serializable]
    internal class OutOfProcTaskAppDomainWrapperBase
#if FEATURE_APPDOMAIN
        : MarshalByRefObject
#endif
    {
        /// <summary>
        /// This is the actual user task whose instance we will create and invoke Execute
        /// </summary>
        private ITask wrappedTask;

#if FEATURE_APPDOMAIN
        /// <summary>
        /// This is an appDomain instance if any is created for running this task
        /// </summary>
        /// <comments>
        /// TaskAppDomain's non-serializability should never be an issue since even if we start running the wrapper
        /// in a separate appdomain, we will not be trying to load the task on one side of the serialization
        /// boundary and run it on the other.
        /// </comments>
        [NonSerialized]
        private AppDomain _taskAppDomain;
#endif

        /// <summary>
        /// Need to keep the build engine around in order to log from the task loader.
        /// </summary>
        private IBuildEngine buildEngine;

        /// <summary>
        /// Need to keep track of the task name also so that we can log valid information
        /// from the task loader.
        /// </summary>
        private string taskName;

        /// <summary>
        /// This is the actual user task whose instance we will create and invoke Execute
        /// </summary>
        public ITask WrappedTask
        {
            get { return wrappedTask; }
        }

        /// <summary>
        /// We have a cancel already requested
        /// This can happen before we load the module and invoke execute.
        /// </summary>
        internal bool CancelPending
        {
            get;
            set;
        }

        /// <summary>
        /// This is responsible for invoking Execute on the Task
        /// Any method calling ExecuteTask must remember to call CleanupTask
        /// </summary>
        /// <remarks>
        /// We also allow the Task to have a reference to the BuildEngine by design
        /// at ITask.BuildEngine
        /// </remarks>
        /// <param name="oopTaskHostNode">The OutOfProcTaskHostNode as the BuildEngine</param>
        /// <param name="taskName">The name of the task to be executed</param>
        /// <param name="taskLocation">The path of the task binary</param>
        /// <param name="taskFile">The path to the project file in which the task invocation is located.</param>
        /// <param name="taskLine">The line in the project file where the task invocation is located.</param>
        /// <param name="taskColumn">The column in the project file where the task invocation is located.</param>
        /// <param name="appDomainSetup">The AppDomainSetup that we want to use to launch our AppDomainIsolated tasks</param>
        /// <param name="taskParams">Parameters that will be passed to the task when created</param>
        /// <returns>Task completion result showing success, failure or if there was a crash</returns>
        internal OutOfProcTaskHostTaskResult ExecuteTask(
                IBuildEngine oopTaskHostNode,
                string taskName,
                string taskLocation,
                string taskFile,
                int taskLine,
                int taskColumn,
#if FEATURE_APPDOMAIN
                AppDomainSetup appDomainSetup,
#endif
                IDictionary<string, TaskParameter> taskParams)
        {
            buildEngine = oopTaskHostNode;
            this.taskName = taskName;

#if FEATURE_APPDOMAIN
            _taskAppDomain = null;
#endif
            wrappedTask = null;

            LoadedType taskType = null;
            try
            {
                TypeLoader typeLoader = new TypeLoader(TaskLoader.IsTaskClass);
                taskType = typeLoader.Load(taskName, AssemblyLoadInfo.Create(null, taskLocation), false);
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                // If it's a TargetInvocationException, we only care about the contents of the inner exception,
                // so just save that instead.
                Exception exceptionToReturn = e is TargetInvocationException ? e.InnerException : e;

                return new OutOfProcTaskHostTaskResult(
                                TaskCompleteType.CrashedDuringInitialization,
                                exceptionToReturn,
                                "TaskInstantiationFailureError",
                                [taskName, taskLocation, String.Empty]);
            }

            OutOfProcTaskHostTaskResult taskResult;
            if (taskType.HasSTAThreadAttribute)
            {
#if FEATURE_APARTMENT_STATE
                taskResult = InstantiateAndExecuteTaskInSTAThread(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn,
#if FEATURE_APPDOMAIN
                    appDomainSetup,
#endif
                    taskParams);
#else
                return new OutOfProcTaskHostTaskResult(
                                                    TaskCompleteType.CrashedDuringInitialization,
                                                    null,
                                                    "TaskInstantiationFailureNotSupported",
                                                    [taskName, taskLocation, typeof(RunInSTAAttribute).FullName]);
#endif
            }
            else
            {
                taskResult = InstantiateAndExecuteTask(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn,
#if FEATURE_APPDOMAIN
                    appDomainSetup,
#endif
                    taskParams);
            }

            return taskResult;
        }

        /// <summary>
        /// This is responsible for cleaning up the task after the OutOfProcTaskHostNode has gathered everything it needs from this execution
        /// For example: We will need to hold on new AppDomains created until we finish getting all outputs from the task
        /// Add any other cleanup tasks here. Any method calling ExecuteTask must remember to call CleanupTask.
        /// </summary>
        internal void CleanupTask()
        {
#if FEATURE_APPDOMAIN
            if (_taskAppDomain != null)
            {
                AppDomain.Unload(_taskAppDomain);
            }

            TaskLoader.RemoveAssemblyResolver();
#endif
            wrappedTask = null;
        }

#if FEATURE_APARTMENT_STATE
        /// <summary>
        /// Execute a task on the STA thread.
        /// </summary>
        /// <comment>
        /// STA thread launching code lifted from XMakeBuildEngine\BackEnd\Components\RequestBuilder\TaskBuilder.cs, ExecuteTaskInSTAThread method.
        /// Any bug fixes made to this code, please ensure that you also fix that code.
        /// </comment>
        private OutOfProcTaskHostTaskResult InstantiateAndExecuteTaskInSTAThread(
                IBuildEngine oopTaskHostNode,
                LoadedType taskType,
                string taskName,
                string taskLocation,
                string taskFile,
                int taskLine,
                int taskColumn,
#if FEATURE_APPDOMAIN
                AppDomainSetup appDomainSetup,
#endif
                IDictionary<string, TaskParameter> taskParams)
        {
            ManualResetEvent taskRunnerFinished = new ManualResetEvent(false);
            OutOfProcTaskHostTaskResult taskResult = null;
            Exception exceptionFromExecution = null;

            try
            {
                ThreadStart taskRunnerDelegate = delegate ()
                {
                    try
                    {
                        taskResult = InstantiateAndExecuteTask(
                                                oopTaskHostNode,
                                                taskType,
                                                taskName,
                                                taskLocation,
                                                taskFile,
                                                taskLine,
                                                taskColumn,
#if FEATURE_APPDOMAIN
                                                appDomainSetup,
#endif
                                                taskParams);
                    }
                    catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
                    {
                        exceptionFromExecution = e;
                    }
                    finally
                    {
                        taskRunnerFinished.Set();
                    }
                };

                Thread staThread = new Thread(taskRunnerDelegate);
                staThread.SetApartmentState(ApartmentState.STA);
                staThread.Name = "MSBuild STA task runner thread";
                staThread.CurrentCulture = Thread.CurrentThread.CurrentCulture;
                staThread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture;
                staThread.Start();

                // TODO: Why not just Join on the thread???
                taskRunnerFinished.WaitOne();
            }
            finally
            {
#if CLR2COMPATIBILITY
                taskRunnerFinished.Close();
#else
                taskRunnerFinished.Dispose();
#endif
                taskRunnerFinished = null;
            }

            if (exceptionFromExecution != null)
            {
                // Unfortunately this will reset the callstack
                throw exceptionFromExecution;
            }

            return taskResult;
        }
#endif

        /// <summary>
        /// Do the work of actually instantiating and running the task.
        /// </summary>
        private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask(
                IBuildEngine oopTaskHostNode,
                LoadedType taskType,
                string taskName,
                string taskLocation,
                string taskFile,
                int taskLine,
                int taskColumn,
#if FEATURE_APPDOMAIN
                AppDomainSetup appDomainSetup,
#endif
                IDictionary<string, TaskParameter> taskParams)
        {
#if FEATURE_APPDOMAIN
            _taskAppDomain = null;
#endif
            wrappedTask = null;

            try
            {
#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter
                wrappedTask = TaskLoader.CreateTask(
                    taskType,
                    taskName,
                    taskFile,
                    taskLine,
                    taskColumn,
                    new TaskLoader.LogError(LogErrorDelegate),
#if FEATURE_APPDOMAIN
                    appDomainSetup,
                    // custom app domain assembly loading won't be available for task host
                    null,
#endif
                    true /* always out of proc */
#if FEATURE_APPDOMAIN
                    , out _taskAppDomain
#endif
                    );
#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter
                wrappedTask.BuildEngine = oopTaskHostNode;
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                Exception exceptionToReturn = e;

                // If it's a TargetInvocationException, we only care about the contents of the inner exception,
                // so just save that instead.
                if (e is TargetInvocationException)
                {
                    exceptionToReturn = e.InnerException;
                }

                return new OutOfProcTaskHostTaskResult(
                    TaskCompleteType.CrashedDuringInitialization,
                    exceptionToReturn,
                    "TaskInstantiationFailureError",
                    [taskName, taskLocation, String.Empty]);
            }

            foreach (KeyValuePair<string, TaskParameter> param in taskParams)
            {
                try
                {
                    PropertyInfo paramInfo = wrappedTask.GetType().GetProperty(param.Key, BindingFlags.Instance | BindingFlags.Public);
                    paramInfo.SetValue(wrappedTask, param.Value?.WrappedParameter, null);
                }
                catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
                {
                    return new OutOfProcTaskHostTaskResult(
                                TaskCompleteType.CrashedDuringInitialization,
                                // If it's a TargetInvocationException, we only care about the contents of the inner exception, so save that instead.
                                e is TargetInvocationException ? e.InnerException : e,
                                "InvalidTaskAttributeError",
                                [param.Key, param.Value.ToString(), taskName]);
                }
            }

            bool success = false;
            try
            {
                if (CancelPending)
                {
                    return new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure);
                }

                // If it didn't crash and return before now, we're clear to go ahead and execute here.
                success = wrappedTask.Execute();
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                return new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, e);
            }

            PropertyInfo[] finalPropertyValues = wrappedTask.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);

            IDictionary<string, Object> finalParameterValues = new Dictionary<string, Object>(StringComparer.OrdinalIgnoreCase);
            foreach (PropertyInfo value in finalPropertyValues)
            {
                // only record outputs
                if (value.GetCustomAttributes(typeof(OutputAttribute), true).Length > 0)
                {
                    try
                    {
                        finalParameterValues[value.Name] = value.GetValue(wrappedTask, null);
                    }
                    catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
                    {
                        // If it's not a critical exception, we assume there's some sort of problem in the parameter getter --
                        // so save the exception, and we'll re-throw once we're back on the main node side of the
                        // communications pipe.
                        finalParameterValues[value.Name] = e;
                    }
                }
            }

            return new OutOfProcTaskHostTaskResult(success ? TaskCompleteType.Success : TaskCompleteType.Failure, finalParameterValues);
        }

        /// <summary>
        /// Logs errors from TaskLoader
        /// </summary>
        private void LogErrorDelegate(string taskLocation, int taskLine, int taskColumn, string message, params object[] messageArgs)
        {
            buildEngine.LogErrorEvent(new BuildErrorEventArgs(
                                                    null,
                                                    null,
                                                    taskLocation,
                                                    taskLine,
                                                    taskColumn,
                                                    0,
                                                    0,
                                                    ResourceUtilities.FormatString(AssemblyResources.GetString(message), messageArgs),
                                                    null,
                                                    taskName));
        }
    }
}
