// 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;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.Execution
{
    /// <summary>
    /// Controls where projects must be built.
    /// </summary>
    public enum NodeAffinity
    {
        /// <summary>
        /// The project may only be scheduled on the in-proc node.  This happens automatically if there is a host object or if a ProjectInstance
        /// was specified.  A host may wish to specify it if they know a task depends explicitly on shared static data or other host-provided
        /// objects.
        /// </summary>
        InProc,

        /// <summary>
        /// The project may only be scheduled on an out-of-proc node.  A host may wish to specify this if it is known the project being built
        /// could contaminate the host environment (or the host contaminates the environment while a build is proceeding.)
        /// </summary>
        OutOfProc,

        /// <summary>
        /// The project may be scheduled anywhere.
        /// </summary>
        Any
    }

    /// <summary>
    /// Implementation of HostServices that
    /// mediates access from the build to the host.
    /// </summary>
    [DebuggerDisplay("#Entries={_hostObjectMap.Count}")]
    public class HostServices : ITranslatable
    {
        /// <summary>
        /// Collection storing host objects for particular project/task/target combinations.
        /// </summary>
        private Dictionary<string, HostObjects> _hostObjectMap;

        /// <summary>
        /// A mapping of project file names to their node affinities.  An entry for String.Empty means that
        /// all projects which don't otherwise have an affinity should use that affinity.
        /// </summary>
        private Dictionary<string, NodeAffinity> _projectAffinities;

        private Lazy<IRunningObjectTableWrapper> _runningObjectTable = new Lazy<IRunningObjectTableWrapper>(() => new RunningObjectTable());

        /// <summary>
        /// Gets any host object applicable to this task name
        /// where the task appears within a target and project with the specified names.
        /// If no host object exists, returns null.
        /// </summary>
        public ITaskHost GetHostObject(string projectFile, string targetName, string taskName)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectFile);
            ErrorUtilities.VerifyThrowArgumentNull(targetName);
            ErrorUtilities.VerifyThrowArgumentNull(taskName);

            HostObjects hostObjects;
            if (_hostObjectMap == null || !_hostObjectMap.TryGetValue(projectFile, out hostObjects))
            {
                return null;
            }

            var monikerNameOrITaskHost =
                hostObjects.GetAnyMatchingMonikerNameOrITaskHost(targetName, taskName);

            if (monikerNameOrITaskHost == null)
            {
                return null;
            }
            else
            {
                if (monikerNameOrITaskHost.IsMoniker)
                {
                    if (NativeMethodsShared.IsWindows)
                    {
                        try
                        {
                            object objectFromRunningObjectTable =
                                _runningObjectTable.Value.GetObject(monikerNameOrITaskHost.MonikerName);
                            return (ITaskHost)objectFromRunningObjectTable;
                        }
                        catch (Exception ex) when (ex is COMException || ex is InvalidCastException)
                        {
                            throw new HostObjectException(projectFile, targetName, taskName, ex);
                        }
                    }
                    else
                    {
                        throw new HostObjectException(
                            projectFile,
                            targetName,
                            taskName,
                            "COM Monikers can only be used on Windows");
                    }
                }
                else
                {
                    return monikerNameOrITaskHost.TaskHost;
                }
            }
        }

        /// <summary>
        /// Register a host object for a particular task/target pair.
        /// Overwrites any existing host object.
        /// </summary>
        public void RegisterHostObject(string projectFile, string targetName, string taskName, ITaskHost hostObject)
        {

            /* Unmerged change from project 'Microsoft.Build (netcoreapp2.1)'
            Before:
                        ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile");
                        ErrorUtilities.VerifyThrowArgumentNull(targetName, "targetName");
            After:
                        ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile));
                        ErrorUtilities.VerifyThrowArgumentNull(targetName, "targetName));
            */
            ErrorUtilities.VerifyThrowArgumentNull(projectFile);
            ErrorUtilities.VerifyThrowArgumentNull(targetName);
            ErrorUtilities.VerifyThrowArgumentNull(taskName);

            // We can only set the host object to a non-null value if the affinity for the project is not out of proc, or if it is, it is only implicitly
            // out of proc, in which case it will become in-proc after this call completes.  See GetNodeAffinity.
            bool isExplicit;
            bool hasExplicitOutOfProcAffinity = (GetNodeAffinity(projectFile, out isExplicit) == NodeAffinity.OutOfProc) && isExplicit;
            ErrorUtilities.VerifyThrowInvalidOperation(!hasExplicitOutOfProcAffinity || hostObject == null, "InvalidHostObjectOnOutOfProcProject");
            _hostObjectMap ??= new Dictionary<string, HostObjects>(StringComparer.OrdinalIgnoreCase);

            HostObjects hostObjects = GetHostObjectsFromMapByKeyOrCreateNew(projectFile);

            hostObjects.RegisterHostObject(targetName, taskName, hostObject);
        }

        /// <summary>
        /// Register a remote host object for a particular task/target pair.
        /// The remote host object require registered in Running Object Table(ROT) already.
        /// Overwrites any existing host object.
        ///
        /// It's caller's responsibly:
        /// To maintain the live cycle of the host object.
        /// Register and unregister from ROT.
        /// Ensure the host object has appropriate COM interface that can be used in task.
        /// </summary>
        /// <param name="projectFile">project file name</param>
        /// <param name="targetName">target name</param>
        /// <param name="taskName">task name</param>
        /// <param name="monikerName">the Moniker used to register host object in ROT</param>
        [SupportedOSPlatform("windows")]
        public void RegisterHostObject(string projectFile, string targetName, string taskName, string monikerName)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectFile);
            ErrorUtilities.VerifyThrowArgumentNull(targetName);
            ErrorUtilities.VerifyThrowArgumentNull(taskName);
            ErrorUtilities.VerifyThrowArgumentNull(monikerName);

            _hostObjectMap ??= new Dictionary<string, HostObjects>(StringComparer.OrdinalIgnoreCase);

            HostObjects hostObjects = GetHostObjectsFromMapByKeyOrCreateNew(projectFile);

            hostObjects.RegisterHostObject(targetName, taskName, monikerName);
        }

        /// <summary>
        /// Unregister the project's host objects, if any and remove any node affinities associated with it.
        /// </summary>
        public void UnregisterProject(string projectFullPath)
        {
            if (projectFullPath != null)
            {
                _hostObjectMap?.Remove(projectFullPath);
                _projectAffinities?.Remove(projectFullPath);
            }
        }

        /// <summary>
        /// Retrieves the node affinity for a particular project file.
        /// </summary>
        public NodeAffinity GetNodeAffinity(string projectFile)
        {
            bool isExplicit;
            return GetNodeAffinity(projectFile, out isExplicit);
        }

        /// <summary>
        /// Sets the node affinity for a particular project file.
        /// </summary>
        /// <param name="projectFile">
        /// The project file.  If set to String.Empty, all projects will use the specified affinity.  If set to null, all affinities will be cleared.
        /// </param>
        /// <param name="nodeAffinity">The <see cref="NodeAffinity"/> to set.</param>
        public void SetNodeAffinity(string projectFile, NodeAffinity nodeAffinity)
        {
            if (projectFile == null)
            {
                _projectAffinities = null;
            }
            else
            {
                if (HasInProcessHostObject(projectFile))
                {
                    ErrorUtilities.VerifyThrowInvalidOperation(nodeAffinity == NodeAffinity.InProc, "InvalidAffinityForProjectWithHostObject");
                }

                if (_projectAffinities == null)
                {
                    _projectAffinities = new Dictionary<string, NodeAffinity>(StringComparer.OrdinalIgnoreCase);
                }

                _projectAffinities[projectFile] = nodeAffinity;
            }
        }

        /// <summary>
        /// Updates the host object table when a project is renamed.
        /// Old full path may be null.
        /// </summary>
        public void OnRenameProject(string oldFullPath, string newFullPath)
        {
            HostObjects hostObjects;
            if (oldFullPath != null && _hostObjectMap != null && _hostObjectMap.TryGetValue(oldFullPath, out hostObjects))
            {
                _hostObjectMap.Remove(oldFullPath);
                _hostObjectMap[newFullPath] = hostObjects;
            }
        }

        /// <summary>
        /// Returns true if there is any in process host object registered for this project file.
        /// </summary>
        internal bool HasInProcessHostObject(string projectFile)
        {
            if (_hostObjectMap == null)
            {
                return false;
            }

            HostObjects hostObjects;

            if (!_hostObjectMap.TryGetValue(projectFile, out hostObjects))
            {
                return false;
            }

            return hostObjects.HasRegisteredInProcessHostObjects;
        }

        /// <summary>
        /// Retrieves the node affinity for a particular project file.
        /// </summary>
        private NodeAffinity GetNodeAffinity(string projectFile, out bool isExplicit)
        {
            isExplicit = false;

            // Projects with a registered host object must build in-proc
            if (HasInProcessHostObject(projectFile))
            {
                return NodeAffinity.InProc;
            }

            // Now see if a specific affinity has been provided.
            if (_projectAffinities != null)
            {
                NodeAffinity affinity;
                if (_projectAffinities.TryGetValue(projectFile, out affinity))
                {
                    isExplicit = true;
                    return affinity;
                }

                if (_projectAffinities.TryGetValue(String.Empty, out affinity))
                {
                    return affinity;
                }
            }

            // Attempts to find a specific affinity failed, so just go with Any.
            return NodeAffinity.Any;
        }

        private HostObjects GetHostObjectsFromMapByKeyOrCreateNew(string projectFile)
        {
            if (!_hostObjectMap.TryGetValue(projectFile, out var hostObjects))
            {
                hostObjects = new HostObjects();
                _hostObjectMap[projectFile] = hostObjects;
            }

            return hostObjects;
        }

        void ITranslatable.Translate(ITranslator translator)
        {
            if (translator.Mode == TranslationDirection.ReadFromStream)
            {
                int count = translator.Reader.ReadInt32();

                var hostObjectMap = new Dictionary<string, HostObjects>();
                for (int i = 0; i < count; i++)
                {
                    var pairKey = translator.Reader.ReadString();
                    var hostObjectMapPairKeyTargetName = translator.Reader.ReadString();
                    var hostObjectMapPairKeyTaskName = translator.Reader.ReadString();
                    var hostObjectMapPairValueMonikerName = translator.Reader.ReadString();
                    var targetTaskKey = new HostObjects.TargetTaskKey(hostObjectMapPairKeyTargetName, hostObjectMapPairKeyTaskName);
                    if (!hostObjectMap.TryGetValue(pairKey, out HostObjects hostObject))
                    {
                        hostObject = new HostObjects();
                        hostObjectMap[pairKey] = hostObject;
                    }

                    if (!hostObject._hostObjects.ContainsKey(targetTaskKey))
                    {
                        hostObject._hostObjects.Add(targetTaskKey, new MonikerNameOrITaskHost(hostObjectMapPairValueMonikerName));
                    }
                }
                _hostObjectMap = hostObjectMap;
            }

            if (translator.Mode == TranslationDirection.WriteToStream)
            {
                if (_hostObjectMap == null)
                {
                    translator.Writer.Write(0);
                }
                else
                {
                    var count = 0;
                    foreach (var pair in _hostObjectMap)
                    {
                        foreach (var hostObjectMapPair in pair.Value._hostObjects)
                        {
                            if (hostObjectMapPair.Value.IsMoniker)
                            {
                                count++;
                            }
                        }
                    }

                    translator.Writer.Write(count);

                    foreach (var pair in _hostObjectMap)
                    {
                        foreach (var hostObjectMapPair in pair.Value._hostObjects)
                        {
                            if (hostObjectMapPair.Value.IsMoniker)
                            {
                                translator.Writer.Write(pair.Key);
                                translator.Writer.Write(hostObjectMapPair.Key._targetName);
                                translator.Writer.Write(hostObjectMapPair.Key._taskName);
                                translator.Writer.Write(hostObjectMapPair.Value.MonikerName);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Test only
        /// </summary>
        /// <param name="runningObjectTable"></param>
        [SupportedOSPlatform("windows")]
        internal void SetTestRunningObjectTable(IRunningObjectTableWrapper runningObjectTable)
        {
            _runningObjectTable = new Lazy<IRunningObjectTableWrapper>(() => runningObjectTable);
        }

        internal class MonikerNameOrITaskHost
        {
            public ITaskHost TaskHost { get; }
            public string MonikerName { get; }
            public bool IsTaskHost { get; } = false;
            public bool IsMoniker { get; } = false;
            public MonikerNameOrITaskHost(ITaskHost taskHost)
            {
                TaskHost = taskHost;
                IsTaskHost = true;
            }

            public MonikerNameOrITaskHost(string monikerName)
            {
                MonikerName = monikerName;
                IsMoniker = true;
            }
        }

        /// <summary>
        /// Bag holding host object information for a single project file.
        /// </summary>
        [DebuggerDisplay("#HostObjects={_hostObjects.Count}")]
        private class HostObjects
        {
            /// <summary>
            /// The mapping of targets and tasks to host objects.
            /// </summary>
            internal Dictionary<TargetTaskKey, MonikerNameOrITaskHost> _hostObjects;

            /// <summary>
            /// Constructor
            /// </summary>
            internal HostObjects()
            {
                _hostObjects = new Dictionary<TargetTaskKey, MonikerNameOrITaskHost>(1);
            }

            /// <summary>
            /// Accessor which indicates if there are any registered in process host objects.
            /// </summary>
            internal bool HasRegisteredInProcessHostObjects
            {
                get
                {
                    return _hostObjects.Any(h => h.Value.IsTaskHost);
                }
            }

            /// <summary>
            /// Registers a host object for this project file
            /// </summary>
            internal void RegisterHostObject(string targetName, string taskName, ITaskHost hostObject)
            {
                if (hostObject == null)
                {
                    _hostObjects.Remove(new TargetTaskKey(targetName, taskName));
                }
                else
                {
                    _hostObjects[new TargetTaskKey(targetName, taskName)] = new MonikerNameOrITaskHost(hostObject);
                }
            }

            /// <summary>
            /// Registers a host object for this project file
            /// </summary>
            [SupportedOSPlatform("windows")]
            internal void RegisterHostObject(string targetName, string taskName, string monikerName)
            {
                if (monikerName == null)
                {
                    _hostObjects.Remove(new TargetTaskKey(targetName, taskName));
                }
                else
                {
                    _hostObjects[new TargetTaskKey(targetName, taskName)] = new MonikerNameOrITaskHost(monikerName);
                }
            }

            /// <summary>
            /// Gets any host object for this project file matching the task and target names specified.
            /// </summary>
            internal MonikerNameOrITaskHost GetAnyMatchingMonikerNameOrITaskHost(string targetName, string taskName)
            {
                if (_hostObjects.TryGetValue(new TargetTaskKey(targetName, taskName), out MonikerNameOrITaskHost hostObject))
                {
                    return hostObject;
                }

                return null;
            }

            /// <summary>
            /// Equatable key for the table
            /// </summary>
            internal struct TargetTaskKey : IEquatable<TargetTaskKey>
            {
                /// <summary>
                /// Target name
                /// </summary>
                internal string _targetName;

                /// <summary>
                /// Task name
                /// </summary>
                internal string _taskName;

                /// <summary>
                /// Constructor
                /// </summary>
                public TargetTaskKey(string targetName, string taskName)
                {
                    _targetName = targetName;
                    _taskName = taskName;
                }

                /// <summary>
                /// Implementation of IEquatable.
                /// </summary>
                public bool Equals(TargetTaskKey other)
                {
                    bool result = (String.Equals(_targetName, other._targetName, StringComparison.OrdinalIgnoreCase) &&
                                   String.Equals(_taskName, other._taskName, StringComparison.OrdinalIgnoreCase));

                    return result;
                }
            }
        }
    }
}
