/********************************************************************++
Copyright (c) Microsoft Corporation.  All rights reserved.
--********************************************************************/

#pragma warning disable 1634, 1691

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Security;
using System.Management.Automation.Host;
using Microsoft.PowerShell.Commands.Internal.Format;

namespace System.Management.Automation.Runspaces
{
    /// <summary>
    /// Defines configuration information for runspace.
    ///
    /// </summary>
    /// <remarks>
    ///
    /// Developers may want to derive from this class when writing their own
    /// host to host monad engine. This will allow host to control the behaviour
    /// of monad engine through customized runspace configuration.
    ///
    ///
    /// </remarks>
    /// <!--
    /// RunspaceConfiguration class encompass configuration information
    /// used to tailor the behaviour of mini-shell run time.
    ///
    /// Information that defines mini-shell run time includes following
    /// categories:
    ///
    ///     1. cmdlets: the list of cmdlets that are strong-bond with
    ///                 the mini-shell executable.
    ///     2. providers: the list of providers that are strong-bond with
    ///                 the mini-shell executable.
    ///     3. relationships: the list of relationships that are strong-bond
    ///                 with the mini-shell executable.
    ///     4. types: these are the format.ps1xml which are embedded as resources
    ///                 in mini-shell executable.
    ///     5. formats: these are the display.xml which are embedded as resources
    ///                 in mini-shell executable.
    ///     6. scripts: these are the scripts that are embedded as resources in
    ///                 mini-shell executable.
    ///     7. profile: this is a special kind of script to be executed at monad
    ///                 engine start-up.
    ///     8. help related information: help banner and minishell help. These are
    ///                 embedded as a resource in mini-shell.
    ///     9. assemblies: this is a set of assemblies that will be loaded at
    ///                 engine start up time. This will make sure that a set of
    ///                 CLR types will be available to monad scripts.
    ///     10. security manager: this is the security manager to be used for
    ///                 enforcing script running, assembly loading policies.
    ///
    /// Runspace configuration information mentioned above comes into the
    /// system at different stages,
    ///
    ///     1. Intrinsic: these are the cmdlets, providers, relationships, types,
    ///        formats which are integrated parts of monad release and present in
    ///        every mini-shell executable. These information come into system at
    ///        design time.
    ///
    ///     2. BuiltIn: these are the cmdlets, providers, relationships, types, formats,
    ///        scripts, profile, help which are provided when mini-shell executable
    ///        is built when running make-kit. These information come into system at
    ///        compile time.
    ///
    ///     3. Addon: these are the cmdlets, providers, relationships etc that are added
    ///        and mini-shell run time, either through RunspaceConfigurationModifier
    ///        or directly changing the data in RunspaceConfiguration itself.
    ///        These information come into system at run time.
    ///
    /// RunspaceConfiguration class is an abstracted class including limited implementation
    /// on some interfaces related with format, type, script, profile and help.
    ///
    /// Concrete classes can be derived from this class. In most cases, concrete classes
    /// are directly generated by makekit with implementation of cmdlets, providers etc filled
    /// in.
    ///
    /// Implementation of RunspaceConfiguration focus on following apects,
    ///
    ///     1. RunspaceConfiguration Information: these are instance-based interfaces to
    ///        to return information from different configuration categories mentioned above.
    ///     2. RunspaceConfiguration Factories: these are static interfaces for creating
    ///        one instance of RunspaceConfiguration-derived class. Implementation of these
    ///        functions normally has some logic about,
    ///
    ///             a. finding appropriate RunspaceConfiguration class from which to create
    ///                an instance.
    ///             b. call RunspaceConfigurationModifier when necessary.
    ///             c. some caching capability.
    ///
    /// This class will be built with monad engine code. Following monad engine components
    /// will likely use this class,
    ///
    ///     1. Monad host will use configuration information like help banner and shell help.
    ///     2. Runspace api will use a RunspaceConfiguration instance and store it as
    ///        a member of execution context for use by command discovery, ETS, Format/Output
    ///     3. Engine start up will use information in profile and scripts.
    ///     4. Command Discovery will use cmdlet and provider information.
    ///     5. ETS will use assembly list for finding types accessible to monad scripts.
    ///     6. ETS will use type information.
    ///     7. Format and output will use format information.
    /// -->
    internal abstract class RunspaceConfiguration
    {
        #region RunspaceConfiguration Factory

