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

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Help;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Runtime.InteropServices;
#if !UNIX
using Microsoft.Win32;
#endif

namespace Microsoft.PowerShell.Commands
{
    /// <summary>
    /// This class implements get-help command.
    /// </summary>
    [Cmdlet(VerbsCommon.Get, "Help", DefaultParameterSetName = "AllUsersView", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096483")]
    public sealed class GetHelpCommand : PSCmdlet
    {
        /// <summary>
        /// Help Views.
        /// </summary>
        internal enum HelpView
        {
            Default = 0x00, // Default View
            DetailedView = 0x01,
            FullView = 0x02,
            ExamplesView = 0x03
        }

        /// <summary>
        /// Default constructor for the GetHelpCommand class.
        /// </summary>
        public GetHelpCommand()
        {
        }

        #region Cmdlet Parameters

        /// <summary>
        /// Target to search for help.
        /// </summary>
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = true)]
        [ValidateNotNullOrEmpty()]
        public string Name { get; set; } = string.Empty;

        /// <summary>
        /// Path to provider location that user is curious about.
        /// </summary>
        [Parameter]
        public string Path { get; set; }

        /// <summary>
        /// List of help categories to search for help.
        /// </summary>
        [Parameter]
        [ValidateSet(
            "Alias", "Cmdlet", "Provider", "General", "FAQ", "Glossary", "HelpFile", "ScriptCommand", "Function", "Filter", "ExternalScript", "All", "DefaultHelp", "DscResource", "Class", "Configuration",
             IgnoreCase = true)]
        public string[] Category { get; set; }

        private readonly string _provider = string.Empty;

        /// <summary>
        /// Changes the view of HelpObject returned.
        /// </summary>
        /// <remarks>
        /// Currently we support following views:
        ///
        /// 1. Reminder (Default - Experienced User)
        /// 2. Detailed (Beginner - Beginning User)
        /// 3. Full     (All Users)
        /// 4. Examples
        /// 5. Parameters
        ///
        /// Currently we support these views only for Cmdlets.
        /// A SnapIn developer can however change these views.
        /// </remarks>
        [Parameter(ParameterSetName = "DetailedView", Mandatory = true)]
        public SwitchParameter Detailed
        {
            set
            {
                if (value.ToBool())
                {
                    _viewTokenToAdd = HelpView.DetailedView;
                }
            }
        }

        /// <summary>
        /// Changes the view of HelpObject returned.
        /// </summary>
        /// <remarks>
        /// Currently we support following views:
        ///
        /// 1. Reminder (Default - Experienced User)
        /// 2. Detailed (Beginner - Beginning User)
        /// 3. Full     (All Users)
        /// 4. Examples
        /// 5. Parameters
        ///
        /// Currently we support these views only for Cmdlets.
        /// A SnapIn developer can however change these views.
        /// </remarks>
        [Parameter(ParameterSetName = "AllUsersView")]
        public SwitchParameter Full
        {
            set
            {
                if (value.ToBool())
                {
                    _viewTokenToAdd = HelpView.FullView;
                }
            }
        }

        /// <summary>
        /// Changes the view of HelpObject returned.
        /// </summary>
        /// <remarks>
        /// Currently we support following views:
        ///
        /// 1. Reminder (Default - Experienced User)
        /// 2. Detailed (Beginner - Beginning User)
        /// 3. Full     (All Users)
        /// 4. Examples
        ///
        /// Currently we support these views only for Cmdlets.
        /// A SnapIn developer can however change these views.
        /// </remarks>
        [Parameter(ParameterSetName = "Examples", Mandatory = true)]
        public SwitchParameter Examples
        {
            set
            {
                if (value.ToBool())
                {
                    _viewTokenToAdd = HelpView.ExamplesView;
                }
            }
        }

        /// <summary>
        /// Parameter name.
        /// </summary>
        /// <remarks>
        /// Support WildCard strings as supported by WildcardPattern class.
        /// </remarks>
        [Parameter(ParameterSetName = "Parameters", Mandatory = true)]
        public string[] Parameter { get; set; }

        /// <summary>
        /// Gets and sets list of Component's to search on.
        /// </summary>
        [Parameter]
        public string[] Component { get; set; }

        /// <summary>
        /// Gets and sets list of Functionality's to search on.
        /// </summary>
        [Parameter]
        public string[] Functionality { get; set; }

        /// <summary>
        /// Gets and sets list of Role's to search on.
        /// </summary>
        [Parameter]
        public string[] Role { get; set; }

        /// <summary>
        /// This parameter,if true, will direct get-help cmdlet to
        /// navigate to a URL (stored in the command MAML file under
        /// the uri node).
        /// </summary>
        [Parameter(ParameterSetName = "Online", Mandatory = true)]
        public SwitchParameter Online
        {
            get
            {
                return _showOnlineHelp;
            }

            set
            {
                _showOnlineHelp = value;
                if (_showOnlineHelp)
                {
                    VerifyParameterForbiddenInRemoteRunspace(this, "Online");
                }
            }
        }

        private bool _showOnlineHelp;

#if !UNIX
        private GraphicalHostReflectionWrapper graphicalHostReflectionWrapper;
        private bool showWindow;

        /// <summary>
        /// Gets or sets a value indicating whether the help should be displayed in a separate window.
        /// </summary>
        [Parameter(ParameterSetName = "ShowWindow", Mandatory = true)]
        public SwitchParameter ShowWindow
        {
            get
            {
                return showWindow;
            }

            set
            {
                showWindow = value;
                if (showWindow)
                {
                    VerifyParameterForbiddenInRemoteRunspace(this, "ShowWindow");
                }
            }
        }
#endif

        // The following variable controls the view.
        private HelpView _viewTokenToAdd = HelpView.Default;

#if LEGACYTELEMETRY
        private readonly Stopwatch _timer = new Stopwatch();
        private bool _updatedHelp;
#endif

        #endregion

        #region Cmdlet API implementation

        /// <summary>
        /// Implements the BeginProcessing() method for get-help command.
        /// </summary>
        protected override void BeginProcessing()
        {
#if LEGACYTELEMETRY
            _timer.Start();
#endif
        }

        /// <summary>
        /// Implements the ProcessRecord() method for get-help command.
        /// </summary>
        protected override void ProcessRecord()
        {
#if !UNIX
            string fileSystemPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(this.Name);
            string normalizedName = FileSystemProvider.NormalizePath(fileSystemPath);
            // In a restricted session, do not allow help on network paths or device paths, because device paths can be used to bypass the restrictions.
            if (Utils.IsSessionRestricted(this.Context) && (FileSystemProvider.PathIsNetworkPath(normalizedName) || Utils.PathIsDevicePath(normalizedName))) {
                Exception e = new ArgumentException(HelpErrors.NoNetworkCommands, "Name");
                ErrorRecord errorRecord = new ErrorRecord(e, "CommandNameNotAllowed", ErrorCategory.InvalidArgument, null);
                this.ThrowTerminatingError(errorRecord);
            }
#endif

            HelpSystem helpSystem = this.Context.HelpSystem;
            try
            {
#if !UNIX
                if (this.ShowWindow)
                {
                    this.graphicalHostReflectionWrapper = GraphicalHostReflectionWrapper.GetGraphicalHostReflectionWrapper(this, "Microsoft.PowerShell.Commands.Internal.HelpWindowHelper");
                }
#endif
                helpSystem.OnProgress += HelpSystem_OnProgress;

                bool failed = false;
                HelpCategory helpCategory = ToHelpCategory(Category, ref failed);

                if (failed)
                    return;

                // Validate input parameters
                ValidateAndThrowIfError(helpCategory);

                HelpRequest helpRequest = new HelpRequest(this.Name, helpCategory);

                helpRequest.Provider = _provider;
                helpRequest.Component = Component;
                helpRequest.Role = Role;
                helpRequest.Functionality = Functionality;
                helpRequest.ProviderContext = new ProviderContext(
                    this.Path,
                    this.Context.Engine.Context,
                    this.SessionState.Path);
                helpRequest.CommandOrigin = this.MyInvocation.CommandOrigin;

                // the idea is to use yield statement in the help lookup to speed up
                // perceived user experience....So HelpSystem.GetHelp returns an
                // IEnumerable..
                IEnumerable<HelpInfo> helpInfos = helpSystem.GetHelp(helpRequest);
                // HelpCommand acts differently when there is just one help object and when
                // there are more than one object...so handling this behavior through
                // some variables.
                HelpInfo firstHelpInfoObject = null;
                int countOfHelpInfos = 0;
                foreach (HelpInfo helpInfo in helpInfos)
                {
                    // honor Ctrl-C from user.
                    if (IsStopping)
                    {
                        return;
                    }

                    if (countOfHelpInfos == 0)
                    {
                        firstHelpInfoObject = helpInfo;
                    }
                    else
                    {
                        // write first help object only once.
                        if (firstHelpInfoObject != null)
                        {
                            WriteObjectsOrShowOnlineHelp(firstHelpInfoObject, false);
                            firstHelpInfoObject = null;
                        }

                        WriteObjectsOrShowOnlineHelp(helpInfo, false);
                    }

                    countOfHelpInfos++;
                }

#if LEGACYTELEMETRY
                _timer.Stop();

                if (!string.IsNullOrEmpty(Name))
                    Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.ReportGetHelpTelemetry(Name, countOfHelpInfos, _timer.ElapsedMilliseconds, _updatedHelp);
#endif
                // Write full help as there is only one help info object
                if (countOfHelpInfos == 1)
                {
                    WriteObjectsOrShowOnlineHelp(firstHelpInfoObject, true);
                }
                else if (_showOnlineHelp && (countOfHelpInfos > 1))
                {
                    throw PSTraceSource.NewInvalidOperationException(HelpErrors.MultipleOnlineTopicsNotSupported, "Online");
                }

                // show errors only if there is no wildcard search or VerboseHelpErrors is true.
                if (((countOfHelpInfos == 0) && (!WildcardPattern.ContainsWildcardCharacters(helpRequest.Target)))
                    || helpSystem.VerboseHelpErrors)
                {
                    // Check if there is any error happened. If yes,
                    // pipe out errors.
                    if (helpSystem.LastErrors.Count > 0)
                    {
                        foreach (ErrorRecord errorRecord in helpSystem.LastErrors)
                        {
                            WriteError(errorRecord);
                        }
                    }
                }
            }
            finally
            {
                helpSystem.OnProgress -= HelpSystem_OnProgress;
                HelpSystem_OnComplete();

                // finally clear the ScriptBlockAst -> Token[] cache
                helpSystem.ClearScriptBlockTokenCache();
            }
        }

        private HelpCategory ToHelpCategory(string[] category, ref bool failed)
        {
            if (category == null || category.Length == 0)
                return HelpCategory.None;

            HelpCategory helpCategory = HelpCategory.None;

            failed = false;

            for (int i = 0; i < category.Length; i++)
            {
                try
                {
                    HelpCategory temp = (HelpCategory)Enum.Parse(typeof(HelpCategory), category[i], true);

                    helpCategory |= temp;
                }
                catch (ArgumentException argumentException)
                {
                    Exception e = new HelpCategoryInvalidException(category[i], argumentException);
                    ErrorRecord errorRecord = new ErrorRecord(e, "InvalidHelpCategory", ErrorCategory.InvalidArgument, null);
                    this.WriteError(errorRecord);

                    failed = true;
                }
            }

            return helpCategory;
        }

        /// <summary>
        /// Change <paramref name="originalHelpObject"/> as per user request.
        ///
        /// This method creates a new type to the existing typenames
        /// depending on Detailed,Full,Example parameters and adds this
        /// new type(s) to the top of the list.
        /// </summary>
        /// <param name="originalHelpObject">Full help object to transform.</param>
        /// <returns>Transformed help object with new TypeNames.</returns>
        /// <remarks>If Detailed and Full are not specified, nothing is changed.</remarks>
        private PSObject TransformView(PSObject originalHelpObject)
        {
            Diagnostics.Assert(originalHelpObject != null,
                "HelpObject should not be null");

            if (_viewTokenToAdd == HelpView.Default)
            {
                s_tracer.WriteLine("Detailed, Full, Examples are not selected. Constructing default view.");
                return originalHelpObject;
            }

            string tokenToAdd = _viewTokenToAdd.ToString();
            // We are changing the types without modifying the original object.
            // The contract between help command and helpsystem does not
            // allow us to modify returned help objects.
            PSObject objectToReturn = originalHelpObject.Copy();
            objectToReturn.TypeNames.Clear();

            if (originalHelpObject.TypeNames.Count == 0)
            {
                string typeToAdd = string.Create(CultureInfo.InvariantCulture, $"HelpInfo#{tokenToAdd}");
                objectToReturn.TypeNames.Add(typeToAdd);
            }
            else
            {
                // User request at the top..
                foreach (string typeName in originalHelpObject.TypeNames)
                {
                    // dont add new types for System.String and System.Object..
                    // as they are handled differently for F&0..(bug935095)
                    if (typeName.ToLowerInvariant().Equals("system.string") ||
                        typeName.ToLowerInvariant().Equals("system.object"))
                    {
                        continue;
                    }

                    string typeToAdd = string.Create(CultureInfo.InvariantCulture, $"{typeName}#{tokenToAdd}");
                    s_tracer.WriteLine("Adding type {0}", typeToAdd);
                    objectToReturn.TypeNames.Add(typeToAdd);
                }

                // Existing typenames at the bottom..
                foreach (string typeName in originalHelpObject.TypeNames)
                {
                    s_tracer.WriteLine("Adding type {0}", typeName);
                    objectToReturn.TypeNames.Add(typeName);
                }
            }

            return objectToReturn;
        }

        /// <summary>
        /// Gets the parameter info for patterns identified by Parameter property.
        /// </summary>
        /// <param name="helpInfo">HelpInfo object to look for the parameter.</param>
        /// <returns>Array of parameter infos.</returns>
        private PSObject[] GetParameterInfo(HelpInfo helpInfo)
        {
            List<PSObject> parameterInfosList = new List<PSObject>(Parameter.Length);

            foreach (var parameter in Parameter)
            {
                foreach (var parameterInfo in helpInfo.GetParameter(parameter))
                {
                    parameterInfosList.Add(parameterInfo);
                }
            }

            return parameterInfosList.ToArray();
        }

        /// <summary>
        /// Gets the parameter info for patterns identified by Parameter property.
        /// Writes the parameter info(s) to the output stream. An error is thrown
        /// if a parameter with a given pattern is not found.
        /// </summary>
        /// <param name="helpInfo">HelpInfo Object to look for the parameter.</param>
        private void GetAndWriteParameterInfo(HelpInfo helpInfo)
        {
            s_tracer.WriteLine("Searching parameters for {0}", helpInfo.Name);

            PSObject[] pInfos = GetParameterInfo(helpInfo);

            if ((pInfos == null) || (pInfos.Length == 0))
            {
                Exception innerException = PSTraceSource.NewArgumentException("Parameter",
                    HelpErrors.NoParmsFound, Parameter);
                WriteError(new ErrorRecord(innerException, "NoParmsFound", ErrorCategory.InvalidArgument, helpInfo));
            }
            else
            {
                foreach (PSObject pInfo in pInfos)
                {
                    WriteObject(pInfo);
                }
            }
        }

        /// <summary>
        /// Validates input parameters. 
        /// </summary>
        /// <param name="cat">Category specified by the user.</param>
        /// <exception cref="ArgumentException">
        /// If the request can't be serviced.
        /// </exception>
        private void ValidateAndThrowIfError(HelpCategory cat)
        {
            if (cat == HelpCategory.None)
            {
                return;
            }

            // categories that support -Parameter, -Role, -Functionality, -Component parameters
            const HelpCategory supportedCategories =
                HelpCategory.Alias | HelpCategory.Cmdlet | HelpCategory.ExternalScript |
                HelpCategory.Filter | HelpCategory.Function | HelpCategory.ScriptCommand;

            if ((cat & supportedCategories) == 0)
            {
                if (Parameter != null)
                {
                    throw PSTraceSource.NewArgumentException("Parameter",
                        HelpErrors.ParamNotSupported, "-Parameter");
                }

                if (Component != null)
                {
                    throw PSTraceSource.NewArgumentException("Component",
                        HelpErrors.ParamNotSupported, "-Component");
                }

                if (Role != null)
                {
                    throw PSTraceSource.NewArgumentException("Role",
                        HelpErrors.ParamNotSupported, "-Role");
                }

                if (Functionality != null)
                {
                    throw PSTraceSource.NewArgumentException("Functionality",
                        HelpErrors.ParamNotSupported, "-Functionality");
                }
            }
        }

        /// <summary>
        /// Helper method used to Write the help object onto the output
        /// stream or show online help (URI extracted from the HelpInfo)
        /// object.
        /// </summary>
        private void WriteObjectsOrShowOnlineHelp(HelpInfo helpInfo, bool showFullHelp)
        {
            if (helpInfo != null)
            {
                // online help can be showed only if showFullHelp is true..
                // showFullHelp will be false when the help tries to display multiple help topics..
                // -Online should not work when multiple help topics are displayed.
                if (showFullHelp && _showOnlineHelp)
                {
                    bool onlineUriFound = false;
                    // show online help
                    s_tracer.WriteLine("Preparing to show help online.");
                    Uri onlineUri = helpInfo.GetUriForOnlineHelp();
                    if (onlineUri != null)
                    {
                        onlineUriFound = true;
                        LaunchOnlineHelp(onlineUri);
                        return;
                    }

                    if (!onlineUriFound)
                    {
                        throw PSTraceSource.NewInvalidOperationException(HelpErrors.NoURIFound);
                    }
                }
#if !UNIX
                else if (showFullHelp && ShowWindow)
                {
                    graphicalHostReflectionWrapper.CallStaticMethod("ShowHelpWindow", helpInfo.FullHelp, this);
                }
#endif
                else
                {
                    // show inline help
                    if (showFullHelp)
                    {
                        if (Parameter != null)
                        {
                            GetAndWriteParameterInfo(helpInfo);
                        }
                        else
                        {
                            PSObject objectToReturn = TransformView(helpInfo.FullHelp);
                            objectToReturn.IsHelpObject = true;
                            WriteObject(objectToReturn);
                        }
                    }
                    else
                    {
                        if (Parameter != null)
                        {
                            PSObject[] pInfos = GetParameterInfo(helpInfo);

                            if ((pInfos == null) || (pInfos.Length == 0))
                            {
                                return;
                            }
                        }

                        WriteObject(helpInfo.ShortHelp);
                    }
                }
            }
        }

        /// <summary>
        /// Opens the Uri. System's default application will be used
        /// to show the uri.
        /// </summary>
        /// <param name="uriToLaunch"></param>
        private void LaunchOnlineHelp(Uri uriToLaunch)
        {
            Diagnostics.Assert(uriToLaunch != null, "uriToLaunch should not be null");

            if (!uriToLaunch.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) &&
                !uriToLaunch.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
            {
                throw PSTraceSource.NewInvalidOperationException(HelpErrors.ProtocolNotSupported,
                    uriToLaunch.ToString(),
                    "http",
                    "https");
            }

            // we use this test hook is to avoid actually calling out to another process
            if (InternalTestHooks.BypassOnlineHelpRetrieval)
            {
                this.WriteObject(string.Format(CultureInfo.InvariantCulture, HelpDisplayStrings.OnlineHelpUri, uriToLaunch.OriginalString));
                return;
            }

            Exception exception = null;
            bool wrapCaughtException = true;
            try
            {
                this.WriteVerbose(string.Format(CultureInfo.InvariantCulture, HelpDisplayStrings.OnlineHelpUri, uriToLaunch.OriginalString));
                System.Diagnostics.Process browserProcess = new System.Diagnostics.Process();

                if (Platform.IsNanoServer || Platform.IsIoT)
                {
                    // We cannot open the URL in browser on headless SKUs.
                    wrapCaughtException = false;
                    exception = PSTraceSource.NewInvalidOperationException(HelpErrors.CannotLaunchURI, uriToLaunch.OriginalString);
                }
                else
                {
                    browserProcess.StartInfo.FileName = uriToLaunch.OriginalString;
                    browserProcess.StartInfo.UseShellExecute = true;
                    browserProcess.Start();
                }
            }
            catch (InvalidOperationException ioe)
            {
                exception = ioe;
            }
            catch (System.ComponentModel.Win32Exception we)
            {
                exception = we;
            }

            if (exception != null)
            {
                if (wrapCaughtException)
                    throw PSTraceSource.NewInvalidOperationException(exception, HelpErrors.CannotLaunchURI, uriToLaunch.OriginalString);
                else
                    throw exception;
            }
        }

        #endregion

        private void HelpSystem_OnProgress(object sender, HelpProgressEventArgs arg)
        {
            var record = new ProgressRecord(0, this.CommandInfo.Name, arg.Activity)
            {
                PercentComplete = arg.PercentComplete
            };

            WriteProgress(record);
        }

        private void HelpSystem_OnComplete()
        {
            var record = new ProgressRecord(0, this.CommandInfo.Name, "Completed")
            {
                RecordType = ProgressRecordType.Completed
            };

            WriteProgress(record);
        }

        #region Helper methods for verification of parameters against NoLanguage mode

        internal static void VerifyParameterForbiddenInRemoteRunspace(Cmdlet cmdlet, string parameterName)
        {
            if (NativeCommandProcessor.IsServerSide)
            {
                string message = StringUtil.Format(CommandBaseStrings.ParameterNotValidInRemoteRunspace,
                    cmdlet.MyInvocation.InvocationName,
                    parameterName);
                Exception e = new InvalidOperationException(message);
                ErrorRecord errorRecord = new ErrorRecord(e, "ParameterNotValidInRemoteRunspace", ErrorCategory.InvalidArgument, null);
                cmdlet.ThrowTerminatingError(errorRecord);
            }
        }

        #endregion

        #region trace

        [TraceSourceAttribute("GetHelpCommand ", "GetHelpCommand ")]
        private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("GetHelpCommand ", "GetHelpCommand ");

        #endregion
    }

