Private/HelpTools.psm1


class HelpTools {
  static [object] GetCimHelp([String]$Class, [String]$Namespace, [bool] $Detailed, [String]$Property, [String]$Method) {
    $Type = ''; $PropertyObject = $null
    $HelpUrl = Get-CimUri -Type $Type -Method $Method -Property $Property
    $CimClass = Get-CimClass $Class -Namespace $Namespace
    $LocalizedClass = Get-WmiClassInfo $Class -Namespace $Namespace

    $HelpObject = New-Object PSObject -Property @{
      Details      = New-Object PSObject -Property @{
        Name        = $CimClass.CimClassName
        Namespace   = $CimClass.CimSystemProperties.Namespace
        SuperClass  = $CimClass.CimSuperClass.ToString()
        Description = @($LocalizedClass.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
            $Paragraph = New-Object PSObject -Property @{Text = $_.Trim() }
            $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
            $Paragraph
          }
        )
      }
      Properties   = @{}
      Methods      = @{}
      RelatedLinks = @(
        New-Object PSObject -Property @{Title = "Online Version"; Link = $HelpUrl }
      )
    }

    $HelpObject.Details.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Details")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim")
    if ($Detailed) {
      $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#DetailedView")
    }

    foreach ($CimProperty in $LocalizedClass.Properties) {
      $PropertyObject = New-Object PSObject -Property @{
        Name         = $CimProperty.Name
        type         = $CimProperty.Type
        Description  = @($CimProperty.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
            $Paragraph = New-Object PSObject -Property @{Text = $_.Trim() }
            $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
            $Paragraph
          })
        RelatedLinks = @(
          New-Object PSObject -Property @{Title = "Online Version"; Link = $HelpUrl }
        )
      }
      $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#Property")
      $HelpObject.Properties.Add($CimProperty.Name, $PropertyObject)
    }

    foreach ($CimMethod in $CimClass.CimClassMethods) {
      $MethodHelp = $LocalizedClass.Methods[$CimMethod.Name]

      $MethodObject = New-Object PSObject -Property @{
        Name         = $CimMethod.Name
        Static       = $CimMethod.Qualifiers["Static"].Value
        Constructor  = $CimMethod.Qualifiers["Constructor"].Value
        Description  = $null
        Parameters   = @{}
        RelatedLinks = @(
          New-Object PSObject -Property @{Title = "Online Version"; Link = $HelpUrl }
        )
      }
      $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#Method")

      $MethodObject.Description = @($MethodHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
          $Paragraph = New-Object PSObject -Property @{Text = $_.Trim() }
          $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
          $Paragraph
        }
      )

      $CimMethod.Parameters | ForEach-Object {
        if ($_.Qualifiers["In"]) {
          $MethodObject.Parameters[$_.Name] = New-Object PSObject -Property @{
            Name        = $_.Name
            Type        = $_.CimType
            ID          = [int]$_.Qualifiers["ID"].Value
            Description = $null
            In          = $true
          }
        }
        if ($_.Qualifiers["Out"]) {
          $MethodObject.Parameters[$_.Name] = New-Object PSObject -Property @{
            Name        = $_.Name
            Type        = $_.CimType
            ID          = [int]$_.Qualifiers["ID"].Value
            Description = $null
            In          = $false
          }
        }
      }
      $HelpObject.Methods.Add($CimMethod.Name, $MethodObject)
    }

    if ($Property) {
      $PropertyObject = $HelpObject.Properties[$Property]

      if ($PropertyObject) {
        Add-Member -InputObject $PropertyObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
        $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#PropertyDetail")
        return $PropertyObject
      } else {
        throw "Property named '$Property' not found."
      }
    } elseif ($Method) {
      $MethodObject = $HelpObject.Methods[$Method]

      if ($MethodObject) {
        Write-Progress "Retrieving Parameter Descriptions"
        $i, $total = 0, $MethodObject.Parameters.Values.Count

        $MethodHelp = $LocalizedClass.Methods[$Method]
        $MethodObject.Parameters.Values | Where-Object { $_.In } | ForEach-Object {
          Write-Progress "Retrieving Parameter Descriptions" -PercentComplete ($i / $total * 100); $i++

          $ParameterHelp = $MethodHelp.InParameters.Properties | Where-Object Name -EQ $_.Name
          $_.Description = @($ParameterHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
              $Paragraph = New-Object PSObject -Property @{Text = $_.Trim() }
              $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
              if ($Paragraph.Text) { $Paragraph }
            })
        }
        $MethodObject.Parameters.Values | Where-Object { !$_.In } | ForEach-Object {
          Write-Progress "Retrieving Parameter Descriptions" -PercentComplete ($i / $total * 100); $i++

          $ParameterHelp = $MethodHelp.OutParameters.Properties | Where-Object Name -EQ $_.Name
          $_.Description = @($ParameterHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
              $Paragraph = New-Object PSObject -Property @{Text = $_.Trim() }
              $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
              if ($Paragraph.Text) { $Paragraph }
            }
          )
        }
        Add-Member -InputObject $MethodObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
        Add-Member -InputObject $MethodObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
        $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#MethodDetail")

        Write-Progress "Retrieving Parameter Descriptions" -Completed

        return $MethodObject
      } else {
        throw "Method named '$Method' not found."
      }
    } else {
      return $HelpObject
    }
  }

  static [object] GetCimUri([Microsoft.Management.Infrastructure.CimClass]$Type, [String]$Method, [String]$Property) {
    $Culture = $(Get-Variable Host).Value.CurrentCulture.Name
    $TypeName = $Type.CimClassName -replace "_", "-"
    if ($Method) {
      # $Page = "$TypeName#methods"
      $Page = "$Method-method-in-class-$TypeName"
    } elseif ($Property) {
      $Page = "$TypeName#properties"
    } else {
      $Page = $TypeName
    }
    return New-Object System.Uri "https://docs.microsoft.com/$Culture/windows/desktop/CIMWin32Prov/$Page"
  }
  static [object] GetCommandSyntax([String]$Name) {
    $r = foreach ($provider in (Get-PSProvider)) {
      if ((Get-Host).name -match "console") {
        "$([char]0x1b)[1;4;38;5;155m$($provider.name)$([char]0x1b)[0m"
      } else {
        $provider.name
      }
      #get first drive
      $path = "$($provider.drives[0]):\"
      Push-Location
      Set-Location $path
      $syn = Get-Command -Name $Name -Syntax | Out-String
      $get = Get-Command -Name $name

      $dynamic = ($get.parameters.GetEnumerator() | Where-Object { $_.value.IsDynamic }).key
      Pop-Location
      if ($dynamic) {
        Write-Verbose "...found $($dynamic.count) dynamic parameters"
        Write-Verbose "...$($dynamic -join ",")"
        foreach ($param in $dynamic) {
          if ((Get-Host).name -match 'console') {
            $syn = $syn -replace "\b$param\b", "$([char]0x1b)[1;38;5;213m$param$([char]0x1b)[0m"
          } else {
            #must be in the PowerShell ISE so don't use any ANSI formatting
          }
        }
      }
      $syn
    }
    return $r
  }
  static [object] GetHelpLocation([type]$Type) {
    # get documentation filename, assembly location and assembly codebase
    $DocFilename = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFileName($Type.assembly.Location), ".xml")
    $Location = [System.IO.Path]::GetDirectoryName($Type.assembly.Location)
    $CodeBase = (New-Object System.Uri $Type.assembly.CodeBase).LocalPath

    Write-Verbose ("Documentation file is '$DocFilename.'")

    ## try localized location (typically newer than base framework dir)
    $FrameworkDir = "${env:windir}\Microsoft.NET\framework\v2.0.50727"
    $Language = [System.Globalization.CultureInfo]::CurrentUICulture.Parent.Name
    $PathList = @(
      "$FrameworkDir\$Language\$DocFilename",
      "$FrameworkDir\$DocFilename",
      "$Location\$DocFilename",
      "$CodeBase\$DocFilename"
    )
    $r = @()
    foreach ($Path in $PathList) {
      if (Test-Path $Path) {
        $r += $Path
      }
    }
    return $r

    # if (!$Online)
    # {
    # # try localized location (typically newer than base framework dir)
    # $frameworkDir = "${env:windir}\Microsoft.NET\framework\v2.0.50727"
    # $lang = [system.globalization.cultureinfo]::CurrentUICulture.parent.name

    # # I love looking at this. A Duff's Device for PowerShell.. well, maybe not.
    # switch
    # (
    # "${frameworkdir}\${lang}\$docFilename",
    # "${frameworkdir}\$docFilename",
    # "$location\$docFilename",
    # "$codebase\$docFilename"
    # )
    # {
    # { test-path $_ } { $_; return; }

    # default
    # {
    # # try next path
    # continue;
    # }
    # }
    # }

    # # failed to find local docs, is it from MS?
    # if ((Get-ObjectVendor $type) -like "*Microsoft*")
    # {
    # # drop locale - site will redirect to correct variation based on browser accept-lang
    # $suffix = ""
    # if ($Members.IsPresent)
    # {
    # $suffix = "_members"
    # }

    # new-object uri ("http://msdn.microsoft.com/library/{0}{1}.aspx" -f $type.fullname,$suffix)

    # return
    # }
  }
  static [object] GetHelpUri([type]$Type, [String]$Member) {
    ## Needed for UrlEncode()
    Add-Type -AssemblyName System.Web
    $uri = $null
    $Vendor = Get-ObjectVendor $Type
    if ($Vendor -like "*Microsoft*") {
      ## drop locale - site will redirect to correct variation based on browser accept-lang
      $Suffix = ""
      if ($Member -eq "_members") {
        $Suffix = "_members"
      } elseif ($Member) {
        $Suffix = ".$Member"
      }

      $Query = [System.Web.HttpUtility]::UrlEncode(("{0}{1}" -f $Type.FullName, $Suffix))
      $uri = New-Object System.Uri "http://msdn.microsoft.com/library/$Query.aspx"
    } else {
      $Suffix = ""
      if ($Member -eq "_members") {
        $Suffix = " members"
      } elseif ($Member) {
        $Suffix = ".$Member"
      }

      if ($Vendor) {
        $Query = [System.Web.HttpUtility]::UrlEncode(("`"{0}`" {1}{2}" -f $Vendor, $Type.FullName, $Suffix))
      } else {
        $Query = [System.Web.HttpUtility]::UrlEncode(("{0}{1}" -f $Type.FullName, $Suffix))
      }
      $uri = New-Object System.Uri "http://www.bing.com/results.aspx?q=$Query"
    }
    return $uri
  }
  static [object] GetLocalizedNamespace([Object]$NameSpace, [Int32]$cultureID, [bool]$quiet) {
    #First, get a list of all localized namespaces under the current namespace
    $localizedNamespaces = Get-CimInstance -Namespace $NameSpace -Class "__Namespace" | Where-Object { $_.Name -like "ms_*" }
    if ($null -eq $localizedNamespaces) {
      if (!$quiet) {
        Write-Warning "Could not get a list of localized namespaces"
      }
      return $null
    }
    return ("$namespace\ms_{0:x}" -f $cultureID)
  }
  static [object] GetManagedDll([String]$FilePath) {
    $output = @()
    $Path = Resolve-Path $FilePath
    if (! [IO.File]::Exists($Path)) {
      throw "$Path does not exist."
    }
    $FileBytes = [System.IO.File]::ReadAllBytes($Path)
    if (($FileBytes[0..1] | ForEach-Object { [Char]$_ }) -join '' -cne 'MZ') {
      throw "$Path is not a valid executable."
    }
    $Length = $FileBytes.Length
    $CompressedStream = New-Object IO.MemoryStream
    $DeflateStream = New-Object IO.Compression.DeflateStream ($CompressedStream, [IO.Compression.CompressionMode]::Compress)
    $DeflateStream.Write($FileBytes, 0, $FileBytes.Length)
    $DeflateStream.Dispose()
    $CompressedFileBytes = $CompressedStream.ToArray()
    $CompressedStream.Dispose()
    $EncodedCompressedFile = [Convert]::ToBase64String($CompressedFileBytes)

    Write-Verbose "Compression ratio: $(($EncodedCompressedFile.Length/$FileBytes.Length).ToString('#%'))"

    $Output = @"
`$EncodedCompressedFile = @'
$EncodedCompressedFile
'@
`$DeflatedStream = New-Object IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String(`$EncodedCompressedFile),[IO.Compression.CompressionMode]::Decompress)
`$UncompressedFileBytes = New-Object Byte[]($Length)
`$DeflatedStream.Read(`$UncompressedFileBytes, 0, $Length) | Out-Null
[Reflection.Assembly]::Load(`$UncompressedFileBytes)
"@

    return $Output
  }

  static [object] GetNetHelp([type]$Type, [bool] $Detailed, [String]$Property, [String]$Method) {


    # if ($Docs = Get-HelpLocation $Type) {
    # Write-Verbose ("Found '$Docs'.")

    # $TypeName = $Type.FullName
    # if ($Method) {
    # $Selector = "M:$TypeName.$Method"
    # } else { ## TODO: Property?
    # $Selector = "T:$TypeName"
    # }

    # ## get summary, if possible
    # $Help = Import-LocalNetHelp $Docs $Selector

    # if ($Help) {
    # $Help #| Format-AssemblyHelp
    # } else {
    # Write-Warning "While some local documentation was found, it was incomplete."
    # }
    # }

    $HelpUrl = Get-HelpUri $Type
    $HelpObject = New-Object PSObject -Property @{
      Details      = New-Object PSObject -Property @{
        Name       = $Type.Name
        namespace  = $Type.namespace
        SuperClass = $Type.BaseType
      }
      Properties   = @{}
      Constructors = @()
      Methods      = @{}
      RelatedLinks = @(
        New-Object PSObject -Property @{Title = "Online Version"; Link = $HelpUrl }
      )
    }
    $HelpObject.Details.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Details")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo")
    if ($Detailed) {
      $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#DetailedView")
      # Write-Error "Local detailed help not available for type '$Type'."
    } else {
      $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net")
    }

    foreach ($NetProperty in $Type.DeclaredProperties) {
      $PropertyObject = New-Object PSObject -Property @{
        Name = $NetProperty.Name
        Type = $NetProperty.PropertyType
      }
      $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Property")
      $HelpObject.Properties.Add($NetProperty.Name, $PropertyObject)
    }

    foreach ($NetConstructor in $Type.DeclaredConstructors | Where-Object { $_.IsPublic }) {
      $ConstructorObject = New-Object PSObject -Property @{
        Name       = $Type.Name
        Namespace  = $Type.Namespace
        Parameters = $NetConstructor.GetParameters()
      }
      $ConstructorObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Constructor")

      $HelpObject.Constructors += $ConstructorObject
    }

    foreach ($NetMethod in $Type.DeclaredMethods | Where-Object { $_.IsPublic -and (!$_.IsSpecialName) } | Group-Object Name) {
      $MethodObject = New-Object PSObject -Property @{
        Name        = $NetMethod.Name
        Static      = $NetMethod.Group[0].IsStatic
        Constructor = $NetMethod.Group[0].IsConstructor
        ReturnType  = $NetMethod.Group[0].ReturnType
        Overloads   = @(
          $NetMethod.Group | ForEach-Object {
            $MethodOverload = New-Object PSObject -Property @{
              Name       = $NetMethod.Name
              Static     = $_.IsStatic
              ReturnType = $_.ReturnType
              Parameters = @(
                $_.GetParameters() | ForEach-Object {
                  New-Object PSObject -Property @{
                    Name          = $_.Name
                    ParameterType = $_.ParameterType
                  }
                }
              )
              Class      = $HelpObject.Details.Name
              Namespace  = $HelpObject.Details.Namespace
            }
            $MethodOverload.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#MethodOverload")
            $MethodOverload
          }
        )
      }
      $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Method")

      $HelpObject.Methods.Add($NetMethod.Name, $MethodObject)
    }

    $DownloadOnlineHelp = $true
    if ($Property) {
      $PropertyObject = $HelpObject.Properties[$Property]

      if ($PropertyObject) {
        $PropertyHelpUrl = Get-HelpUri $Type -Member $Property
        Add-Member -InputObject $PropertyObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
        Add-Member -InputObject $PropertyObject -Name RelatedLinks -Value @(New-Object PSObject -Property @{Title = "Online Version"; Link = $PropertyHelpUrl }) -MemberType NoteProperty
        $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#PropertyDetail")

        if ($DownloadOnlineHelp) {
          $OnlineHelp = Import-OnlineHelp $PropertyHelpUrl
          if ($OnlineHelp) {
            Add-Member -InputObject $PropertyObject -Name Summary -Value $OnlineHelp.Summary -MemberType NoteProperty
          }
        }

        return $PropertyObject
      } else {
        throw "Property named '$Property' not found."
      }
    } elseif ($Method) {
      $MethodObject = $HelpObject.Methods[$Method]

      if ($MethodObject) {
        $MethodHelpUrl = Get-HelpUri $Type -Member $Method
        Add-Member -InputObject $MethodObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
        Add-Member -InputObject $MethodObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
        Add-Member -InputObject $MethodObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
        Add-Member -InputObject $MethodObject -Name RelatedLinks -Value @(New-Object PSObject -Property @{Title = "Online Version"; Link = $MethodHelpUrl }) -MemberType NoteProperty
        $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#MethodDetail")

        if ($DownloadOnlineHelp) {
          $OnlineHelp = Import-OnlineHelp $MethodHelpUrl
          if ($OnlineHelp) {
            Add-Member -InputObject $MethodObject -Name Summary -Value $OnlineHelp.Summary -MemberType NoteProperty
          }
        }

        return $MethodObject
      } else {
        throw "Method named '$Method' not found."
      }
    } else {
      if ($DownloadOnlineHelp) {
        $OnlineHelp = Import-OnlineHelp $HelpUrl
        if ($OnlineHelp) {
          Add-Member -InputObject $HelpObject.Details -Name Summary -Value $OnlineHelp.Summary -MemberType NoteProperty
        }
      }
      return $HelpObject
    }
  }
  static [object] GetObjectHelp([PSObject]$Object, [bool] $Detailed, [String]$Method, [String]$Property, [bool] $Online) {
    $help = $null
    $Type = $null
    $TypeName = $null
    # $Selector = $null

    Write-Verbose "Start processing..."
    Write-Verbose ("Input object (Type:" + $Object.GetType() + ", IsType:" + ($Object -is [System.type]) + ")")
    if ($Object -is [Management.Automation.PSMemberInfo]) {
      if ($Object -is [System.Management.Automation.PSMethod]) {
        $Method = $Object.Name
        $Type = Resolve-MemberOwnerType $Object
      } else {
        Write-Error "Unable to identify owning time of PSMembers."
        return $null
      }
    } elseif ($Object -is [Microsoft.PowerShell.Commands.MemberDefinition]) {
      if ($Object.MemberType -eq "Method") {
        $Method = $Object.Name
      } else {
        $Property = $Object.Name
      }
      if ($Object.TypeName -match '^System.Management.ManagementObject#(.+)') {
        $Type = $Object.TypeName
      } else {
        $Type = "$($Object.TypeName)" -as [System.type]
      }
    } elseif ($Object -is [Microsoft.Management.Infrastructure.CimClass]) {
      $Type = $Object
    } elseif ($Object -is [Microsoft.Management.Infrastructure.CimInstance]) {
      $Type = $Object.PSBase.CimClass
    } elseif ($Object -is [System.Management.ManagementObject]) {
      $Type = Get-CimClass $Object.__CLASS -Namespace $Object.__NAMESPACE
    } elseif ($Object -is [System.__ComObject]) {
      $Type = $Object
    } elseif ($Object -is [System.String]) {
      switch -regex ($Object) {
        '^\[[^\[\]]+\]$' {
          ## .NET Type (ex: [System.String])
          try {
            $Type = { $Object }.Invoke()
          } catch { $null }
          break
        }
        '^(Win32|CIM)_[\w]+' {
          $Type = Get-CimClass $Object
        }
        ## TODO: WMI / CIM
        default {}
      }
    } elseif ($Object -as [System.type]) {
      $Type = $Object -as [System.type]
    }

    if (!$Type) {
      Write-Error "Could not identify object"
      return $null
    }

    Write-Verbose ("Object (Type:" + $Object.GetType() + ", IsType:" + ($Object -is [System.type]) + ")")
    Write-Verbose ("Method is: $Method")
    Write-Verbose ("Property is: $Property")

    $Culture = $(Get-Variable Host).Value.CurrentCulture.Name
    ## TODO: Support culture parameter?

    if ($Type -is [Microsoft.Management.Infrastructure.CimClass]) {
      if ($Online) {
        if ($Uri = Get-CimUri -Type $Type -Method $Method -Property $Property) {
          [System.Diagnostics.process]::Start($Uri.ToString()) | Out-Null
        }
      } else {
        if ($Method) {
          $help = Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.namespace -Method $Method
        } elseif ($Property) {
          $help = Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.namespace -Property $Property
        } else {
          $help = Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.namespace -Detailed:$Detailed
        }
      }
    } elseif ($Type -is [System.type]) {
      if ($Online) {
        $Member = if ($Method) {
          $Method
        } elseif ($Property) {
          $Property
        } else {
          $null
        }
        if ($Uri = Get-HelpUri $Type -Member $Member) {
          [System.Diagnostics.process]::Start($Uri.ToString()) | Out-Null
        }
      } else {
        if ($Method) {
          $help = Get-NetHelp -Type $Type -Method $Method
        } elseif ($Property) {
          $help = Get-NetHelp -Type $Type -Property $Property
        } else {
          $help = Get-NetHelp -Type $Type -Detailed:$Detailed
        }
      }
    } elseif ($Type -is [System.__ComObject]) {
      if ($Online) {
        if ($Type.PSTypeNames[0] -match 'System\.__ComObject#(.*)$') {
          if (Test-Path "HKLM:\SOFTWARE\Classes\Interface\$($Matches[1])") {
            $TypeKey = (Get-ItemProperty "HKLM:\SOFTWARE\Classes\Interface\$($Matches[1])").'(default)'
            if ('_Application' -contains $TypeKey) {
              # $TypeLib = ..
              # $Version = ..
              # $TypeName = (Get-ItemProperty "HKLM:\SOFTWARE\Classes\TypeLib\$TypeLib\$Version").'(default)'
            } else {
              $TypeName = $TypeKey
            }
          }
        }
        $Uri = "http://social.msdn.microsoft.com/Search/$Culture/?query=$TypeName"
        [System.Diagnostics.process]::Start($uri) | Out-Null
      } else {
        Write-Error "Local help not supported for COM objects."
      }
    }
    return $help
  }
  static [object] GetHelp([object]$Target) {
    return [HelpTools]::GetHelp($Target, $false)
  }
  static [object] GetHelp([object]$Target, [bool]$Detailed) {
    return [HelpTools]::GetObjectHelp($Target, $Detailed, $null, $null, $false)
  }
  static [object] GetOnlineHelp([object]$Target) {
    return [HelpTools]::GetObjectHelp($Target, $false, $null, $null, $true)
  }

  static [object] GetObjectVendor([type] $Type, [bool] $CompanyOnly) {
    $Assembly = $Type.assembly
    $attrib = $Assembly.GetCustomAttributes([Reflection.AssemblyCompanyAttribute], $false) | Select-Object -First 1

    if ($attrib.Company) {
      return $attrib.Company
    } else {
      if ($CompanyOnly) { return  $null }

      # try copyright
      $attrib = $Assembly.GetCustomAttributes([Reflection.AssemblyCopyrightAttribute], $false) | Select-Object -First 1

      if ($attrib.Copyright) {
        return $attrib.Copyright
      }
    }
    Write-Verbose ("Assembly has no [AssemblyCompany] or [AssemblyCopyright] attributes.")
    return $null
  }
  static [object] ResolveMemberOwnerType([System.Management.Automation.PSMethod]$Method) {
    # TODO: support overloads, support interface definitions

    Write-Verbose ("Resolving owning type of '$($Method.Name)'.")
    # hackety-hack - this is prone to breaking in the future
    $TargetType = [System.Management.Automation.PSMethod].GetField("baseObject", "Instance,NonPublic").GetValue($Method)
    if (($TargetType -isnot [System.type]) -and (!$TargetType.__CLASS)) {
      $TargetType = $TargetType.GetType()
    }

    if ($TargetType -is [System.Management.ManagementObject]) {
      $DeclaringType = Get-CimClass $TargetType.__CLASS -Namespace $TargetType.__NAMESPACE
    } else {
      if ($Method.OverloadDefinitions -match "static") {
        $Flags = "Static,Public"
      } else {
        $Flags = "Instance,Public"
      }

      # TODO: support overloads
      $MethodInfo = $TargetType.GetMethods($Flags) | Where-Object { $_.Name -eq $Method.Name } | Select-Object -First 1

      if (!$MethodInfo) {
        # this shouldn't happen.
        throw "Could not resolve owning type."
      }

      $DeclaringType = $MethodInfo.DeclaringType
    }

    Write-Verbose ("Owning type is $($TargetType.FullName). Method declared on $($DeclaringType.FullName).")
    return $DeclaringType
  }
  static [object] SearchWmiHelp([ScriptBlock]$DescriptionExpression, [ScriptBlock]$MethodExpression, [ScriptBlock]$PropertyExpression, [Object] $Namespaces, [Object] $CultureID, [bool] $List) {
    $resultWmiClasses = @{}
    foreach ($namespace in $Namespaces) {
      #First, get a list of all localized namespaces under the current namespace

      $localizedNamespace = Get-LocalizedNamespace $namespace
      if ($null -eq $localizedNamespace) {
        Write-Verbose "Could not get a list of localized namespaces"
        return $null
      }

      $localizedClasses = Get-CimInstance -Namespace $localizedNamespace -Query "select * from meta_class"
      $count = 0
      foreach ($WmiClass in $localizedClasses) {
        $count++
        Write-Progress "Searching Wmi Classes" "$count of $($localizedClasses.Count)" -PercentComplete ($count * 100 / $localizedClasses.Count)
        $classLocation = $localizedNamespace + ':' + $WmiClass.__Class
        $classInfo = Get-WmiClassInfo $classLocation
        [bool]$found = $false
        if ($null -ne $classInfo) {
          if (! $resultWmiClasses.ContainsKey($classLocation)) {
            $resultWmiClasses.Add($wmiClass.__Class, $classInfo)
          }

          $descriptionMatch = [bool]($classInfo.Description | Where-Object $DescriptionExpression)
          $methodMatch = [bool]($classInfo.Methods.GetEnumerator() | Where-Object $MethodExpression)
          $propertyMatch = [bool]($classInfo.Properties.GetEnumerator() | Where-Object $PropertyExpression)

          $found = $descriptionMatch -or $methodMatch -or $propertyMatch

          if (! $found) {
            $resultWmiClasses.Remove($WmiClass.__Class)
          }
        }
      }
    }

    if ($List) {
      $resultWmiClasses.Keys | Sort-Object
    } else {
      $resultWmiClasses.GetEnumerator() | Sort-Object Key
    }
    return $null
  }
}