        /// <summary>
        /// Create an instance of RunspaceConfiguration type implemented from an assembly.
        /// </summary>
        /// <param name="assemblyName">Assembly name from which to find runspace configuration implementation</param>
        /// <returns>Runspace configuration instance created</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Exception thrown when <paramref name="assemblyName"/> is null or empty.
        /// </exception>
        /// <!--
        /// This is a RunspaceConfiguration factory function which will create an instance
        /// of default RunspaceConfiguration-derived type, whose name is decided by an
        /// assembly attribute in the assembly specified.
        /// -->
        public static RunspaceConfiguration Create(string assemblyName)
        {
            if (String.IsNullOrEmpty(assemblyName))
            {
                throw PSTraceSource.NewArgumentNullException("assemblyName");
            }

            // If this assembly can't be loaded, Assembly.Load will throw an exception.
            // Just let the exception bubble up.

            Assembly assembly = null;
            foreach (Assembly asm in ClrFacade.GetAssemblies())
            {
                if (string.Equals(asm.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase))
                {
                    assembly = asm;
                    break;
                }
            }

            if (assembly == null)
            {
                assembly = Assembly.Load(new AssemblyName(assemblyName));
            }

            return Create(assembly);
        }

        /// <summary>
        /// Create one single shell runspace configuration object from console file
        /// </summary>
        /// <param name="consoleFilePath"></param>
        /// <param name="warnings"></param>
        /// <returns></returns>
        public static RunspaceConfiguration Create(string consoleFilePath, out PSConsoleLoadException warnings)
        {
            return RunspaceConfigForSingleShell.Create(consoleFilePath, out warnings);
        }

        /// <summary>
        /// Create one single shell runspace configuration object for a msh version.
        /// </summary>
        /// <returns></returns>
        public static RunspaceConfiguration Create()
        {
            return RunspaceConfigForSingleShell.CreateDefaultConfiguration();
        }

#if V2
        /// <summary>
        /// This is a RunspaceConfiguration factory function which will create an instance
        /// based on a type specified in a particular assembly.
        ///
        /// This function will likely being exposed as a public interface in v2.
        /// </summary>
        /// <param name="assemblyName"></param>
        /// <param name="runspaceConfigTypeName"></param>
        /// <returns></returns>
        private static RunspaceConfiguration Create(string assemblyName, string runspaceConfigTypeName)
        {
            // If this assembly can't be loaded, Assembly.Load will throw an exception.
            // Just let the exception bubble up.
            Assembly assembly = Assembly.Load(assemblyName);

            try
            {
                Type runspaceConfigType = assembly.GetType(runspaceConfigTypeName, true);
                return Create(runspaceConfigType);
            }
            catch (SecurityException)
            {
                throw new RunspaceConfigurationTypeException(assembly.FullName, runspaceConfigTypeName);
            }
        }
#endif