    /// <summary>
    /// Helper methods used as powershell extension from a types file.
    /// </summary>
    public static class GetHelpCodeMethods
    {
        /// <summary>
        /// Checks whether the default runspace associated with the current thread has the standard Get-Help cmdlet.
        /// </summary>
        /// <returns>True if Get-Help is found, false otherwise.</returns>
        private static bool DoesCurrentRunspaceIncludeCoreHelpCmdlet()
        {
            InitialSessionState iss = Runspace.DefaultRunspace.InitialSessionState;
            if (iss is null)
            {
                return false;
            }

            Collection<SessionStateCommandEntry> getHelpEntries = iss.Commands["Get-Help"];
            SessionStateCommandEntry getHelpEntry = null;
            for (int i = 0; i < getHelpEntries.Count; ++i)
            {
                if (getHelpEntries[i].Visibility is not SessionStateEntryVisibility.Public)
                {
                    continue;
                }

                // If we have multiple entries for Get-Help,
                // our assumption is that the standard Get-Help is not available.
                if (getHelpEntry is not null)
                {
                    return false;
                }

                getHelpEntry = getHelpEntries[i];
            }

            return getHelpEntry is SessionStateCmdletEntry getHelpCmdlet
                && getHelpCmdlet.ImplementingType == typeof(GetHelpCommand);
        }

