// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Security;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Security;
using System.Text;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using PathType = System.IO.Path;

namespace Microsoft.PowerShell.Commands
{
    /// <summary>
    /// Languages supported for code generation.
    /// </summary>
    public enum Language
    {
        /// <summary>
        /// The C# programming language.
        /// </summary>
        CSharp
    }

    /// <summary>
    /// Types supported for the OutputAssembly parameter.
    /// </summary>
    public enum OutputAssemblyType
    {
        /// <summary>
        /// A Dynamically linked library (DLL).
        /// </summary>
        Library,

        /// <summary>
        /// An executable application that targets the console subsystem.
        /// </summary>
        ConsoleApplication,

        /// <summary>
        /// An executable application that targets the graphical subsystem.
        /// </summary>
        WindowsApplication
    }

    /// <summary>
    /// Adds a new type to the Application Domain.
    /// This version is based on CodeAnalysis (Roslyn).
    /// </summary>
    [Cmdlet(VerbsCommon.Add, "Type", DefaultParameterSetName = FromSourceParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096601")]
    [OutputType(typeof(Type))]
    public sealed class AddTypeCommand : PSCmdlet
    {
        #region Parameters

        /// <summary>
        /// The source code of this generated type.
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromSourceParameterSetName)]
        [ValidateTrustedData]
        public string TypeDefinition
        {
            get
            {
                return _sourceCode;
            }

            set
            {
                _sourceCode = value;
            }
        }

        /// <summary>
        /// The name of the type (class) used for auto-generated types.
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromMemberParameterSetName)]
        [ValidateTrustedData]
        public string Name { get; set; }

        /// <summary>
        /// The source code of this generated method / member.
        /// </summary>
        [Parameter(Mandatory = true, Position = 1, ParameterSetName = FromMemberParameterSetName)]
        public string[] MemberDefinition
        {
            get
            {
                return new string[] { _sourceCode };
            }

            set
            {
                _sourceCode = string.Empty;

                if (value != null)
                {
                    _sourceCode = string.Join('\n', value);
                }
            }
        }

        private string _sourceCode;

        /// <summary>
        /// The namespace used for the auto-generated type.
        /// </summary>
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [AllowNull]
        [Alias("NS")]
        public string Namespace { get; set; } = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes";

        /// <summary>
        /// Any using statements required by the auto-generated type.
        /// </summary>
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [ValidateNotNull]
        [Alias("Using")]
        public string[] UsingNamespace { get; set; } = Array.Empty<string>();

        /// <summary>
        /// The path to the source code or DLL to load.
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromPathParameterSetName)]
        [ValidateTrustedData]
        public string[] Path
        {
            get
            {
                return _paths;
            }

            set
            {
                if (value == null)
                {
                    _paths = null;
                    return;
                }

                string[] pathValue = value;

                List<string> resolvedPaths = new();

                // Verify that the paths are resolved and valid
                foreach (string path in pathValue)
                {
                    // Try to resolve the path
                    Collection<string> newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(path, out ProviderInfo _);

                    // If it didn't resolve, add the original back
                    // for a better error message.
                    if (newPaths.Count == 0)
                    {
                        resolvedPaths.Add(path);
                    }
                    else
                    {
                        resolvedPaths.AddRange(newPaths);
                    }
                }

                ProcessPaths(resolvedPaths);
            }
        }

        /// <summary>
        /// The literal path to the source code or DLL to load.
        /// </summary>
        [Parameter(Mandatory = true, ParameterSetName = FromLiteralPathParameterSetName)]
        [Alias("PSPath", "LP")]
        [ValidateTrustedData]
        public string[] LiteralPath
        {
            get
            {
                return _paths;
            }

            set
            {
                if (value == null)
                {
                    _paths = null;
                    return;
                }

                List<string> resolvedPaths = new();
                foreach (string path in value)
                {
                    string literalPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(path);
                    resolvedPaths.Add(literalPath);
                }

                ProcessPaths(resolvedPaths);
            }
        }

        private void ProcessPaths(List<string> resolvedPaths)
        {
            // Validate file extensions.
            // Make sure we don't mix source files from different languages (if we support any other languages in future).
            string activeExtension = null;
            foreach (string path in resolvedPaths)
            {
                string currentExtension = PathType.GetExtension(path).ToUpperInvariant();

                switch (currentExtension)
                {
                    case ".CS":
                        Language = Language.CSharp;
                        break;

                    case ".DLL":
                        _loadAssembly = true;
                        break;

                    // Throw an error if it is an unrecognized extension
                    default:
                        ErrorRecord errorRecord = new(
                            new Exception(
                                StringUtil.Format(AddTypeStrings.FileExtensionNotSupported, currentExtension)),
                            "EXTENSION_NOT_SUPPORTED",
                            ErrorCategory.InvalidArgument,
                            currentExtension);

                        ThrowTerminatingError(errorRecord);
                        break;
                }

                if (activeExtension == null)
                {
                    activeExtension = currentExtension;
                }
                else if (!string.Equals(activeExtension, currentExtension, StringComparison.OrdinalIgnoreCase))
                {
                    // All files must have the same extension otherwise throw.
                    ErrorRecord errorRecord = new(
                        new Exception(
                            StringUtil.Format(AddTypeStrings.MultipleExtensionsNotSupported)),
                        "MULTIPLE_EXTENSION_NOT_SUPPORTED",
                        ErrorCategory.InvalidArgument,
                        currentExtension);

                    ThrowTerminatingError(errorRecord);
                }
            }

            _paths = resolvedPaths.ToArray();
        }

        private string[] _paths;

        /// <summary>
        /// The name of the assembly to load.
        /// </summary>
        [Parameter(Mandatory = true, ParameterSetName = FromAssemblyNameParameterSetName)]
        [Alias("AN")]
        [ValidateTrustedData]
        public string[] AssemblyName { get; set; }

        private bool _loadAssembly = false;

        /// <summary>
        /// The language used to compile the source code.
        /// Default is C#.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        public Language Language { get; set; } = Language.CSharp;

        /// <summary>
        /// Any reference DLLs to use in the compilation.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [Parameter(ParameterSetName = FromPathParameterSetName)]
        [Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
        [Alias("RA")]
        public string[] ReferencedAssemblies
        {
            get
            {
                return _referencedAssemblies;
            }

            set
            {
                if (value != null)
                {
                    _referencedAssemblies = value;
                }
            }
        }

        private string[] _referencedAssemblies = Array.Empty<string>();

        /// <summary>
        /// The path to the output assembly.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [Parameter(ParameterSetName = FromPathParameterSetName)]
        [Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
        [Alias("OA")]
        public string OutputAssembly
        {
            get
            {
                return _outputAssembly;
            }

            set
            {
                _outputAssembly = value;

                if (_outputAssembly != null)
                {
                    _outputAssembly = _outputAssembly.Trim();

                    // Try to resolve the path
                    ProviderInfo provider = null;
                    Collection<string> newPaths = new();

                    try
                    {
                        newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(_outputAssembly, out provider);
                    }
                    // Ignore the ItemNotFound -- we handle it.
                    catch (ItemNotFoundException) { }

                    ErrorRecord errorRecord = new(
                        new Exception(
                            StringUtil.Format(AddTypeStrings.OutputAssemblyDidNotResolve, _outputAssembly)),
                        "INVALID_OUTPUT_ASSEMBLY",
                        ErrorCategory.InvalidArgument,
                        _outputAssembly);

                    // If it resolved to a non-standard provider,
                    // generate an error.
                    if (!string.Equals("FileSystem", provider.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        ThrowTerminatingError(errorRecord);
                        return;
                    }

                    // If it resolved to more than one path,
                    // generate an error.
                    if (newPaths.Count > 1)
                    {
                        ThrowTerminatingError(errorRecord);
                        return;
                    }
                    // It didn't resolve to any files. They may
                    // want to create the file.
                    else if (newPaths.Count == 0)
                    {
                        // We can't create one with wildcard characters
                        if (WildcardPattern.ContainsWildcardCharacters(_outputAssembly))
                        {
                            ThrowTerminatingError(errorRecord);
                        }
                        // Create the file
                        else
                        {
                            _outputAssembly = SessionState.Path.GetUnresolvedProviderPathFromPSPath(_outputAssembly);
                        }
                    }
                    // It resolved to a single file
                    else
                    {
                        _outputAssembly = newPaths[0];
                    }
                }
            }
        }

        private string _outputAssembly = null;

        /// <summary>
        /// The output type of the assembly.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [Parameter(ParameterSetName = FromPathParameterSetName)]
        [Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
        [Alias("OT")]
        public OutputAssemblyType OutputType { get; set; } = OutputAssemblyType.Library;

        /// <summary>
        /// Flag to pass the resulting types along.
        /// </summary>
        [Parameter]
        public SwitchParameter PassThru { get; set; }

        /// <summary>
        /// Flag to ignore warnings during compilation.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [Parameter(ParameterSetName = FromPathParameterSetName)]
        [Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
        public SwitchParameter IgnoreWarnings { get; set; }

        /// <summary>
        /// Roslyn command line parameters.
        /// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/CommandLine.md
        ///
        /// Parser options:
        ///     langversion:string - language version from:
        ///                                 [enum]::GetNames([Microsoft.CodeAnalysis.CSharp.LanguageVersion])
        ///     define:symbol list - preprocessor symbols:
        ///                                 /define:UNIX,DEBUG      - CSharp
        ///
        /// Compilation options:
        ///     optimize{+|-}            - optimization level
        ///     parallel{+|-}            - concurrent build
        ///     warnaserror{+|-}         - report warnings to errors
        ///     warnaserror{+|-}:strings - report specific warnings to errors
        ///     warn:number              - warning level (0-4) for CSharp
        ///     nowarn                   - disable all warnings
        ///     nowarn:strings           - disable a list of individual warnings
        ///     usings:strings           - ';'-delimited usings for CSharp
        ///
        /// Emit options:
        ///     platform:string          - limit which platforms this code can run on; must be x86, x64, Itanium, arm, AnyCPU32BitPreferred or anycpu (default)
        ///     delaysign{+|-}           - delay-sign the assembly using only the public portion of the strong name key
        ///     keyfile:file             - specifies a strong name key file
        ///     keycontainer:string      - specifies a strong name key container
        ///     highentropyva{+|-}       - enable high-entropy ASLR.
        /// </summary>
        [Parameter(ParameterSetName = FromSourceParameterSetName)]
        [Parameter(ParameterSetName = FromMemberParameterSetName)]
        [Parameter(ParameterSetName = FromPathParameterSetName)]
        [Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
        [ValidateNotNullOrEmpty]
        public string[] CompilerOptions { get; set; }

        #endregion Parameters

        #region GererateSource

        private string GenerateTypeSource(string typeNamespace, string typeName, string sourceCodeText, Language language)
        {
            string usingSource = string.Format(
                    CultureInfo.InvariantCulture,
                    GetUsingTemplate(language), GetUsingSet(language));

            string typeSource = string.Format(
                    CultureInfo.InvariantCulture,
                    GetMethodTemplate(language), typeName, sourceCodeText);

            if (!string.IsNullOrEmpty(typeNamespace))
            {
                return usingSource + string.Format(
                    CultureInfo.InvariantCulture,
                    GetNamespaceTemplate(language), typeNamespace, typeSource);
            }
            else
            {
                return usingSource + typeSource;
            }
        }

        // Get the -FromMember template for a given language
        private static string GetMethodTemplate(Language language)
        {
            switch (language)
            {
                case Language.CSharp:
                    return
                        "    public class {0}\n" +
                        "    {{\n" +
                        "    {1}\n" +
                        "    }}\n";
            }

            throw PSTraceSource.NewNotSupportedException();
        }

        // Get the -FromMember namespace template for a given language
        private static string GetNamespaceTemplate(Language language)
        {
            switch (language)
            {
                case Language.CSharp:
                    return
                        "namespace {0}\n" +
                        "{{\n" +
                        "{1}\n" +
                        "}}\n";
            }

            throw PSTraceSource.NewNotSupportedException();
        }

        // Get the -FromMember namespace template for a given language
        private static string GetUsingTemplate(Language language)
        {
            switch (language)
            {
                case Language.CSharp:
                    return
                        "using System;\n" +
                        "using System.Runtime.InteropServices;\n" +
                        "{0}" +
                        "\n";
            }

            throw PSTraceSource.NewNotSupportedException();
        }

        // Generate the code for the using statements
        private string GetUsingSet(Language language)
        {
            StringBuilder usingNamespaceSet = new();

            switch (language)
            {
                case Language.CSharp:
                    foreach (string namespaceValue in UsingNamespace)
                    {
                        usingNamespaceSet.Append("using " + namespaceValue + ";\n");
                    }

                    break;

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }

            return usingNamespaceSet.ToString();
        }

        #endregion GererateSource

        /// <summary>
        /// Prevent code compilation in ConstrainedLanguage mode.
        /// </summary>
        protected override void BeginProcessing()
        {
            // Prevent code compilation in ConstrainedLanguage mode, or NoLanguage mode under system lock down.
            if (SessionState.LanguageMode == PSLanguageMode.ConstrainedLanguage ||
                (SessionState.LanguageMode == PSLanguageMode.NoLanguage && SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce))
            {
                if (SystemPolicy.GetSystemLockdownPolicy() != SystemEnforcementMode.Audit)
                {
                    ThrowTerminatingError(
                        new ErrorRecord(
                            new PSNotSupportedException(AddTypeStrings.CannotDefineNewType),
                            nameof(AddTypeStrings.CannotDefineNewType),
                            ErrorCategory.PermissionDenied,
                            targetObject: null));
                }

                SystemPolicy.LogWDACAuditMessage(
                    context: Context,
                    title: AddTypeStrings.AddTypeLogTitle,
                    message: AddTypeStrings.AddTypeLogMessage,
                    fqid: "AddTypeCmdletDisabled",
                    dropIntoDebugger: true);
            }

            // 'ConsoleApplication' and 'WindowsApplication' types are currently not working in .NET Core
            if (OutputType != OutputAssemblyType.Library)
            {
                ThrowTerminatingError(
                    new ErrorRecord(
                        new PSNotSupportedException(AddTypeStrings.AssemblyTypeNotSupported),
                        nameof(AddTypeStrings.AssemblyTypeNotSupported),
                        ErrorCategory.NotImplemented,
                        targetObject: OutputType));
            }
        }

        /// <summary>
        /// Generate and load the type(s).
        /// </summary>
        protected override void EndProcessing()
        {
            // Generate an error if they've specified an output
            // assembly type without an output assembly
            if (string.IsNullOrEmpty(_outputAssembly) && this.MyInvocation.BoundParameters.ContainsKey(nameof(OutputType)))
            {
                ErrorRecord errorRecord = new(
                    new Exception(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            AddTypeStrings.OutputTypeRequiresOutputAssembly)),
                    "OUTPUTTYPE_REQUIRES_ASSEMBLY",
                    ErrorCategory.InvalidArgument,
                    OutputType);

                ThrowTerminatingError(errorRecord);
                return;
            }

            if (_loadAssembly)
            {
                // File extension is ".DLL" (ParameterSetName = FromPathParameterSetName or FromLiteralPathParameterSetName).
                LoadAssemblies(_paths);
            }
            else if (ParameterSetName == FromAssemblyNameParameterSetName)
            {
                LoadAssemblies(AssemblyName);
            }
            else
            {
                // Process a source code from files or strings.
                SourceCodeProcessing();
            }
        }

        #region LoadAssembly

        // We now ship .NET Core's reference assemblies with PowerShell, so that Add-Type can work
        // in a predictable way and won't be broken when we move to newer version of .NET Core.
        // The reference assemblies are located at '$PSHOME\ref' for pwsh.
        //
        // For applications that host PowerShell, the 'ref' folder will be deployed to the 'publish'
        // folder, not where 'System.Management.Automation.dll' is located. So here we should use
        // the entry assembly's location to construct the path to the 'ref' folder.
        // For pwsh, the entry assembly is 'pwsh.dll', so the entry assembly's location is still
        // $PSHOME.
        // However, 'Assembly.GetEntryAssembly()' returns null when the managed code is called from
        // unmanaged code (PowerShell WSMan remoting scenario), so in that case, we continue to use
        // the location of 'System.Management.Automation.dll'.
        private static readonly string s_netcoreAppRefFolder = PathType.Combine(
            PathType.GetDirectoryName(
                (Assembly.GetEntryAssembly() ?? typeof(PSObject).Assembly).Location),
            "ref");

        // Path to the folder where .NET Core runtime assemblies are located.
        private static readonly string s_frameworkFolder = PathType.GetDirectoryName(typeof(object).Assembly.Location);

        // These assemblies are always automatically added to ReferencedAssemblies.
        private static readonly Lazy<PortableExecutableReference[]> s_autoReferencedAssemblies = new(InitAutoIncludedRefAssemblies);

        // A HashSet of assembly names to be ignored if they are specified in '-ReferencedAssemblies'
        private static readonly Lazy<HashSet<string>> s_refAssemblyNamesToIgnore = new(InitRefAssemblyNamesToIgnore);

        // These assemblies are used, when ReferencedAssemblies parameter is not specified.
        private static readonly Lazy<IEnumerable<PortableExecutableReference>> s_defaultAssemblies = new(InitDefaultRefAssemblies);

        private bool InMemory { get { return string.IsNullOrEmpty(_outputAssembly); } }

        // These dictionaries prevent reloading already loaded and unchanged code.
        // We don't worry about unbounded growing of the cache because in .Net Core 2.0 we can not unload assemblies.
        // TODO: review if we will be able to unload assemblies after migrating to .Net Core 2.1.
        private static readonly ConcurrentDictionary<string, object> s_sourceTypesCache = new();
        private static readonly ConcurrentDictionary<int, Assembly> s_sourceAssemblyCache = new();

        private static readonly string s_defaultSdkDirectory = Utils.DefaultPowerShellAppBase;

        private const ReportDiagnostic defaultDiagnosticOption = ReportDiagnostic.Error;

        private static readonly string[] s_writeInformationTags = new string[] { "PSHOST" };
        private int _syntaxTreesHash;

        private const string FromMemberParameterSetName = "FromMember";
        private const string FromSourceParameterSetName = "FromSource";
        private const string FromPathParameterSetName = "FromPath";
        private const string FromLiteralPathParameterSetName = "FromLiteralPath";
        private const string FromAssemblyNameParameterSetName = "FromAssemblyName";

        private void LoadAssemblies(IEnumerable<string> assemblies)
        {
            foreach (string assemblyName in assemblies)
            {
                // CoreCLR doesn't allow re-load TPA assemblies with different API (i.e. we load them by name and now want to load by path).
                // LoadAssemblyHelper helps us avoid re-loading them, if they already loaded.
                // codeql[cs/dll-injection-remote] - This is expected PowerShell behavior and integral to the purpose of the class. It allows users to load any C# dependencies they need for their PowerShell application and add other types they require.
                Assembly assembly = LoadAssemblyHelper(assemblyName) ?? Assembly.LoadFrom(ResolveAssemblyName(assemblyName, false));

                if (PassThru)
                {
                    WriteTypes(assembly);
                }
            }
        }

        /// <summary>
        /// Initialize the list of reference assemblies that will be used when '-ReferencedAssemblies' is not specified.
        /// </summary>
        private static IEnumerable<PortableExecutableReference> InitDefaultRefAssemblies()
        {
            // Default reference assemblies consist of .NET reference assemblies and the 'S.M.A' assembly.
            // Today, there are 161 .NET reference assemblies, so the needed capacity is 162, but we use 200
            // as the initial capacity to cover the possible increase of .NET reference assemblies in future.
            var defaultRefAssemblies = new List<PortableExecutableReference>(capacity: 200);

            foreach (string file in Directory.EnumerateFiles(s_netcoreAppRefFolder, "*.dll", SearchOption.TopDirectoryOnly))
            {
                defaultRefAssemblies.Add(MetadataReference.CreateFromFile(file));
            }

            // Add System.Management.Automation.dll
            defaultRefAssemblies.Add(MetadataReference.CreateFromFile(typeof(PSObject).Assembly.Location));

            return defaultRefAssemblies;
        }

        /// <summary>
        /// Initialize the set of assembly names that should be ignored when they are specified in '-ReferencedAssemblies'.
        ///   - System.Private.CoreLib.ni.dll - the runtime dll that contains most core/primitive types
        ///   - System.Private.Uri.dll - the runtime dll that contains 'System.Uri' and related types
        /// Referencing these runtime dlls may cause ambiguous type identity or other issues.
        ///   - System.Runtime.dll - the corresponding reference dll will be automatically included
        ///   - System.Runtime.InteropServices.dll - the corresponding reference dll will be automatically included.
        /// </summary>
        private static HashSet<string> InitRefAssemblyNamesToIgnore()
        {
            return new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
                PathType.GetFileName(typeof(object).Assembly.Location),
                PathType.GetFileName(typeof(Uri).Assembly.Location),
                PathType.GetFileName(GetReferenceAssemblyPathBasedOnType(typeof(object))),
                PathType.GetFileName(GetReferenceAssemblyPathBasedOnType(typeof(SecureString)))
            };
        }

        /// <summary>
        /// Initialize the list of reference assemblies that will be automatically added when '-ReferencedAssemblies' is specified.
        /// </summary>
        private static PortableExecutableReference[] InitAutoIncludedRefAssemblies()
        {
            return new PortableExecutableReference[] {
                MetadataReference.CreateFromFile(GetReferenceAssemblyPathBasedOnType(typeof(object))),
                MetadataReference.CreateFromFile(GetReferenceAssemblyPathBasedOnType(typeof(SecureString)))
            };
        }

        /// <summary>
        /// Get the path of reference assembly where the type is declared.
        /// </summary>
        private static string GetReferenceAssemblyPathBasedOnType(Type type)
        {
            string refAsmFileName = PathType.GetFileName(ClrFacade.GetAssemblies(type.FullName).First().Location);
            return PathType.Combine(s_netcoreAppRefFolder, refAsmFileName);
        }

        private string ResolveAssemblyName(string assembly, bool isForReferenceAssembly)
        {
            ErrorRecord errorRecord;

            // if it's a path, resolve it
            if (assembly.Contains(PathType.DirectorySeparatorChar) || assembly.Contains(PathType.AltDirectorySeparatorChar))
            {
                if (PathType.IsPathRooted(assembly))
                {
                    return assembly;
                }
                else
                {
                    var paths = SessionState.Path.GetResolvedPSPathFromPSPath(assembly);
                    if (paths.Count > 0)
                    {
                        return paths[0].Path;
                    }
                    else
                    {
                        errorRecord = new ErrorRecord(
                            new Exception(
                                string.Format(ParserStrings.ErrorLoadingAssembly, assembly)),
                            "ErrorLoadingAssembly",
                            ErrorCategory.InvalidOperation,
                            assembly);

                        ThrowTerminatingError(errorRecord);
                        return null;
                    }
                }
            }

            string refAssemblyDll = assembly;
            if (!assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
            {
                // It could be a short assembly name or a full assembly name, but we
                // always want the short name to find the corresponding assembly file.
                var assemblyName = new AssemblyName(assembly);
                refAssemblyDll = assemblyName.Name + ".dll";
            }

            // We look up in reference/framework only when it's for resolving reference assemblies.
            // In case of 'Add-Type -AssemblyName' scenario, we don't attempt to resolve against framework assemblies because
            //   1. Explicitly loading a framework assembly usually is not necessary in PowerShell 6+.
            //   2. A user should use assembly name instead of path if they want to explicitly load a framework assembly.
            if (isForReferenceAssembly)
            {
                // If it's for resolving a reference assembly, then we look in NetCoreApp ref assemblies first
                string netcoreAppRefPath = PathType.Combine(s_netcoreAppRefFolder, refAssemblyDll);
                if (File.Exists(netcoreAppRefPath))
                {
                    return netcoreAppRefPath;
                }

                // Look up the assembly in the framework folder. This may happen when assembly is not part of
                // NetCoreApp, but comes from an additional package, such as 'Json.Net'.
                string frameworkPossiblePath = PathType.Combine(s_frameworkFolder, refAssemblyDll);
                if (File.Exists(frameworkPossiblePath))
                {
                    return frameworkPossiblePath;
                }

                // The assembly name may point to a third-party assembly that is already loaded at run time.
                if (!assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                {
                    Assembly result = LoadAssemblyHelper(assembly);
                    if (result != null)
                    {
                        return result.Location;
                    }
                }
            }

            // Look up the assembly in the current folder
            var resolvedPaths = SessionState.Path.GetResolvedPSPathFromPSPath(refAssemblyDll);

            if (resolvedPaths.Count > 0)
            {
                string currentFolderPath = resolvedPaths[0].Path;
                if (File.Exists(currentFolderPath))
                {
                    return currentFolderPath;
                }
            }

            errorRecord = new ErrorRecord(
                new Exception(
                    string.Format(ParserStrings.ErrorLoadingAssembly, assembly)),
                "ErrorLoadingAssembly",
                ErrorCategory.InvalidOperation,
                assembly);

            ThrowTerminatingError(errorRecord);
            return null;
        }

        // LoadWithPartialName is deprecated, so we have to write the closest approximation possible.
        // However, this does give us a massive usability improvement, as users can just say
        // Add-Type -AssemblyName Forms (instead of System.Windows.Forms)
        // This is just long, not unmaintainable.
        private static Assembly LoadAssemblyHelper(string assemblyName)
        {
            Assembly loadedAssembly = null;

            // First try by strong name
            try
            {
                loadedAssembly = Assembly.Load(new AssemblyName(assemblyName));
            }
            // Generates a FileNotFoundException if you can't load the strong type.
            // So we'll try from the short name.
            catch (System.IO.FileNotFoundException) { }
            // File load exception can happen, when we trying to load from the incorrect assembly name
            // or file corrupted.
            catch (System.IO.FileLoadException) { }

            return loadedAssembly;
        }

        private IEnumerable<PortableExecutableReference> GetPortableExecutableReferences()
        {
            if (ReferencedAssemblies.Length > 0)
            {
                var tempReferences = new List<PortableExecutableReference>(s_autoReferencedAssemblies.Value);
                foreach (string assembly in ReferencedAssemblies)
                {
                    if (string.IsNullOrWhiteSpace(assembly))
                    {
                        continue;
                    }

                    string resolvedAssemblyPath = ResolveAssemblyName(assembly, true);

                    // Ignore some specified reference assemblies
                    string fileName = PathType.GetFileName(resolvedAssemblyPath);
                    if (s_refAssemblyNamesToIgnore.Value.Contains(fileName))
                    {
                        WriteVerbose(StringUtil.Format(AddTypeStrings.ReferenceAssemblyIgnored, resolvedAssemblyPath));
                        continue;
                    }

                    tempReferences.Add(MetadataReference.CreateFromFile(resolvedAssemblyPath));
                }

                return tempReferences;
            }
            else
            {
                return s_defaultAssemblies.Value;
            }
        }

        private void WriteTypes(Assembly assembly)
        {
            foreach (Type type in assembly.GetTypes())
            {
                // We only write out types that are not auto-generated by compiler.
                if (type.GetCustomAttribute<CompilerGeneratedAttribute>() is null)
                {
                    WriteObject(type);
                }
            }
        }

        #endregion LoadAssembly

        #region SourceCodeProcessing

        private static OutputKind OutputAssemblyTypeToOutputKind(OutputAssemblyType outputType)
        {
            switch (outputType)
            {
                case OutputAssemblyType.Library:
                    return OutputKind.DynamicallyLinkedLibrary;

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }
        }

        private CommandLineArguments ParseCompilerOption(IEnumerable<string> args)
        {
            string sdkDirectory = s_defaultSdkDirectory;
            string baseDirectory = this.SessionState.Path.CurrentLocation.Path;

            switch (Language)
            {
                case Language.CSharp:
                    return CSharpCommandLineParser.Default.Parse(args, baseDirectory, sdkDirectory);

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }
        }

        private SyntaxTree ParseSourceText(SourceText sourceText, ParseOptions parseOptions, string path = "")
        {
            switch (Language)
            {
                case Language.CSharp:
                    return CSharpSyntaxTree.ParseText(sourceText, (CSharpParseOptions)parseOptions, path);

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }
        }

        private CompilationOptions GetDefaultCompilationOptions()
        {
            switch (Language)
            {
                case Language.CSharp:
                    return new CSharpCompilationOptions(OutputAssemblyTypeToOutputKind(OutputType));

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }
        }

        private bool IsSourceCodeUpdated(List<SyntaxTree> syntaxTrees, out Assembly assembly)
        {
            Diagnostics.Assert(syntaxTrees.Count != 0, "syntaxTrees should contains a source code.");

            _syntaxTreesHash = SyntaxTreeArrayGetHashCode(syntaxTrees);

            if (s_sourceAssemblyCache.TryGetValue(_syntaxTreesHash, out Assembly hashedAssembly))
            {
                assembly = hashedAssembly;
                return false;
            }
            else
            {
                assembly = null;
                return true;
            }
        }

        private void SourceCodeProcessing()
        {
            ParseOptions parseOptions = null;
            CompilationOptions compilationOptions = null;
            EmitOptions emitOptions = null;

            if (CompilerOptions != null)
            {
                var arguments = ParseCompilerOption(CompilerOptions);

                HandleCompilerErrors(arguments.Errors);

                parseOptions = arguments.ParseOptions;
                compilationOptions = arguments.CompilationOptions.WithOutputKind(OutputAssemblyTypeToOutputKind(OutputType));
                emitOptions = arguments.EmitOptions;
            }
            else
            {
                parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest);
                compilationOptions = GetDefaultCompilationOptions();
            }

            if (!IgnoreWarnings.IsPresent)
            {
                compilationOptions = compilationOptions.WithGeneralDiagnosticOption(defaultDiagnosticOption);
            }

            SourceText sourceText;
            List<SyntaxTree> syntaxTrees = new();

            switch (ParameterSetName)
            {
                case FromPathParameterSetName:
                case FromLiteralPathParameterSetName:
                    foreach (string filePath in _paths)
                    {
                        using (var sourceFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                        {
                            sourceText = SourceText.From(sourceFile);
                            syntaxTrees.Add(ParseSourceText(sourceText, parseOptions, path: filePath));
                        }
                    }

                    break;
                case FromMemberParameterSetName:
                    _sourceCode = GenerateTypeSource(Namespace, Name, _sourceCode, Language);

                    sourceText = SourceText.From(_sourceCode);
                    syntaxTrees.Add(ParseSourceText(sourceText, parseOptions));
                    break;
                case FromSourceParameterSetName:
                    sourceText = SourceText.From(_sourceCode);
                    syntaxTrees.Add(ParseSourceText(sourceText, parseOptions));
                    break;
                default:
                    Diagnostics.Assert(false, "Invalid parameter set: {0}", this.ParameterSetName);
                    break;
            }

            if (!string.IsNullOrEmpty(_outputAssembly) && !PassThru.IsPresent)
            {
                CompileToAssembly(syntaxTrees, compilationOptions, emitOptions);
            }
            else
            {
                // if the source code was already compiled and loaded and not changed
                // we get the assembly from the cache.
                if (IsSourceCodeUpdated(syntaxTrees, out Assembly assembly))
                {
                    CompileToAssembly(syntaxTrees, compilationOptions, emitOptions);
                }
                else
                {
                    WriteVerbose(AddTypeStrings.AlreadyCompiledandLoaded);

                    if (PassThru)
                    {
                        WriteTypes(assembly);
                    }
                }
            }
        }

        private void CompileToAssembly(List<SyntaxTree> syntaxTrees, CompilationOptions compilationOptions, EmitOptions emitOptions)
        {
            IEnumerable<PortableExecutableReference> references = GetPortableExecutableReferences();
            Compilation compilation = null;

            switch (Language)
            {
                case Language.CSharp:
                    compilation = CSharpCompilation.Create(
                        PathType.GetRandomFileName(),
                        syntaxTrees: syntaxTrees,
                        references: references,
                        options: (CSharpCompilationOptions)compilationOptions);
                    break;

                default:
                    throw PSTraceSource.NewNotSupportedException();
            }

            DoEmitAndLoadAssembly(compilation, emitOptions);
        }

        private void CheckDuplicateTypes(Compilation compilation, out ConcurrentBag<string> newTypes)
        {
            AllNamedTypeSymbolsVisitor visitor = new();
            visitor.Visit(compilation.Assembly.GlobalNamespace);

            foreach (var symbolName in visitor.DuplicateSymbols)
            {
                ErrorRecord errorRecord = new(
                    new Exception(
                        string.Format(AddTypeStrings.TypeAlreadyExists, symbolName)),
                    "TYPE_ALREADY_EXISTS",
                    ErrorCategory.InvalidOperation,
                    symbolName);
                WriteError(errorRecord);
            }

            if (!visitor.DuplicateSymbols.IsEmpty)
            {
                ErrorRecord errorRecord = new(
                    new InvalidOperationException(AddTypeStrings.CompilerErrors),
                    "COMPILER_ERRORS",
                    ErrorCategory.InvalidData,
                    null);
                ThrowTerminatingError(errorRecord);
            }

            newTypes = visitor.UniqueSymbols;

            return;
        }

        // Visit symbols in all namespaces and collect duplicates.
        private sealed class AllNamedTypeSymbolsVisitor : SymbolVisitor
        {
            public readonly ConcurrentBag<string> DuplicateSymbols = new();
            public readonly ConcurrentBag<string> UniqueSymbols = new();

            public override void VisitNamespace(INamespaceSymbol symbol)
            {
                // Main cycle.
                // For large files we could use symbol.GetMembers().AsParallel().ForAll(s => s.Accept(this));
                foreach (var member in symbol.GetMembers())
                {
                    member.Accept(this);
                }
            }

            public override void VisitNamedType(INamedTypeSymbol symbol)
            {
                // It is namespace-fully-qualified name
                var symbolFullName = symbol.ToString();

                if (s_sourceTypesCache.ContainsKey(symbolFullName))
                {
                    DuplicateSymbols.Add(symbolFullName);
                }
                else
                {
                    UniqueSymbols.Add(symbolFullName);
                }
            }
        }

        private static void CacheNewTypes(ConcurrentBag<string> newTypes)
        {
            foreach (var typeName in newTypes)
            {
                s_sourceTypesCache.TryAdd(typeName, null);
            }
        }

        private void CacheAssembly(Assembly assembly)
        {
            s_sourceAssemblyCache.TryAdd(_syntaxTreesHash, assembly);
        }

        private void DoEmitAndLoadAssembly(Compilation compilation, EmitOptions emitOptions)
        {
            EmitResult emitResult;

            CheckDuplicateTypes(compilation, out ConcurrentBag<string> newTypes);

            if (InMemory)
            {
                using (var ms = new MemoryStream())
                {
                    emitResult = compilation.Emit(peStream: ms, options: emitOptions);

                    HandleCompilerErrors(emitResult.Diagnostics);

                    if (emitResult.Success)
                    {
                        ms.Seek(0, SeekOrigin.Begin);
                        Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms);

                        CacheNewTypes(newTypes);
                        CacheAssembly(assembly);

                        if (PassThru)
                        {
                            WriteTypes(assembly);
                        }
                    }
                }
            }
            else
            {
                using (var fs = new FileStream(_outputAssembly, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
                {
                    emitResult = compilation.Emit(peStream: fs, options: emitOptions);
                }

                HandleCompilerErrors(emitResult.Diagnostics);

                if (emitResult.Success && PassThru)
                {
                    Assembly assembly = Assembly.LoadFrom(_outputAssembly);

                    CacheNewTypes(newTypes);
                    CacheAssembly(assembly);

                    WriteTypes(assembly);
                }
            }
        }

        private void HandleCompilerErrors(ImmutableArray<Diagnostic> compilerDiagnostics)
        {
            if (compilerDiagnostics.Length > 0)
            {
                bool IsError = false;

                foreach (var diagnisticRecord in compilerDiagnostics)
                {
                    // We shouldn't specify input and output files in CompilerOptions parameter
                    // so suppress errors from Roslyn default command line parser:
                    //      CS1562: Outputs without source must have the /out option specified
                    //      CS2008: No inputs specified
                    //      BC2008: No inputs specified
                    //
                    // On emit phase some warnings (like CS8019/BS50001) don't suppressed
                    // and present in diagnostic report with DefaultSeverity equal to Hidden
                    // so we skip them explicitly here too.
                    if (diagnisticRecord.IsSuppressed || diagnisticRecord.DefaultSeverity == DiagnosticSeverity.Hidden ||
                        string.Equals(diagnisticRecord.Id, "CS2008", StringComparison.InvariantCulture) ||
                        string.Equals(diagnisticRecord.Id, "CS1562", StringComparison.InvariantCulture) ||
                        string.Equals(diagnisticRecord.Id, "BC2008", StringComparison.InvariantCulture))
                    {
                        continue;
                    }

                    if (!IsError)
                    {
                        IsError = diagnisticRecord.Severity == DiagnosticSeverity.Error ||
                                 (diagnisticRecord.IsWarningAsError && diagnisticRecord.Severity == DiagnosticSeverity.Warning);
                    }

                    string errorText = BuildErrorMessage(diagnisticRecord);

                    if (diagnisticRecord.Severity == DiagnosticSeverity.Warning)
                    {
                        WriteWarning(errorText);
                    }
                    else if (diagnisticRecord.Severity == DiagnosticSeverity.Info)
                    {
                        WriteInformation(errorText, s_writeInformationTags);
                    }
                    else
                    {
                        ErrorRecord errorRecord = new(
                            new Exception(errorText),
                            "SOURCE_CODE_ERROR",
                            ErrorCategory.InvalidData,
                            diagnisticRecord);

                        WriteError(errorRecord);
                    }
                }

                if (IsError)
                {
                    ErrorRecord errorRecord = new(
                        new InvalidOperationException(AddTypeStrings.CompilerErrors),
                        "COMPILER_ERRORS",
                        ErrorCategory.InvalidData,
                        null);
                    ThrowTerminatingError(errorRecord);
                }
            }
        }

        private static string BuildErrorMessage(Diagnostic diagnisticRecord)
        {
            var location = diagnisticRecord.Location;

            if (location.SourceTree == null)
            {
                // For some error types (linker?) we don't have related source code.
                return diagnisticRecord.ToString();
            }
            else
            {
                var text = location.SourceTree.GetText();
                var textLines = text.Lines;

                var lineSpan = location.GetLineSpan(); // FileLinePositionSpan type.
                var errorLineNumber = lineSpan.StartLinePosition.Line;

                // This is typical Roslyn diagnostic message which contains
                // a message number, a source context and an error position.
                var diagnisticMessage = diagnisticRecord.ToString();
                var errorLineString = textLines[errorLineNumber].ToString();
                var errorPosition = lineSpan.StartLinePosition.Character;

                StringBuilder sb = new(diagnisticMessage.Length + errorLineString.Length * 2 + 4);

                sb.AppendLine(diagnisticMessage);
                sb.AppendLine(errorLineString);

                for (var i = 0; i < errorLineString.Length; i++)
                {
                    if (!char.IsWhiteSpace(errorLineString[i]))
                    {
                        // We copy white chars from the source string.
                        sb.Append(errorLineString, 0, i);
                        // then pad up to the error position.
                        sb.Append(' ', Math.Max(0, errorPosition - i));
                        // then put "^" into the error position.
                        sb.AppendLine("^");
                        break;
                    }
                }

                return sb.ToString();
            }
        }

        private static int SyntaxTreeArrayGetHashCode(IEnumerable<SyntaxTree> sts)
        {
            // We use our extension method EnumerableExtensions.SequenceGetHashCode<T>().
            List<int> stHashes = new();
            foreach (var st in sts)
            {
                stHashes.Add(SyntaxTreeGetHashCode(st));
            }

            return stHashes.SequenceGetHashCode<int>();
        }

        private static int SyntaxTreeGetHashCode(SyntaxTree st)
        {
            int hash;

            if (string.IsNullOrEmpty(st.FilePath))
            {
                // If the file name does not exist, the source text is set by the user using parameters.
                // In this case, we assume that the source text is of a small size and we can re-allocate by ToString().
                hash = st.ToString().GetHashCode();
            }
            else
            {
                // If the file was modified, the write time stamp was also modified
                // so we do not need to calculate the entire file hash.
                var updateTime = File.GetLastWriteTimeUtc(st.FilePath);
                hash = Utils.CombineHashCodes(st.FilePath.GetHashCode(), updateTime.GetHashCode());
            }

            return hash;
        }

        #endregion SourceCodeProcessing
    }
}