        /// <summary>
        /// Create an RunspaceConfiguration-derived instance based an RunspaceConfiguration-derived type
        /// in specified assembly. The type name is defined by assembly attribute:
        /// RunspaceConfigurationTypeAttribute.
        /// </summary>
        /// <param name="assembly"></param>
        /// <returns>Runspace configuration instance created</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Exception thrown when <paramref name="assembly"/> is null or empty.
        /// </exception>
        private static RunspaceConfiguration Create(Assembly assembly)
        {
            if (assembly == null)
                throw PSTraceSource.NewArgumentNullException("assembly");

            object[] attributes = assembly.GetCustomAttributes(typeof(RunspaceConfigurationTypeAttribute), false);

            if (attributes == null || attributes.Length == 0)
            {
                throw new RunspaceConfigurationAttributeException("RunspaceConfigurationAttributeNotExist", assembly.FullName);
            }

            if (attributes.Length > 1)
            {
                throw new RunspaceConfigurationAttributeException("RunspaceConfigurationAttributeDuplicate", assembly.FullName);
            }

            RunspaceConfigurationTypeAttribute runspaceConfigTypeAttribute = (RunspaceConfigurationTypeAttribute)attributes[0];

            try
            {
                Type runspaceConfigType = assembly.GetType(runspaceConfigTypeAttribute.RunspaceConfigurationType, true, false);
                return Create(runspaceConfigType);
            }
            catch (SecurityException)
            {
                throw new RunspaceConfigurationTypeException(assembly.FullName, runspaceConfigTypeAttribute.RunspaceConfigurationType);
            }
        }

        /// <summary>
        /// Create RunspaceConfiguration-derived instance based on a derived type.
        /// </summary>
        /// <param name="runspaceConfigType">A type that is derived from RunspaceConfiguration</param>
        /// <returns></returns>
        private static RunspaceConfiguration Create(Type runspaceConfigType)
        {
            MethodInfo method =
                runspaceConfigType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);

            if (method == null)
                return null;

            return (RunspaceConfiguration)method.Invoke(null, null);
        }

        #endregion RunspaceConfiguration Factory

        #region Shell Id

        /// <summary>
        /// Gets the shell id for current runspace configuration.
        /// </summary>
        public abstract string ShellId
        {
            get;
        }

        #endregion

        #region PSSnapin Api's

        /// <summary>
        /// Add a PSSnapin to runspace configuration.
        /// </summary>
        /// <remarks>
        /// This member provides logic for adding PSSnapin.
        ///
        /// RunspaceConfiguration derived class should not override this member.
        ///
        /// </remarks>
        /// <param name="name">name of the PSSnapin</param>
        /// <param name="warning">warning message</param>
        /// <returns></returns>
        public PSSnapInInfo AddPSSnapIn(string name, out PSSnapInException warning)
        {
            return DoAddPSSnapIn(name, out warning);
        }

        internal virtual PSSnapInInfo DoAddPSSnapIn(string name, out PSSnapInException warning)
        {
            throw PSTraceSource.NewNotSupportedException();
        }

        /// <summary>
        /// Remove a PSSnapin from runspace configuration.
        /// </summary>
        /// <remarks>
        /// This member provides logic for removing PSSnapin.
        ///
        /// RunspaceConfiguration derived class should not override this member.
        /// </remarks>
        /// <param name="name">name of the PSSnapin</param>
        /// <param name="warning">warning message</param>
        /// <returns></returns>
        public PSSnapInInfo RemovePSSnapIn(string name, out PSSnapInException warning)
        {
            return DoRemovePSSnapIn(name, out warning);
        }

        internal virtual PSSnapInInfo DoRemovePSSnapIn(string name, out PSSnapInException warning)
        {
            throw PSTraceSource.NewNotSupportedException();
        }

        #endregion

        #region Cmdlet, Provider, Relationship

        private RunspaceConfigurationEntryCollection<CmdletConfigurationEntry> _cmdlets;

        /// <summary>
        /// Gets the cmdlets defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own cmdlet list.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<CmdletConfigurationEntry> Cmdlets
        {
            get {
                return _cmdlets ?? (_cmdlets = new RunspaceConfigurationEntryCollection<CmdletConfigurationEntry>());
            }
        }

        private RunspaceConfigurationEntryCollection<ProviderConfigurationEntry> _providers;

        /// <summary>
        /// Gets the providers defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own provider list.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<ProviderConfigurationEntry> Providers
        {
            get {
                return _providers ??
                       (_providers = new RunspaceConfigurationEntryCollection<ProviderConfigurationEntry>());
            }
        }