        /// <summary>
        /// Retrieves the HelpUri given a CommandInfo instance.
        /// </summary>
        /// <param name="commandInfoPSObject">
        /// CommandInfo instance wrapped as PSObject
        /// </param>
        /// <returns>
        /// null if <paramref name="commandInfoPSObject"/> is not a CommandInfo type.
        /// null if HelpUri could not be retrieved either from CommandMetadata or
        /// help content.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")]
        public static string GetHelpUri(PSObject commandInfoPSObject)
        {
            if (commandInfoPSObject == null)
            {
                return string.Empty;
            }

            CommandInfo cmdInfo = PSObject.Base(commandInfoPSObject) as CommandInfo;
            // GetHelpUri helper method is expected to be used only by System.Management.Automation.CommandInfo
            // objects from types.ps1xml
            if ((cmdInfo == null) || (string.IsNullOrEmpty(cmdInfo.Name)))
            {
                return string.Empty;
            }

            // The type checking is needed to avoid a try..catch exception block as
            // the CommandInfo.CommandMetadata throws an InvalidOperationException
            // instead of returning null.
            if ((cmdInfo is CmdletInfo) || (cmdInfo is FunctionInfo) ||
                (cmdInfo is ExternalScriptInfo) || (cmdInfo is ScriptInfo))
            {
                if (!string.IsNullOrEmpty(cmdInfo.CommandMetadata.HelpUri))
                {
                    return cmdInfo.CommandMetadata.HelpUri;
                }
            }

            AliasInfo aliasInfo = cmdInfo as AliasInfo;
            if ((aliasInfo != null) &&
                (aliasInfo.ExternalCommandMetadata != null) &&
                (!string.IsNullOrEmpty(aliasInfo.ExternalCommandMetadata.HelpUri)))
            {
                return aliasInfo.ExternalCommandMetadata.HelpUri;
            }

            // if everything else fails..depend on Get-Help infrastructure to get us the Uri.
            string cmdName = cmdInfo.Name;
            if (!string.IsNullOrEmpty(cmdInfo.ModuleName))
            {
                cmdName = string.Create(CultureInfo.InvariantCulture, $"{cmdInfo.ModuleName}\\{cmdInfo.Name}");
            }

            if (DoesCurrentRunspaceIncludeCoreHelpCmdlet())
            {
                // Win8: 651300 if core get-help is present in the runspace (and it is the only get-help command), use
                // help system directly and avoid perf penalty.
                var currentContext = System.Management.Automation.Runspaces.LocalPipeline.GetExecutionContextFromTLS();
                if ((currentContext != null) && (currentContext.HelpSystem != null))
                {
                    HelpRequest helpRequest = new HelpRequest(cmdName, cmdInfo.HelpCategory);
                    helpRequest.ProviderContext = new ProviderContext(
                        string.Empty,
                        currentContext,
                        currentContext.SessionState.Path);
                    helpRequest.CommandOrigin = CommandOrigin.Runspace;
                    foreach (
                        Uri result in
                            currentContext.HelpSystem.ExactMatchHelp(helpRequest).Select(
                                helpInfo => helpInfo.GetUriForOnlineHelp()).Where(static result => result != null))
                    {
                        return result.OriginalString;
                    }
                }
            }
            else
            {
                // win8: 546025. Using Get-Help as command, instead of calling HelpSystem.ExactMatchHelp
                // for the following reasons:
                // 1. Exchange creates proxies for Get-Command and Get-Help in their scenario
                // 2. This method is primarily used to get uri faster while serializing the CommandInfo objects (from Get-Command)
                // 3. Exchange uses Get-Help proxy to not call Get-Help cmdlet at-all while serializing CommandInfo objects
                // 4. Using HelpSystem directly will not allow Get-Help proxy to do its job.
                var getHelpPS = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)
                    .AddCommand("get-help")
                    .AddParameter("Name", cmdName)
                    .AddParameter("Category", cmdInfo.HelpCategory.ToString());
                try
                {
                    Collection<PSObject> helpInfos = getHelpPS.Invoke();

                    if (helpInfos != null)
                    {
                        for (int index = 0; index < helpInfos.Count; index++)
                        {
                            HelpInfo helpInfo;
                            if (LanguagePrimitives.TryConvertTo<HelpInfo>(helpInfos[index], out helpInfo))
                            {
                                Uri result = helpInfo.GetUriForOnlineHelp();
                                if (result != null)
                                {
                                    return result.OriginalString;
                                }
                            }
                            else
                            {
                                Uri result = BaseCommandHelpInfo.GetUriFromCommandPSObject(helpInfos[index]);
                                return (result != null) ? result.OriginalString : string.Empty;
                            }
                        }
                    }
                }
                finally
                {
                    getHelpPS.Dispose();
                }
            }

            return string.Empty;
        }
    }
}