        /*
        /// <summary>
        /// Gets the relationships defined in runspace configuration.
        /// </summary>
        public virtual Collection<RelationshipConfigurationEntry> RelationshipData
        {
            get;
        }
        */

        #endregion

        #region Types and Formats

        /// <summary>
        /// types.ps1xml information
        /// </summary>
        ///
        internal TypeTable TypeTable
        {
            get { return _typeTable ?? (_typeTable = new TypeTable()); }
        }
        private TypeTable _typeTable;

        private RunspaceConfigurationEntryCollection<TypeConfigurationEntry> _types;

        /// <summary>
        /// Gets the type data files defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own types to be
        /// loaded at engine start up.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<TypeConfigurationEntry> Types
        {
            get { return _types ?? (_types = new RunspaceConfigurationEntryCollection<TypeConfigurationEntry>()); }
        }

        private RunspaceConfigurationEntryCollection<FormatConfigurationEntry> _formats;

        /// <summary>
        /// Gets the format data files defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own formats to be
        /// loaded at engine start up.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<FormatConfigurationEntry> Formats
        {
            get {
                return _formats ?? (_formats = new RunspaceConfigurationEntryCollection<FormatConfigurationEntry>());
            }
        }

        internal TypeInfoDataBaseManager FormatDBManager { get; } = new TypeInfoDataBaseManager();

        #endregion

        #region Script Data

        private RunspaceConfigurationEntryCollection<ScriptConfigurationEntry> _scripts;

        /// <summary>
        /// Gets the scripts defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own scripts
        /// to be include in monad engine.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<ScriptConfigurationEntry> Scripts
        {
            get {
                return _scripts ?? (_scripts = new RunspaceConfigurationEntryCollection<ScriptConfigurationEntry>());
            }
        }

        #endregion

        #region Profile configuration

        private RunspaceConfigurationEntryCollection<ScriptConfigurationEntry> _initializationScripts;

        /// <summary>
        /// Gets the initialization scripts defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own initialization scripts
        /// to be run at the start up of monad engine.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<ScriptConfigurationEntry> InitializationScripts
        {
            get {
                return _initializationScripts ??
                       (_initializationScripts = new RunspaceConfigurationEntryCollection<ScriptConfigurationEntry>());
            }
        }

        #endregion

        #region Assemblies

        private RunspaceConfigurationEntryCollection<AssemblyConfigurationEntry> _assemblies;

        /// <summary>
        /// Gets the assemblies defined in runspace configuration.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own assemblies
        /// to be loaded at the start up of monad engine.
        /// </remarks>
        public virtual RunspaceConfigurationEntryCollection<AssemblyConfigurationEntry> Assemblies
        {
            get {
                return _assemblies ??
                       (_assemblies = new RunspaceConfigurationEntryCollection<AssemblyConfigurationEntry>());
            }
        }

        #endregion

        #region Security Manager

        private AuthorizationManager _authorizationManager = null;

        /// <summary>
        /// Gets the authorization manager to be used for runspace.
        /// </summary>
        /// <remarks>
        /// RunspaceConfiguration derived class can override this member to provide its own authorization
        /// manager for monad engine.
        /// </remarks>
        public virtual AuthorizationManager AuthorizationManager
        {
            get {
                return _authorizationManager ??
                       (_authorizationManager = new Microsoft.PowerShell.PSAuthorizationManager(this.ShellId));
            }
        }

        #endregion

        #region Configuration Update

        private PSHost _host = null;
        internal void Bind(ExecutionContext executionContext)
        {
            _host = executionContext.EngineHostInterface;

            Initialize(executionContext);

            this.Assemblies.OnUpdate += executionContext.UpdateAssemblyCache;

            s_runspaceInitTracer.WriteLine("initializing assembly list");
            try
            {
                this.Assemblies.Update(true);
            }
            catch (RuntimeException e)
            {
                s_runspaceInitTracer.WriteLine("assembly list initialization failed");
                MshLog.LogEngineHealthEvent(
                    executionContext,
                    MshLog.EVENT_ID_CONFIGURATION_FAILURE,
                    e,
                    Severity.Error);
                executionContext.ReportEngineStartupError(e.Message);

                throw;
            }

            if (executionContext.CommandDiscovery != null)
            {
                this.Cmdlets.OnUpdate += executionContext.CommandDiscovery.UpdateCmdletCache;

                // Force an update here so that cmdlet cache is updated in engine.
                s_runspaceInitTracer.WriteLine("initializing cmdlet list");
                try
                {
                    this.Cmdlets.Update(true);
                }
                catch (PSNotSupportedException e)
                {
                    s_runspaceInitTracer.WriteLine("cmdlet list initialization failed");
                    MshLog.LogEngineHealthEvent(
                        executionContext,
                        MshLog.EVENT_ID_CONFIGURATION_FAILURE,
                        e,
                        Severity.Error);
                    executionContext.ReportEngineStartupError(e.Message);
                    throw;
                }
            }

            if (executionContext.EngineSessionState != null)
            {
                this.Providers.OnUpdate += executionContext.EngineSessionState.UpdateProviders;

                // Force an update here so that provider cache is updated in engine.
                s_runspaceInitTracer.WriteLine("initializing provider list");
                try
                {
                    this.Providers.Update(true);
                }
                catch (PSNotSupportedException e)
                {
                    s_runspaceInitTracer.WriteLine("provider list initialization failed");
                    MshLog.LogEngineHealthEvent(
                        executionContext,
                        MshLog.EVENT_ID_CONFIGURATION_FAILURE,
                        e,
                        Severity.Error);
                    executionContext.ReportEngineStartupError(e.Message);
                    throw;
                }
            }
        }

        internal void Unbind(ExecutionContext executionContext)
        {
            if (executionContext == null)
                return;

            if (executionContext.CommandDiscovery != null)
            {
                this.Cmdlets.OnUpdate -= executionContext.CommandDiscovery.UpdateCmdletCache;
            }

            if (executionContext.EngineSessionState != null)
            {
                this.Providers.OnUpdate -= executionContext.EngineSessionState.UpdateProviders;
            }

            this.Assemblies.OnUpdate -= executionContext.UpdateAssemblyCache;
        }

        private bool _initialized = false;
        private Object _syncObject = new Object();
        internal void Initialize(ExecutionContext executionContext)
        {
#pragma warning disable 56517

            lock (_syncObject)
            {
                if (!_initialized)
                {
                    _initialized = true;

                    this.Types.OnUpdate += this.UpdateTypes;
                    this.Formats.OnUpdate += this.UpdateFormats;

                    s_runspaceInitTracer.WriteLine("initializing types information");
                    try
                    {
                        this.UpdateTypes();
                    }
                    catch (RuntimeException e)
                    {
                        s_runspaceInitTracer.WriteLine("type information initialization failed");
                        MshLog.LogEngineHealthEvent(
                            executionContext,
                            MshLog.EVENT_ID_CONFIGURATION_FAILURE,
                            e,
                            Severity.Warning);
                        executionContext.ReportEngineStartupError(e.Message);
                    }

                    s_runspaceInitTracer.WriteLine("initializing format information");
                    try
                    {
                        this.UpdateFormats(true);
                    }
                    catch (RuntimeException e)
                    {
                        s_runspaceInitTracer.WriteLine("format information initialization failed");
                        MshLog.LogEngineHealthEvent(
                            executionContext,
                            MshLog.EVENT_ID_CONFIGURATION_FAILURE,
                            e,
                            Severity.Warning);
                        executionContext.ReportEngineStartupError(e.Message);
                    }
                }
            }

#pragma warning restore 56517
        }

        internal void UpdateTypes()
        {
            Collection<string> independentErrors = new Collection<string>();
            Collection<int> entryIndicesToRemove = new Collection<int>();
            Collection<PSSnapInTypeAndFormatErrors> PSSnapinFiles = FormatAndTypeDataHelper.GetFormatAndTypesErrors(
                    this,
                    _host,
                    this.Types,
                    independentErrors,
                    entryIndicesToRemove);

            if (entryIndicesToRemove.Count > 0)
            {
                RemoveNeedlessEntries(RunspaceConfigurationCategory.Types, entryIndicesToRemove);
            }

            this.TypeTable.Update(PSSnapinFiles, _authorizationManager, _host);
            FormatAndTypeDataHelper.ThrowExceptionOnError(
                "ErrorsUpdatingTypes",
                independentErrors,
                PSSnapinFiles,
                RunspaceConfigurationCategory.Types);
        }

        internal void UpdateFormats()
        {
            UpdateFormats(false);
        }

        private void UpdateFormats(bool preValidated)
        {
            Collection<string> independentErrors = new Collection<string>();
            Collection<int> entryIndicesToRemove = new Collection<int>();
            Collection<PSSnapInTypeAndFormatErrors> PSSnapinFiles = FormatAndTypeDataHelper.GetFormatAndTypesErrors(
                this,
                _host,
                this.Formats,
                independentErrors,
                entryIndicesToRemove);

            if (entryIndicesToRemove.Count > 0)
            {
                RemoveNeedlessEntries(RunspaceConfigurationCategory.Formats, entryIndicesToRemove);
            }

            this.FormatDBManager.UpdateDataBase(PSSnapinFiles, this.AuthorizationManager, _host, preValidated);
            FormatAndTypeDataHelper.ThrowExceptionOnError(
                "ErrorsUpdatingFormats",
                independentErrors,
                PSSnapinFiles,
                RunspaceConfigurationCategory.Formats);
        }

        /// <summary>
        /// Remove duplicate entries and entries with files that cannot be loaded from the Types and Formats
        /// </summary>
        /// <param name="category"></param>
        /// <param name="entryIndicesToRemove"></param>
        private void RemoveNeedlessEntries(RunspaceConfigurationCategory category, IList<int> entryIndicesToRemove)
        {
            for (int i = entryIndicesToRemove.Count - 1; i >= 0; i--)
            {
                if (category == RunspaceConfigurationCategory.Types)
                {
                    this.Types.RemoveItem(entryIndicesToRemove[i]);
                }
                else if (category == RunspaceConfigurationCategory.Formats)
                {
                    this.Formats.RemoveItem(entryIndicesToRemove[i]);
                }
            }
        }

        #endregion

        [TraceSource("RunspaceInit", "Initialization code for Runspace")]
        private static PSTraceSource s_runspaceInitTracer = PSTraceSource.GetTracer("RunspaceInit", "Initialization code for Runspace", false);
    }

    /// <summary>
    /// Enum for describing different kind information that can be configured in runspace configuration.
    /// </summary>
    ///
    /// <!--
    /// This is used in RunspaceConfiguration.Update()
    /// -->
    internal enum RunspaceConfigurationCategory
    {
        /// <summary>
        /// Cmdlets
        /// </summary>
        Cmdlets,

        /// <summary>
        /// Providers
        /// </summary>
        Providers,

        /// <summary>
        /// Assemblies
        /// </summary>
        Assemblies,

        /// <summary>
        /// Scripts
        /// </summary>
        Scripts,

        /// <summary>
        /// Initialization scripts
        /// </summary>
        InitializationScripts,

        /// <summary>
        /// Types
        /// </summary>
        Types,

        /// <summary>
        /// Formats
        /// </summary>
        Formats,
    }

#if V2

    /// <summary>
    /// Defines enum for runspace constraints
    /// </summary>
    [Flags]
    public enum RunspaceConstraints
    {
        /// <summary>
        /// No constraint.
        /// </summary>
        None,

        /// <summary>
        /// Allow console applications.
        /// </summary>
        AllowConsoleApplication,

        /// <summary>
        /// Allow user interaction.
        /// </summary>
        AllowUserInteraction,

        /// <summary>
        /// For GUI cases.
        /// </summary>
        AllowGUIDesktop
    }

#endif
}
