SCOMHelper.psm1

<#
    2020.11.19 - Update-SCOMComputerGroup: improved a little bit. Update-OMGroupDiscovery2: added default Mgmtservername value and failure logging.
    2020.08.18 - Added new function: Update-SCOMComputerGroup. Fixed small issue with New-SCOMClassGraph.
    2020.08.11 - Added a Private.ps1 file to include common functions.
    2020.07.22 - Remove 'Import-Module OperationsManager'
#>


# Load private functions
. (Join-Path $PSScriptRoot Private.ps1)


#######################################################################
#######################################################################
#######################################################################
#######################################################################


<#
    .Synopsis
    Will clear SCOM agent cache on local machine
   
    .EXAMPLE
    Clear-SCOMCache -HealthServiceStatePath 'D:\Program Files\Microsoft System Center 2012 R2\Operations Manager\Server\Health Service State'
 
    .EXAMPLE
    Clear-SCOMCache -TimeoutSeconds 60
 
    .INPUTS
    None
   
    .OUTPUTS
    Boolean [true|false]
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Version History:
    2020.07.21 - Improved error handling a bit. Added cmdletbinding. Changed outputtype to Bool.
    2020.02.27 - Cleaned up code a bit. Improved logic. Removed WaitSeconds parameter as it was not effective.
    2018.05.25 - Cleaned up. Revised a few things.
     
#>

Function Clear-SCOMCache {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$false, 
      PositionalBinding=$false,
      HelpUri = 'http://www.microsoft.com/',
  ConfirmImpact='Medium')]
  [OutputType([BOOL])]
  
  Param(
    
    # How many seconds to wait before aborting
    [int]$TimeoutSeconds = 30,
    
    # If the registry does not contain the correct path you can specify a path here.
    [string]$HealthServiceStatePath = (Join-Path (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory 'Health Service State' )

  )
 
 
  $ServiceName = 'HealthService'
  Write-Verbose "Stopping service: $ServiceName"
  
  $StartTime = Get-Date
  Try {
    Stop-Service -Name $ServiceName -Verbose -Force -PassThru
    While ( ((Get-Service -Name $ServiceName).Status -ne "Stopped") -AND ((Get-Date) -le ($StartTime.AddSeconds($TimeoutSeconds) )) ) {
      Write-Verbose "Waiting for $ServiceName to stop..."
      Start-Sleep -Seconds 5
    } 
  } Catch {
    Write-Warning "Problem while stopping service: $($_). Continuing..."
  }
  
  If ((Get-Service -Name $ServiceName).Status -ne "Stopped") {
    Write-Warning "Failed to stop service: $ServiceName in the time allowed! Will attempt to leave HealthService in 'Started' state."
    Start-Service $ServiceName -Verbose
    Return $false
  }
  Else {
    Write-Verbose "Attempting to remove cache folder: $HealthServiceStatePath"
    Try {
      Get-Item $HealthServiceStatePath | Remove-Item -Recurse -Force -Verbose -ErrorAction Stop
      Start-Sleep -Seconds 1
    } Catch {
      Write-Warning "Error while deleting cache files. $($_)"  
    }
    If (Test-Path -PathType Container $HealthServiceStatePath) {
      Write-Warning "Failed to remove folder: [$($HealthServiceStatePath)] !"
    }
    Else {
      [bool]$folderRemoved = $true
      Write-Verbose "Health Service State folder removed succesfully: [$($HealthServiceStatePath)]."
    }

    $StartTime = Get-Date
    Try {
      Start-Service -Name $ServiceName -Verbose
      While ( ((Get-Service -Name $ServiceName).Status -ne "Running") -AND ((Get-Date) -le ($StartTime.AddSeconds($TimeoutSeconds) )) ) {
        Start-Sleep -Seconds 3
      }
      If ((Get-Service -Name $ServiceName).Status -ne "Running") {
        Write-Warning "Failed to start service: $ServiceName in the time allowed! Verify service manually."
      }
      Else {
        If ( (Test-Path -PathType Container $HealthServiceStatePath) -AND ([bool]$folderRemoved)){
          Write-Verbose "Cache folder auto-created: $HealthServiceStatePath !"
          Write-Verbose "SCOM agent Cache has been cleared."
          Return $true
        }
      }
    } Catch {
      Throw
      Return $false
    }

    If (-NOT [bool]$folderRemoved){
      Write-Warning "SCOM cache was not fully cleared; not all files/folders were deleted. For best results, try again or reboot and try again. Alternatively you could try clearing cache manually."
      Return $false
    }
    
    #default scenario, assumed something didn't go quite right
    Return $false
  }
} #End Function
#######################################################################

<#
    .Synopsis
    An easy way to compare individual characters of two strings.
     
    .EXAMPLE
    Compare-String -String1 "Captain Jack will get you by tonight" -String2 "Çaptain JAck"
    # Example Output
 
    Index String1/ASCII String2/ASCII Compare Algorithm String1_Hash String2_Hash
    ----- ------------- ------------- ------- --------- ------------ ------------
    0 C:67 Ç:199 Different SHA1 32096c2e0eff33d844ee6d675407ace18289357d 1d866b5fef7cdae2d4deba53c7ae4bdbf76ca671
    1 a:97 a:97 Same SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
    2 p:112 p:112 Same SHA1 516b9783fca517eecbd1d064da2d165310b19759 516b9783fca517eecbd1d064da2d165310b19759
    3 t:116 t:116 Same SHA1 8efd86fb78a56a5145ed7739dcb00c78581c5375 8efd86fb78a56a5145ed7739dcb00c78581c5375
    4 a:97 a:97 Same SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
    5 i:105 i:105 Same SHA1 042dc4512fa3d391c5170cf3aa61e6a638f84342 042dc4512fa3d391c5170cf3aa61e6a638f84342
    6 n:110 n:110 Same SHA1 d1854cae891ec7b29161ccaf79a24b00c274bdaa d1854cae891ec7b29161ccaf79a24b00c274bdaa
    7 :32 :32 Same SHA1 b858cb282617fb0956d960215c8e84d1ccf909c6 b858cb282617fb0956d960215c8e84d1ccf909c6
    8 J:74 J:74 Same SHA1 58668e7669fd564d99db5d581fcdb6a5618440b5 58668e7669fd564d99db5d581fcdb6a5618440b5
    9 a:97 A:65 Different SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 6dcd4ce23d88e2ee9568ba546c007c63d9131c1b
    10 c:99 c:99 Same SHA1 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4
    11 k:107 k:107 Same SHA1 13fbd79c3d390e5d6585a21e11ff5ec1970cff0c 13fbd79c3d390e5d6585a21e11ff5ec1970cff0c
    
    The Example above will compare the individual characters of the shortest parameter to that of the other parameter and display ASCII values for each while indicating any differences between characters of the same index/location.
     
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Version: 1.0
    Version History:
    2020.02.26 - Slight revision. Added commenting, better Help content. Added to SCOMHelper module.
    2018.04.20 - Original
#>

Function Compare-String {
  Param(
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$String1,

    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$String2,

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet("SHA", "SHA1", "MD5", "SHA256", "SHA384", "SHA512","RIPEMD160")]
    $Algorithm = "SHA1" #SHA1 is default. Ref: https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=netframework-4.8
  )

  ######################### MAIN ################################
  [System.Collections.ArrayList]$arr = @()
  0..([math]::min(($String1.Length -1),($String2.Length -1))) | ForEach-Object {
    $String1_char = [string]$String1[$_]
    $String2_char = [string]$String2[$_]
    $String1_hash = Get-StringHash $String1_char -Algorithm $Algorithm
    $String2_hash = Get-StringHash $String2_char -Algorithm $Algorithm
    $obj = New-Object PSCustomObject
    $obj | Add-Member -Name 'Index' -Value $_ -MemberType NoteProperty
    $obj | Add-Member -Name 'String1/ASCII' -Value ("$($String1_char):$([double][char]$String1_char)") -MemberType NoteProperty
    $obj | Add-Member -Name 'String2/ASCII' -Value ("$($String2_char):$([double][char]$String2_char)") -MemberType NoteProperty

    If ($String1_hash -ne $String2_hash){
      $obj | Add-Member -Name 'Compare' -Value "Different" -MemberType NoteProperty
    }
    Else{
      $obj | Add-Member -Name 'Compare' -Value "Same" -MemberType NoteProperty
    }
    $obj | Add-Member -Name "Algorithm" -Value $Algorithm -MemberType NoteProperty  
    $obj | Add-Member -Name "String1_Hash" -Value $String1_hash -MemberType NoteProperty
    $obj | Add-Member -Name "String2_Hash" -Value $String2_hash -MemberType NoteProperty
        
    $null = $arr.Add($obj)
  }
  Return ($arr | Format-Table -AutoSize)
} #End Function
#######################################################################

<#
    .DESCRIPTION
    Will install MOMAgent.msi on one or more target computers. Agent will need to be manually approved unless security settings allow automatic approval.
 
    .EXAMPLE
    #
    # This example will set two target computer names, prompt for a credential, then deploy the agent to the target computers.
    # The installation scriptblock will wait 120 seconds for the service to install and present a status of "Running" before returning the service status to the output.
    PS C:\> $Computers = "scorch1.contoso.com","devdb01.contoso.com"
    PS C:\> $MyCred = Get-Credential -UserName 'Contoso\tpaul' -Message "Enter Action credential (Local Admin on targets)"
    PS C:\> Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 1 -NOAPM 1 -InstallCredential $MyCred -TargetComputer $Computers -WaitForServiceSeconds 120
 
    .EXAMPLE
    #
    # This example will pipe the the array of computer names to the function. This is slower than passing the array as a parameter with '-TargetComputer'.
    # The installation scriptblock will wait 120 seconds for the service to install and present a status of "Running" before returning the service status to the output.
    PS C:\> $Computers = "scorch1.contoso.com","devdb01.contoso.com"
    PS C:\> $Computers | Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 1
 
 
    .EXAMPLE
    #
    # This example demonstrates how you would specify an alternate Default Action Account. This is a very, very rare requirement but here's an example anyway.
    PS C:\> $InstallCred = Get-Credential -UserName 'Contoso\tpaul' -Message "Enter Action credential (Local Admin on targets)"
    PS C:\> $DefaultActionCred = Get-Credential -UserName 'Contoso\svc_LowPrivAccount' -Message "Enter Default Action credential (for very, very rare low-priv scenarios)"
    PS C:\> Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 0 -NOAPM 1 -TargetComputer "devdb01.contoso.com" -WaitForServiceSeconds 180 -DefaultActionAccountCredential $DefaultActionCred -InstallCredential $InstallCred
 
 
    .INPUTS
    Function accepts an array of computer names as piped input.
    .OUTPUTS
    Outputs objects for each installation attempt.
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/2019/11/12/scomhelper/
    History:
    2020.05.13 - First version
     
    For additional info on manual installation parameters see: https://docs.microsoft.com/en-us/system-center/scom/manage-deploy-windows-agent-manually?view=sc-om-2019
     
#>

Function Deploy-SCOMAgent {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$true, 
      PositionalBinding=$false,
      HelpUri = 'http://www.microsoft.com/',
  ConfirmImpact='Medium')]
  Param (
    # The account that will remotely execute the installation of the agent. This function relies on WSMAN therefore this credential must have rights to PSRemoting access to the target machines. This is not the Microsoft Monitoring Agent (HealthService) service credentials. Default: <current user creds>
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [Alias("Credential")]
    [pscredential]$InstallCredential,


    # {0|1} Indicates whether the management group settings properties will be set on the command line. Use 0 if you want to set the properties at the command line. Use 1 to use the management group settings from Active Directory.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet('0','1')]
    [string]$USE_SETTINGS_FROM_AD = 0,

    # {0|1} If USE_SETTINGS_FROM_AD=1, then USE_MANUALLY_SPECIFIED_SETTINGS must equal 0.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet('0','1')]
    [string]$USE_MANUALLY_SPECIFIED_SETTINGS = 1,

    # Specifies the management group (name) that will manage the computer.
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=3,
    ParameterSetName='Parameter Set 1')]
    [string]$MANAGEMENT_GROUP,


    # Specifies the fully qualified domain name for the management server. To use a gateway server, enter the gateway server FQDN as MANAGEMENT_SERVER_DNS.
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=4,
    ParameterSetName='Parameter Set 1')]
    [ValidatePattern('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)')]
    [string]$MANAGEMENT_SERVER_DNS,


    # Use this parameter if the computer's DNS and Active Directory names differ to set to the fully qualified Active Directory Domain Services name.
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=5,
    ParameterSetName='Parameter Set 1')]
    [ValidatePattern('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)')]
    [string]$MANAGEMENT_SERVER_AD_NAME,


    # Sets the health service port number. It is rare to modify this and not advised.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=6,
    ParameterSetName='Parameter Set 1')]
    [ValidateRange(1025,65535)]
    [int]$SECURE_PORT = 5723,


    # Optional parameter. Use this parameter if you want to install the agent to a path other than the default installation path. Note that \Agent will be appended to this value.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=7,
    ParameterSetName='Parameter Set 1')]
    [System.IO.FileInfo]$INSTALLDIR,


    # Indicates whether the service will run as a specified user account (0) or the default 'LocalSystem' account (1). Default=1.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=8,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet('0','1')]
    [string]$ACTIONS_USE_COMPUTER_ACCOUNT=1,


    # This is the Microsoft Monitoring Agent (HealthService) Default Action Account credentials if other than 'LocalSystem'. Use of an alternate credential is very rare and typically unnecessary. Use with caution.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=9,
    ParameterSetName='Parameter Set 1')]
    [pscredential]$DefaultActionAccountCredential,
        
        
    # {0|1} Optional parameter. Installs the Operations Manager agent without .NET Application Performance Monitoring. If you are using AVIcode 5.7, NOAPM=1 leaves the AVIcode agent in place. If you are using AVIcode 5.7 and install the Operations Manager agent by using momagent.msi without NOAPM=1, the AVIcode agent will not work correctly and an alert will be generated.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=10,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet('0','1')]
    [string]$NOAPM=1,


    # One or more target computers FQDN, comma-separated.
    [Parameter(Mandatory=$True, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true, 
        ValueFromRemainingArguments=$false, 
        Position=11,
    ParameterSetName='Parameter Set 1')]
    [string[]]$TargetComputer,


    # The full path to the agent installation file. Default = '<SCOM Mgmt Server Install Directory>\AgentManagement\amd64\MOMAgent.msi'
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=12,
    ParameterSetName='Parameter Set 1')]
    [System.IO.FileInfo]$AgentMSIPath,


    # The number of seconds to wait for the service to appear after installation before returning from the remote PSSession. Default:180.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=13,
    ParameterSetName='Parameter Set 1')]
    [int]$WaitForServiceSeconds = 180
  )

  #region Begin
  Begin {  

    $hash = @{}
    $hash.msi = $null
    $hash.outDir = @((Join-Path $env:Windir "Temp"),'C:\Temp')
    If (-NOT $AgentMSIPath){
      $hash.InstallDirectory =  (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory
      $hash.agentPath = (Join-Path $hash.InstallDirectory 'AgentManagement\amd64\MOMAgent.msi' )
    }
    Try{
      $hash.fileObj = Get-Item -Path $hash.agentPath -ErrorAction STOP
      $hash.msi = [System.IO.File]::ReadAllBytes($hash.fileObj.FullName)
    } Catch {
      Throw "Failure getting MOMAgent.msi from path: [$($hash.agentPath)]! Either specify the correct path to the file or put the file at this location. "
      Return 1
    }
    # amount of time to wait for HealthService service to enumerate in Services before returning from Scriptblock
    $hash.WaitForServiceSeconds = $WaitForServiceSeconds
    [bool]$hash.WhatIf = (-Not ($PSCmdlet.ShouldProcess($hash)))
    
    $Params = @{
      USE_SETTINGS_FROM_AD = $USE_SETTINGS_FROM_AD
      USE_MANUALLY_SPECIFIED_SETTINGS = $USE_MANUALLY_SPECIFIED_SETTINGS
      MANAGEMENT_GROUP = $MANAGEMENT_GROUP
      MANAGEMENT_SERVER_DNS = $MANAGEMENT_SERVER_DNS
      MANAGEMENT_SERVER_AD_NAME = $MANAGEMENT_SERVER_AD_NAME
      SECURE_PORT = $SECURE_PORT
      INSTALLDIR = $INSTALLDIR
      ACTIONS_USE_COMPUTER_ACCOUNT = $ACTIONS_USE_COMPUTER_ACCOUNT
      NOAPM  = $NOAPM
    }

    If ($DefaultActionAccountCredential){
      $Params.ACTIONSUSER = $DefaultActionAccountCredential.GetNetworkCredential().UserName
      Try {
        $Params.ACTIONSDOMAIN = $DefaultActionAccountCredential.GetNetworkCredential().Domain
      }Catch{
        $Params.ACTIONSDOMAIN = ''
      }
      $Params.ACTIONSPASSWORD = $DefaultActionAccountCredential.GetNetworkCredential().Password
    }
    #-------------------------------------------------
    #region InstallationBlock
    $scriptBlock = [scriptblock]{
      Param (
        $hash,
        $Params
      )
      # Buid custom object to return. Initially assume it doesn't exist.
      $obj = New-Object -TypeName PSCUSTOMOBJECT
      $obj | Add-Member -MemberType NoteProperty -Name 'ServerName' -Value "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)"
      $obj | Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'HealthService'
      $obj | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'NOT FOUND'
      $obj | Add-Member -MemberType NoteProperty -Name 'Comment' -Value ''

      Try{
        # If service already exists, do nothing. Return.
        $Service = Get-Service -Name HealthService -ErrorAction Stop
        $obj.Status = $Service.Status.ToString()
        $obj.Comment = "Service already exists. No action taken."
        Return $obj
      }Catch {
        # Assume service doesn't appear yet. Proceed to install.
      }

      # Write the .msi to temp dir on remote target.
      Try {
        $exePath = (Join-Path $hash.outDir[0] $hash.fileObj.Name)
        [System.IO.File]::WriteAllBytes($exePath, $hash.msi)
      }Catch {
        Try{
          $exePath = (Join-Path $hash.outDir[1] $hash.fileObj.Name)
          [System.IO.File]::WriteAllBytes($exePath, $hash.msi)
        }
        Catch {
          $obj.Comment = "Remote connection successful but failed to copy bits to path: [$($exePath)]. Service not installed. Error was $_"
          Return $obj
        }
      }

      #region Build CMD string
      $CMD = "msiexec.exe /i $exePath /passive /l*v $(Split-Path $exePath -Parent)\OMAgentinstall.log"
      ForEach ($Key in @($Params.Keys)) {
        switch ($Key)
        {
          # Wrap InstallDir in double quotes
          {($Key -match 'INSTALLDIR') -AND ($Params.$Key.LENGTH -ge 1) } {$CMD += " $($Key)=```"$($Params.$_)```""; Continue; }
        
          {$Params.$_.LENGTH -ge 1 } {$CMD += " $($Key)=$($Params.$_)"}

          Default {Continue}
        }
      }
      
      # it makes sense to hardcode this param=1
      $CMD += " AcceptEndUserLicenseAgreement=1"
      #endregion Build CMD


      #Install Command
      If ($hash.WhatIf){
        $obj.Status = "Running (WHATIF)"
        $obj.Comment = "(WHATIF) Service installed successfully. Bits were actually written to this path successfully: [$($exePath)]"
        Return $obj
      }
      
      Else {
        # Get down to business!
        Invoke-Expression $CMD
          
        #Set timer
        $ServiceTimer = [System.Diagnostics.Stopwatch]::StartNew()
        Do{
          Try{
            $Service = Get-Service -Name HealthService -ErrorAction Stop
          }Catch {
            # Assume service doesn't appear yet
            Start-Sleep -Seconds 3
          }
        }While (($ServiceTimer.Elapsed.TotalSeconds -lt $hash.WaitForServiceSeconds) -AND  ([string]($Service.Status) -ne "Running") )
        $ServiceTimer.Stop()

        If ([bool]$Service ){
          $obj.Status = [string]($Service.Status)
          $obj.Comment = "Service installed successfully."
        }
        Else {
          $obj.Comment = "Service not installed/found. Command line: `n$($CMD) `n`nTimerSeconds:$($ServiceTimer.Elapsed.TotalSeconds), WaitForServiceSeconds:$($hash.WaitForServiceSeconds), ServiceStatus:$($Service.Status), ServiceRetrievedSuccess:$([bool]$Service)."
        }
      }
      Return $obj
    }
    #endregion #region InstallationBlock
    #-------------------------------------------------

  }
  #endregion Begin




  #region Process
  Process {
  
    # ----------- Configure remote session -----------
    #region ConfigureSessionRemotely
    $SessionConfigName = 'MaxDataSizeMB500'
    $ConfigureSessionBlock = [scriptblock]{
      Param(
        $SessionConfigName 
      )
      Try {
        # If session option is already registered, get it
        $var = Get-PSSessionConfiguration -Name $SessionConfigName -ErrorAction Stop
      }Catch {
        $var = Register-PSSessionConfiguration -Name $SessionConfigName
      }
      $var = Set-PSSessionConfiguration -Name $SessionConfigName -MaximumReceivedDataSizePerCommandMB 500 -MaximumReceivedObjectSizeMB 500
    }
    $SessionParams = @{
      ComputerName = $TargetComputer
    }
    If ($InstallCredential) {
      $SessionParams.Credential = $InstallCredential
    }

    $Session = New-PSSession @SessionParams 
    #Set session config on remote
    Try {
      Invoke-Command -Session $Session -ArgumentList $SessionConfigName -ScriptBlock $ConfigureSessionBlock -ErrorAction Stop
    }
    Catch {
      Write-Warning "WinRM Reset will typically cause an 'I/O operation has been aborted...' message." 
    }

    Remove-PSSession -Session $Session
    #endregion ConfigureSessionRemotely
    #-------------------------------------------------



    #-------------------------------------------------
    #region SetMyLocalSessionConfig
    Try {
      # If session option is already registered, get it
      $var = (Get-PSSessionConfiguration -Name $SessionConfigName -ErrorAction Stop)
    }Catch {
      $var = Register-PSSessionConfiguration -Name $SessionConfigName
    }
    $var = Set-PSSessionConfiguration -Name $SessionConfigName -MaximumReceivedDataSizePerCommandMB 500 -MaximumReceivedObjectSizeMB 500
    #endregion SetMyLocalSessionConfig
    #-------------------------------------------------


    #-------------------------------------------------
    #region PrepareInstallSession
    $SessionName = "MySCOMInstallSession"
    $SessionParams = @{
      Name = $SessionName
      ComputerName = $TargetComputer 
      ConfigurationName = $SessionConfigName
    }
    If ($InstallCredential) {
      $SessionParams.Credential = $InstallCredential
    }
    $Session = New-PSSession @SessionParams 
    #endregion PrepareInstallSession
    #-------------------------------------------------
        

    # Execute installation
    $InvokeParams = @{
      Session = $Session
      ScriptBlock = $scriptBlock
      ArgumentList = @($hash, $Params)
      Verbose = $true
    }
    $Result = Invoke-Command @InvokeParams
    Remove-PSSession -Session $Session

    Return $Result
  }
  #region Process
  
  End {
  
  }
  
} #end Function


#######################################################################
<#
    .SYNOPSIS
    Operations Manager Powershell function to output the effective monitoring configuration.
    .DESCRIPTION
    This will output a generous amount of information about monitors, rules, discoveries and the effective config for a given object (and all hosted/contained objects). CSV or HTML output.
    There exists a similar function in the standard OpsMan PowerShell module but AFAIK, even with the most recent UR10(SCOM2016) update at the time of this writing, it is limited in its output and I don’t find it very useful which is why I wrote this function.
 
    .EXAMPLE
    #
    #Example
    PS C:\> $ComputerName = "DB01.CONTOSO.COM"
    PS C:\> $objs = Get-SCOMClass -Name 'Microsoft.Windows.Computer' | Get-SCOMClassInstance | Where-Object Name -match "$ComputerName"
    PS C:\> $ExportPath = 'C:\Temp\EffectiveConfig\'
    PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $objs -ExportCSVPath $ExportPath -IndividualFiles -SortBy AlertPriority -SortOrder Ascending -Verbose
 
    The commands above will export a CSV file of monitoring configuration for the Windows Computer object and everything hosted on it to a CSV file sorted ascending by AlertPriorty, with verbose output to the screen.
    The command will also output individual CSV files for every contained or hosted object.
 
    .EXAMPLE
    #
    #Example
    PS C:\> $obj = Get-SCOMClass -Name 'Microsoft.SQLServer.2016.DBEngine' | Get-SCOMClassInstance | Where DisplayName -eq 'MySQLInstance'
    PS C:\> $ExportPath = 'C:\Temp\EffectiveConfig\'
    PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $obj -ExportHTMLPath $ExportPath -HTMLFormat Basic
 
    The commands above will output a fancy HTML report for the specified SQL instance(s).
    NOTE: The 'fancy' HTML report should only be used for instances with few hosted objects. This report does not perform well with a high row count.
 
    .EXAMPLE
    #
    #EXAMPLE
    PS C:\> $Name = 'DC3SQLDev01'
    PS C:\> $OutFolder = 'D:\Temp'
 
    PS C:\> $Date = (Get-date -f "yyyyMMdd_hhmmss")
    PS C:\> $Computer = Get-SCOMClass -Name 'microsoft.windows.computer' | Get-SCOMClassInstance | Where Name -match $Name
    PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $Computer -ExportCSVPath (Join-Path $OutFolder ("$($Name)_EffectiveConfig_$($Date).csv")) -SortBy WorkflowId
 
 
    .NOTES
    Author: Tyson Paul
    Original Date: 7-25-2014
    History
    2020.07.16: Added another example.
    2020.02.19: Found that override property "Enabled" value (true/false) is case-sensitive and therefore must be lower case for this report to run correctly. Added function to fix existing overrides if they are found while running this function/report. Ended up fixing Start-SCOMOverrideTool.ps1 script too.
    Clean-Name function: improved invalid/illegal character replacement strategy.
    2019.08.26: Major revision.
 
    .PARAMETER MonitoringObject
    Accepts one or more SCOM Monitoring Objects (use Get-SCOMClassInstance to retrieve a monitoring object).
    .PARAMETER ExportCSVPath
    Path/folder to export CSV report(s).
    .PARAMETER ExportHTMLPath
    Path/folder to export HTML report(s).
    .PARAMETER HTMLFormat
    There are two options: Basic and Fancy. Both report styles include a text filter powered by Javascript.
    .PARAMETER IndividualFiles
    Will also output individual report files for each object that may be hosted on or contained by the MonitoringObject(s).
    .PARAMETER SortBy
    Report will be sorted by this column/property.
    .PARAMETER SortOrder
    Ascending or Descending. Default is Ascending.
 
#>

Function Export-SCOMEffectiveMonitoringConfigurationReport {
  [CmdletBinding(DefaultParameterSetName='P1',
      SupportsShouldProcess=$true,
  PositionalBinding=$false)]

  Param
  (

    #Any valid monitoring object(s) (class instance or group)
    [Parameter(Mandatory=$true, ParameterSetName = "P1",
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=0)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject[]]$MonitoringObject,


    #Directory to export CSV fil
    [Parameter(Mandatory=$false, ParameterSetName = "P1",
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=1)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$ExportCSVPath,


    #Directory to export HTML file
    [Parameter(Mandatory=$false, ParameterSetName='P1',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=2)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$ExportHTMLPath,


    #Formatting of HTML export
    [Parameter(Mandatory=$false, ParameterSetName='P1',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=3)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Fancy", "Basic")]
    [string]$HTMLFormat = "Basic",

    #Report rows will be sorted by this Workflow property
    [Parameter(Mandatory=$false, ParameterSetName='P1',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=4)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Index",
        "EntityName",
        "EntityId",
        "Path",
        "ClassName",
        "ClassId",
        "WorkflowName",
        "WorkflowDisplayName",
        "WorkflowType",
        "WorkflowId",
        "WorkflowMPName",
        "Enabled",
        "GeneratesAlert",
        "AlertSeverity",
        "AlertPriority",
        "Overridden",
        "Description",
        "Parameters",
    "Overrides")]
    [string]$SortBy = "WorkflowDisplayName",

    #Formatting of HTML export
    [Parameter(Mandatory=$false, ParameterSetName='P1',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=5)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Ascending", "Descending")]
    [string]$SortOrder = 'Ascending',

    # Will output individual csv or html files for all individual objects in addition to the merged/master file
    [switch]$IndividualFiles

  )

  $TESTING = $FALSE
  <# UNCOMMENT FOR TESTING
      # This will avoid loading Mons/Rules every time while testing.
      WRITE-HOST 'TESTING' -F RED -B YELLOW
      $TESTING = $TRUE
  #>


  ###################################################################
  # Will clean up names/strings with special characters (like URLs and Network paths)
  Function Clean-Name {
    Param(
      [string]$uglyString
    )
    # Remove problematic characters and leading/trailing spaces
    #$prettyString = (($uglyString.Replace(':','_')).Replace('/','_')).Replace('\','_').Trim()
    $prettyString = $uglyString.Split([IO.Path]::GetInvalidFileNameChars()) -join '_'

    # If the string has been modified, output that info
    If ($uglyString -ne $prettyString) {
      Write-Verbose "There was a small problem with the characters in this parameter: [$($uglyString)]..."
      Write-Verbose "Original Name:`t`t$($uglyString)"
      Write-Verbose "Modified Name:`t`t$($prettyString)"
    }

    Return $prettyString
    #>
  }
  ########################################################################################################
  
  Function Build-HTMLBasic {
  
    [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
        SupportsShouldProcess=$true, 
    PositionalBinding=$false)]
    Param (
      #array of workflows to be exported to the Path
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=0,
      ParameterSetName='Parameter Set 1')]
      [System.Object[]]$arrWFs,

      #title of HTML doc
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=2,
      ParameterSetName='Parameter Set 1')]
      [string]$Title = ""
    )


    $HTML = ''
    $HTML += @"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>$Title</title>
    </head>
    <body>
        <style type="text/css">
 
a { text-decoration: none; }
a:link, a:visited {
    color: blue;
}
a:hover {
    color: red;
}
.trrow {}
.tg {border-collapse:collapse;border-spacing:1;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:6px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .defaultstyle{border-color:inherit;vertical-align:center;text-align:left}
.tg .headerstyle{font-weight:bold;font-size:100%;border-color:inherit;vertical-align:center}
.tg .columnNameStyle{border-color:inherit;vertical-align:center;text-align:center}
 
 
.tg .classnamestyle{font-weight:bold;border-color:inherit;vertical-align:center}
.tg .keystyle{font-weight:bold;color:#fe0000;border-color:inherit;text-align:right;vertical-align:center}
.tg .keypropertystyle{font-weight:bold;color:#fe0000;border-color:inherit;vertical-align:center}
.tg .propertystyle{color:#22771a;border-color:inherit;vertical-align:center;horizontal-align:center}
propertystyle
@media screen and (max-width: 767px)
 
{
    .tg {width: auto !important;}
    .tg col {width: auto !important;}
    .tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;}
}
 
#myInput {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAACpF6WWAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAACYktHRAD/h4/MvwAAAAl2cEFnAAABKgAAASkAUBZlMQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxMy0wNC0xMFQwNjo1OTowNy0wNzowMI5BiVEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTMtMDQtMTBUMDY6NTk6MDctMDc6MDD/HDHtAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0VGl0bGUAc2VhcmNoLWljb27Cg+x9AAACKklEQVQ4T6WUSavqQBCFK+2sII7gShFXLpUsBBHFf+1KcAQFwaWiolsnnBDn++4p0iHRqPDuByFJd/Wp6qrqVn5+IQP3+52m0ymtVis6Ho885na7KRgMUiKR4O9vmEQHgwGNx2NyOp0khCBFUXgcJo/Hg67XK8ViMcpkMjz+Dl200+nQZrMhh8PBE4gYQgDidrudvzEOm2KxyP9WsCginM1mHKEUS6VSFA6HOWI4G41GPAfx2+1GgUCAVFXVZMwovwY/lUqFPB4PiyFn+XxemzbT6/VovV6z8Ol0olwux+LPCBQFEQKIvhME2WyWbWGHFCD/VghUGVvE1rDlb6TTabbFmuVyqY2aEWgbFALeI5GINvyeUCjEtlgju+IZoRWfkS30CURoxFJUNjMEt9stf38CNjJKIFvNiMBJgTebzcZt843hcMhCELWqPBDxeJwulwtvC/3X7/e1qVfgFD0rC5tMJrUZM8Lr9VI0GmVBRDCfz6nZbHI/Sna7HXW7XZpMJtxSiBIP1lmhH9NqtaqfGKQDTmQREBnSgwfmMqfYYblc1o+2xHShtNttLgSiee4EmMEp3hDBPJzikimVSuRyuTTLJ1GwWCz4pCB3UhiL/X4/Hw50C5zjLSM+n898weCogxdRIzAGxigAdtNqtV6EC4UC+Xy+z6Kf2O/31Gg0TMK4ZBDxf4uCw+FA9XpdF0aaUOg/iQLcHbVaTb/p0Cl/FgXIJ/oYnaCqKv0DC6dltH6Ks84AAAAASUVORK5CYII=); /* Add a search icon to input */
    background-position: 10px 12px; /* Position the search icon */
    background-repeat: no-repeat; /* Do not repeat the icon image */
    width: 100%; /* Full-width */
    font-size: 16px; /* Increase font-size */
    padding: 12px 20px 12px 40px; /* Add some padding */
    border: 1px solid #ddd; /* Add a grey border */
    margin-bottom: 6px; /* Add some space below the input */
}
 
#myTable tr {
    /* Add a bottom border to all table rows */
    border-bottom: 1px solid #ddd;
     
}
 
#myTable tr.header, #myTable tr:hover {
    /* Add a grey background color to the table header and on hover */
    background-color: #f1f1f1;
}
 
        </style>
 
<script>
function myFunction() {
  // Declare variables
  var input, filter, table, tr, td1,td2,td3,td4,td5,td6,td7,td8,td9, i;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("myTable");
  //tr = table.getElementsByTagName("tr");
  tr = table.getElementsByClassName("trrow");
 
  // Loop through all table rows, and hide those who don't match the search query
  for (i = 0; i < tr.length; i++) {
   td1 = tr[i].getElementsByClassName("defaultstyle")[1];
     td2 = tr[i].getElementsByClassName("defaultstyle")[2];
     td3 = tr[i].getElementsByClassName("defaultstyle")[3];
     td4 = tr[i].getElementsByClassName("defaultstyle")[4];
     td5 = tr[i].getElementsByClassName("defaultstyle")[5];
     td6 = tr[i].getElementsByClassName("defaultstyle")[6];
     td7 = tr[i].getElementsByClassName("defaultstyle")[7];
     td8 = tr[i].getElementsByClassName("defaultstyle")[8];
     td9 = tr[i].getElementsByClassName("defaultstyle")[9];
    if (td1) {
      if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td3.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1) || (td7.innerHTML.toUpperCase().indexOf(filter) > -1) || (td8.innerHTML.toUpperCase().indexOf(filter) > -1) || (td9.innerHTML.toUpperCase().indexOf(filter) > -1) ) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}
</script>
 
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for text..">
 
<div class="tg-wrap">
 
<table id="myTable" class="tg">
 
"@



    $headerHTML = ''
    $rows = ''
    ForEach ($header in $arrOrderedHeaders ) {
      $headerHTML += @"
    <th class="headerstyle">$($header)</th>
 
"@

    }
    $HTML += @"
  <tr>
$headerHTML
  </tr>
 
"@


    $i =1
    #Build rows
    ForEach ($WF in $arrWFs[0..($arrWFs.Count -1)]) {
      $percentComplete = "{0:N0}" -f (($i/([math]::Max(($arrWFs.Count),1 )) ) *100)
      Write-Progress -Activity "Building HTML report headers" -Status "$($percentComplete) percent complete." -PercentComplete $percentComplete 
      $rows += @"
  <tr class="trrow">
 
"@

      ForEach ($Header in $arrOrderedHeaders) {
        If ($Header -eq 'Index') {
          $rows += @"
    <td class="defaultstyle">$($i)</td>
 
"@
      
        }
        Else{
          $rows += @"
    <td class="defaultstyle">$($WF.$Header)</td>
 
"@

        }
      }#end ForEach Header

      $rows += @"
  </tr>
 
"@

      $i++
    }#end ForEach WF
    Write-Progress -Activity "Building HTML report" -Status "$($percentComplete) percent complete." -PercentComplete 100 -Completed
    $HTML += @"
$rows
</table>
</div>
</body>
</html>
"@


    Return $HTML

  }# End Build-HTMLBasic
  ###################################################################
  Function Build-HTMLFancyResize {
    [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
        SupportsShouldProcess=$true, 
    PositionalBinding=$false)]
    Param (
      #array of workflows to be exported to the Path
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=0,
      ParameterSetName='Parameter Set 1')]
      [System.Object[]]$arrWFs,

      #title of HTML doc
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=2,
      ParameterSetName='Parameter Set 1')]
      [string]$Title = ""
    )  


    $HTML = ''
    $HTML += @"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>$Title</title>
    </head>
 
<style type="text/css">
 
.trrow {}
 
td{
    text-indent:5px;
    color:#444;
    border-bottom:1px solid #bbb;
    border-left:1px solid #bbb;
}
"@


  


    $headerHTML = @"
    <tr>
 
"@

    $rows = ''
    $CSSColumns =''
    [int]$pxFactor = 8
    $maxColumnSize = (50 * $pxFactor)
    #Build CSS style for each column and Header HTML
    ForEach ($header in $arrOrderedHeaders ) {
      #CSS
      #header name width in px
      Try{
        #[int]$headerNamePx = [System.Math]::Max(($header.Length * $pxFactor), (($arrAllWorkflows.$header | Measure-Object -Maximum).Maximum.ToString().Length * $pxFactor) )
        [int]$headerNamePx = [System.Math]::Max(($header.Length * $pxFactor), ([System.Math]::Min($maxColumnSize, (($arrWFs.$header | Where-Object Length -gt 0 | % {$_.Length} ) | Measure-Object -Maximum ).Maximum * $pxFactor ))  )
      } Catch {
        [int]$headerNamePx = ($header.length * $pxFactor)
      }

      $CSSColumns += @"
td.$($header), th.$($header) {
    text-align: left;
    width:$($headerNamePx)px;
    min-width:$([math]::Min(30,($headerNamePx /4)))px;
}
"@


      #Header HTML
      $headerHTML += @"
      <th class="$header">$($header)</th>
 
"@


    }#end ForEach $header
    
    $headerHTML += @"
  </tr>
 
"@


    $HTML += @"
  $CSSColumns
 
"@


    $HTML += @'
 
td.Overrides, th.Overrides{
    border-right:1px solid #2e638e;
}
 
td.bottom{
    border-bottom:1px solid #2e638e;
}
 
td.disabled, th.disabled{
    width:25px;
    text-align: center;
    min-width:25px;
}
 
#myInput {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAACpF6WWAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAACYktHRAD/h4/MvwAAAAl2cEFnAAABKgAAASkAUBZlMQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxMy0wNC0xMFQwNjo1OTowNy0wNzowMI5BiVEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTMtMDQtMTBUMDY6NTk6MDctMDc6MDD/HDHtAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0VGl0bGUAc2VhcmNoLWljb27Cg+x9AAACKklEQVQ4T6WUSavqQBCFK+2sII7gShFXLpUsBBHFf+1KcAQFwaWiolsnnBDn++4p0iHRqPDuByFJd/Wp6qrqVn5+IQP3+52m0ymtVis6Ho885na7KRgMUiKR4O9vmEQHgwGNx2NyOp0khCBFUXgcJo/Hg67XK8ViMcpkMjz+Dl200+nQZrMhh8PBE4gYQgDidrudvzEOm2KxyP9WsCginM1mHKEUS6VSFA6HOWI4G41GPAfx2+1GgUCAVFXVZMwovwY/lUqFPB4PiyFn+XxemzbT6/VovV6z8Ol0olwux+LPCBQFEQKIvhME2WyWbWGHFCD/VghUGVvE1rDlb6TTabbFmuVyqY2aEWgbFALeI5GINvyeUCjEtlgju+IZoRWfkS30CURoxFJUNjMEt9stf38CNjJKIFvNiMBJgTebzcZt843hcMhCELWqPBDxeJwulwtvC/3X7/e1qVfgFD0rC5tMJrUZM8Lr9VI0GmVBRDCfz6nZbHI/Sna7HXW7XZpMJtxSiBIP1lmhH9NqtaqfGKQDTmQREBnSgwfmMqfYYblc1o+2xHShtNttLgSiee4EmMEp3hDBPJzikimVSuRyuTTLJ1GwWCz4pCB3UhiL/X4/Hw50C5zjLSM+n898weCogxdRIzAGxigAdtNqtV6EC4UC+Xy+z6Kf2O/31Gg0TMK4ZBDxf4uCw+FA9XpdF0aaUOg/iQLcHbVaTb/p0Cl/FgXIJ/oYnaCqKv0DC6dltH6Ks84AAAAASUVORK5CYII=); /* Add a search icon to input */
    background-position: 10px 12px; /* Position the search icon */
    background-repeat: no-repeat; /* Do not repeat the icon image */
    width: 100%; /* Full-width */
    font-size: 16px; /* Increase font-size */
    padding: 12px 20px 12px 40px; /* Add some padding */
    border: 1px solid #ddd; /* Add a grey border */
    margin-bottom: 6px; /* Add some space below the input */
}
 
#normal tr:hover {
    /* Add a grey background color to the table header and on hover */
    background-color: #f1f1f1;
}
 
 
body{
    background-color: white;
    text-align:center;
    padding:55px;
}
 
 
.center{
    text-align:left;
}
 
#table{
    width:100%;
    max-width:1600px;
}
 
th{
 background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABkAAAAAeCAMAAABDhiIoAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAwBQTFRFAAAA////nMHblrzTjrbQhq/Ne6bJZ5rJYZjOWYOpSovFKlZ5DVeUCFiZB1eWBVebA1idAFifcLX7b7H2bK3vaajmZaDaY5zUYpnQA2W+A2a+A2W9BWW6BWO2DWOwVKTuUp/mUJreTpXWTZLQS47KSozHdrfyeLn0dbXva6Xbc7HpcKvhaqDRaJ3NZ5zMZ5vLZZnHZpnIZJbEYpPAYJC7XYu0W4euWoSri8P4iL7xhLnpgLLge6vXd6TOeafRdqPMpMvuo8ntpszuAGbAAGa/AGW+AGS7AGO5AGK3AGG0AF+wAF6tAFyoAFumAFqjAFmiAFmhAWW9AWS6AWK2AV2qAmGzA2a8A2W7A2S4A2O2A2K0A2CvA1ynA1ukA1qiA1mgA1mfBF6sBWW5BWS3BWK0BWGyBWGxBWCvBVulBma5BmW4BmW2BmOyBmGuBVmfBVidB2S0Bl6pBluhBlqeBlmcBleaB2SyB2KwB2KvB2CsB2CqB1+oB12lB1yjBleZCGW2CGS1CGS0CGOyCGGtB1ugB1iaCFqdCmW0Cma0CmSxCmOvC2azC2WyCmGpCl+lCVeYCVeWC2KsCludClqbClmZCliXC12iC1yfDWWyDWSwC1qaC1mYC1eWC1eVDWSvDWOuDWKtDWKrDWGqDWCpDWCnDFmaDV+lDV2iDVyfDVudDVqbDVqaDVmYDViXDVeWDleULmOOLWCKK1l+N2yXNmmUNWaPM2OKMmCGSICtTIa1VZLEa7HqdsD9dL35kcfzjcHriLrjhbXcgrDWfqrOfKfLAVidBFibBlmbB1iZDFiVLF2EKld6MV6CMl+DMV6BMV2Af63Rk8LlkL7gjrrbiLLRh7DPqNDwntP5ms7yl8ntlcbpirbVjLjXibTTqdz+oNDwp9j5pNT0lsHelL7akLjTncvpmcbjkrzXkbrVj7jSj7fRmsHZl73Vq9XwocnioMfgnsXencPcmL7Vps/os+L9nMTcmcDXqdTso8zksd32rtnyteH3uOb6vOv9w/P+////BZXfmQAAAQB0Uk5T////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AFP3ByUAAAUwSURBVHja7J3NayRFGMaftzM9+ZhETDZmWbInBQ+i91UCLiz4kYvr5qx48uTRgysoKAh7WcSrBxFE8LKC6EZccXEh3gUv+wcYVt2PrEl6MvZMpjxUVVd1T013PM2UPD+Gmaq3qnuaruZ95q2vkc6XqEfC5jN3zgSsf4TroonzIIQQEhetb155IiQVt/XHU8rkE09RlATSvjUkPFJ7GUO2BCGExCYgwK8NVZSoUsZmnX1Y0pADAMCf7pi3mrXiHFuCEELiExAVVg1nV75aqGERUSgrFwsAgEFrAABopwC6j3eLwOOaH3+EI5FltgQhhMQnIEc1xalz/LaL6nDpwCgGAAzaAAZACqRIu4AAAwGS41kMWksmEOnoSoa8nVe/R9gShBASn4D8NWpNhknJvbvxjSGwD+zrUGPE/Q9KlhZ2MQAg2PeK0kKyBLAnpoAQQkiEAvJPbQUJC4kA6LvoRfzi+aN5FD1jWomOGnSCAkIIIREKyJ1xTl1BlH6rFoRVxab2sPeIMR2cUCUoIIQQEp2ACN4d79YVarWjIVvuCFN1l/EsW4IQQuKLQPZKXr6Ym5sMSwKQ1IYM3tzcmeOZYEWpCzZusyUIISQ+Abk7Lvw4+drA1XtYvbcK3B9fv76Til1YhBASG/Lk7mpjnVohqQpNeGn6yDnK7LIlCCEktgjkPi6PyELaTwuTACqxYyECINGrDMX0agkAUYBgD1ixaSRQlTlblrNaMdYL0VjfBV5nSxBCSGwC8tVFGyI4Y98mzJCIMgs2RNuMJLhREW1advUFQ0FYP4DfzwJYjyD6+CKSNnyNjzEhZBLIjxevBqw28REAfOAsYl9lCfGWA4rLSw7MIqwigWGPOKKQT6bkOrj5CyFkKgXE8+0VBfEExFOQh95hK94Zikim60pgO7HwW52akAae5i0ghExaQB7rvmeSMwFvfuwZimDC7WqF03YflLnyoX2bMwfl/vGwNrcn1mw0IqL4zBBCiPbxa9d/qnjIYjQk7adGQZx3H1GRwu3PeYZ+tTAsDnYzLDWl2tGPoglTPsWEkAkJSPb2iFuXko/XZDrd0bmuKSvvhSi+xCTjf7dfeedK5Ts/ZEv8V97nLSCETFhATh++UfuL1kQanUIhMmCtTkCQYcUubz/1QBds4WblSy7YxPbm9ub25rZf9jGbhRBCYhCQ7z6rxB+BZOqUBDmWIch0ZhFATycXvBlY2cgZRjqptkq5r0u5ebZLI5u8BYSQCdP6+3x5+qy/f2IxNtFPgZY3dq6kk1XP1F0YPbs7WfMwx6WX4rhjb07HZVxzyU/5GBNCJhKB3HjVrSh4IRQw3LCJF4si13cFPY/qUZO7u+j0RO+QMhgbgpT4vJT7fprv2Mt8aAghBABadifdDexsHGrbL8CGk5LnrOvPSkrQB9DOzZ8SPtQfB+iZyVhS7NFYmYg1RkfK5qnunuGSFUIIsQKi2QF2bFq5JLBhZlDZ2b26M0pSADly/b+2OYA2sAQc9IA5M+tKXPUiEVhGIVxaQQghMQqI+vnyqEcXuL6oHBlMNjOyoKdkdQEsIi9m+MKOfvd6wYgjCV/BrecBXGdLEEJIZMgPl84VquHbQ5lgd9RiuW9rHnZe1ti/kgp0At1iSxBCSGQUYYEav0mHuDoBDu3iQl16BLsoXQUPF+4GQggh/wtaCs+ES9qegmQdXwk6biV64D2DXUh4CsADYAtmGeGF8Zdxky1BCCGRId+epFKjrdoBVsqueWk/+kiGCfSLU5sIISQ6/gUAAP//AwB2PSXhkCthvAAAAABJRU5ErkJggg==);
   height:30px;
   background-repeat:no-repeat;
   color:white;
   text-shadow: #012b4d 2px 2px 2px;
   text-align: center;
}
 
 
.grip{
    width:20px;
    height:15px;
    margin-top:-3px;
    background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAhCAYAAACFtMg3AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAB/5JREFUeNrkmHlsFNcdxz/v7doYwxpfJXXMDWkogZYYis2ppJEohZaGEKVBRVEplEIE5bIxmHCl5QqUcAVRpa0UqaqaSk16KEENpiVpS0qQaRMCxCngQGhMYL3rvbzHzLzXP9bH2Ltmdw1SUPuTRnozO/OZ932/37557ytisZjGFieDmk4XAGVZxAyDcDhKOBgi2BLG1xzg0383sGPJY3QNpVR7O1Ln7/SbpUxi0SjRSJRIOEQkGsUyFS2hABcuneOp7cva75VSJrBPZJ3F3kONxlSKiBkjGI0SikYIxiK4Q0GuXW/ipYrlnZ4XdsEng5oVq/eCZYIVQyiDVFFS3I/f712TVHCkzs/uympMFIY2MYVKycvvX0zNy7uTij6RdZbpRw+AoXGGTZyR1LxBvQt5f86+Dl4nsVUHyTQa3T6+tfonCdcjdX72VW3ImNd8w832b1clF1t7KGPe1bCH0a+u7BA8dv5m3mw2E8ROmlxGrOkaABOnjE/atov+wtxqxi89RNl3NtN4JpQgdlzFRBrDTQCML5+UtG0XvejBpyn/wSHGfO8Z/iTPJYidNuYBsi94AZjy4KikbbvoXi/MZeSWXYhfn2/UVTUHKSwqBKVAW2CZaK0QykRrjRSgzFinEXc64m3TUigNXp+fh2bMZdb0UVTVHGS2uy+F6hJKK0wUWmtMLLTWIASmthBSdIx8K09ZirDO5w2XYNqsOG/11ufxPOEn0jcGlgWGRpoKaSgwFVJKdNRCSonQ8X9oVmvxxoQGE/KO5TCjYj7SCGTziyOb8DQ1tU4CEi0EkyaNJeq+ClpRMXEsMe8nKKWomFxGxH2VmGESM0yUUnib/Ty7rZLFCx+mjfeH4gBuMYy2+WVsRQXXWm6itKasvJzGFjdaacomTOQ/ITeWYWEZFiGVR21fwZadHbyX9j9L4W9c5PiyW2cezdTRI8k6F+/zlAdGkl3fhFKKyV+6H+e5m0SVSVSZEFPkHcvhuaqNLF74MEJrzc/fuqgH3lfCoqe3UZCf35plA7QCZcXPtY6fdwmvL8DGLZUMGzGER4rjKbPzZrpzKTIvYaJQWmGhUFqjhE5ghelHrUtS86PkvAWrtuJ5LECkTwtYGmHEsywMC6EFqLbxiLOFJcir7c22lRvbeRJg4bQR4tL5Bo4cqMbr9bbVGCAQQgCtpSdkF7FB1q6vpKS0o3Ndea8XhXA7hyKEQNqel1p0Eeui1iVZ80z3vBd3b6DwFRc5gRwQoIVAC0BKtJC2KbhV7PFcNi3d2InXfsvSR0aL+vc/4vD+arzNvrg46YyD2o4uYldUVdJ/wFC+MVCKrtmy844WteB2DkMADiRSJBPrZNm6qpS8n+6qoei3LnJCvcEp0E6JdgiQHRUjtIO847lUL9qYwOukYtXXvyzO/vMyh55f25FpIVuzTPu51x9k8YqVFN5TyhPDhejuk2DnvVEQ5EbWYKSQ7aKlFu1iF65elTbv8HOtogM5aIdAO+IZ1kIitCDvWA4/fGp9Ul7CUmb9o2XinX9cZN+etXh9PhCOTln2+vzMX7SMvIISFozOFqm+g3ZebUGE61mDOzIr+nLclcW8Jcsz5h3Yvo6i3+WRE+yFloDUCA39juWy6PGqbnlCa50UXP3LOj1u3AAqq/dQ0M+FUCYebzNPLlhO/5JBVE7pJTJZANh5D3ljuAzF0TzB49+/Pd7qjbtomu3DzI5R8Mccvvvo2lvyuhUMsOTFd/TUigGs37AXlGLmk0so6F/Kjul9BD0IO09aihnz7gyveusejN5h5nxtVWqe1vqWx4IjZ/Sv6j16yOw1euVrhk51/93Ok6lG8dNAmDMXJcWFeVzzBrjd+Kx5tyzp/8WQ/J+Fs7sfXr18E78/8MVoJPqB56anornxRsvOxXPe7emL7hae0zCMpI5Hfn4eSimcDgeWYSIMg441Zvdxt/PSdjw8vgBXXj+UVgfbHA//e0F2VNZgojC1iYUFQFNuLqelM21eW/tNRz2zXtkfdzwiJo5InOe6kYvzVFZaPGe3jocjG+3Itn2+/BmVXKTOz+GqDeRqM8HiydKCTCfLdsejjwJDo6SJU8Z5Zo7AkSavW8cDYFLFaIzr9Rl1bOz8zZxym+xat6nT9SzhpLx8coK7kQ7vRCDEzNeOdLpu9nYy6StjEtyNlCXdneMRz2rc9cCRjedmI1ePvpCyZF6+cF1X1RxkUJ8Cpl75EIcVz3DbIsfE4pM8F3+XMiNe7rA+XPxqA5Y0oLXK21yPwo9c9DrhSIvXreNhdz20SH/l18a7GvJwsnQElm1b2eZ6ZFLMbbyWy0GG/WUg0pIgdILrkfZ3eP6EQnGl/mN+dngDHtuWECE5eeoc2SX3J+yFbxV2XkO0mbdLRyCQaK058/ZJSnsXpzOZJuVFLkUY+tdBCCRoeOvsBWKjCrEyWE1073hoBWYMYYQhGsxoFO28yzEfp+4dDnbHQ5k95sXqoww5OQCBzfHQVuYrrZSOR4Zh5100A9R9/r52x6MnYeeZF0wGnR7Q4XhIkbngtB2PDMLO+9Dycab/cOQd4qn3TAbWlcTtHYfsmeBbOh49DDvvA4K8WzwMh74zPP6luffsPRlNqknf/ON5E8SpU1fYs3MN3ubmHpV0d7zzIkhDfvEd4zlOC4oaCm5/t7Rr/jjx579dYce2VXh9/oxm1lS8j6PhO8qLNsTS56XrKAz+5qrbdifuBl7ajsfnivLvyH70s+b9dwCwRlJdGtc2+wAAAABJRU5ErkJggg==);
    margin-left:-5px;
    position:relative;
    z-index:88;
    cursor:e-resize;
}
 
.grip:hover{
    background-position-x:-20px;
}
.JCLRLastGrip .grip{
    background-position-y:-18px;
    left:-2px;
}
 
.dragging .grip{
    background-position-x:-40px;
}
 
.sampleText{
    position:relative;
    width:100%;
}
 
.dotted{
    background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAECAYAAABCxiV9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAADdJREFUeNpi/P//PwMy4IuYWsfAwMDxaUV2FRMDJvBiYGDwZmBgYGBE14kMmBjwAAAAAAD//wMAlfQKloYQcBIAAAAASUVORK5CYII=);
    background-repeat:repeat-y;
}
 
input.check{
     
}
 
#sample2Txt{
    float:right;
}
label{
    color:#0361ae;
}
      .scroll{
          overflow:scroll;
          max-width:100%;
          position:relative;
      }
  </style>
  <script type="text/javascript">
/*!
 * jQuery JavaScript Library v1.6.2
 * http://jquery.com/
 *
 * Copyright 2011, John Resig
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 * Copyright 2011, The Dojo Foundation
 * Released under the MIT, BSD, and GPL Licenses.
 *
 * Date: Thu Jun 30 14:16:56 2011 -0400
 */
(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bZ(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bY(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bC.test(a)?d(a,e):bY(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bY(a+"["+e+"]",b[e],c,d);else d(a,b)}function bX(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bR,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bX(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bX(a,c,d,e,"*",g));return l}function bW(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bN),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bA(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bv:bw;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bg(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(H)return H.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),h&&f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,l.done(m);m();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,u=/\:|^on/,v,w;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(o);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(o);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(n," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h<i;h++){var j=e[h];if(j.selected&&(f.support.optDisabled?!j.disabled:j.getAttribute("disabled")===null)&&(!j.parentNode.disabled||!f.nodeName(j.parentNode,"optgroup"))){b=f(j).val();if(g)return b;d.push(b)}}if(g&&!d.length&&e.length)return f(e[c]).val();return d},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.
shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget,c=!1,d=a.type;a.type=a.data,b!==this&&(b&&(c=f.contains(this,b)),c||(f.event.handle.apply(this,arguments),a.type=d))},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){if(a===b){g=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be=/^\s*<!(?:\[CDATA\[|\-\-)/,bf={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bg(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bm)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i;b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j
)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1></$2>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bl(k[i]);else bl(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bn=/alpha\([^)]*\)/i,bo=/opacity=([^)]*)/,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c)),h="number"),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bA(a,b,d);f.swap(a,bu,function(){e=bA(a,b,d)});return e}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cs(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cr("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cr("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=cs(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block"))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],cj.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=ck.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[i]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cr("show",1),slideUp:cr("hide",1),slideToggle:cr("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=cn||cp(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cl&&(co?(cl=!0,g=function(){cl&&(co(g),e.tick())},co(g)):cl=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=cn||cp(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b<a.length;++b)a[b]()||a.splice(b--,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cl),cl=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var ct=/^t(?:able|d|h)$/i,cu=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cv(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!ct.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window);
  </script>
   
   
  <script type="text/javascript">
/**
               _ _____ _ _ _
              | | __ \ (_) | | | |
      ___ ___ | | |__) |___ ___ _ ______ _| |__ | | ___
     / __/ _ \| | _ // _ \/ __| |_ / _` | '_ \| |/ _ \
    | (_| (_) | | | \ \ __/\__ \ |/ / (_| | |_) | | __/
     \___\___/|_|_| \_\___||___/_/___\__,_|_.__/|_|\___|
      
    v1.6 - jQuery plugin created by Alvaro Prieto Lauroba
     
    Licences: MIT & GPL
    Feel free to use or modify this plugin as far as my full name is kept
     
    If you are going to use this plug-in in production environments it is
    strongly recommended to use its minified version: colResizable.min.js
 
*/
 
(function($){
     
    var d = $(document); //window object
    var h = $("head"); //head object
    var drag = null; //reference to the current grip that is being dragged
    var tables = {}; //object of the already processed tables (table.id as key)
    var count = 0; //internal count to create unique IDs when needed.
     
    //common strings for packing
    var ID = "id";
    var PX = "px";
    var SIGNATURE ="JColResizer";
    var FLEX = "JCLRFlex";
     
    //short-cuts
    var I = parseInt;
    var M = Math;
    var ie = navigator.userAgent.indexOf('Trident/4.0')>0;
    var S;
    try{S = sessionStorage;}catch(e){} //Firefox crashes when executed as local file system
     
    //append required CSS rules
    h.append("<style type='text/css'> .JColResizer{table-layout:fixed;} .JColResizer > tbody > tr > td, .JColResizer > tbody > tr > th{overflow:hidden;padding-left:0!important; padding-right:0!important;} .JCLRgrips{ height:0px; position:relative;} .JCLRgrip{margin-left:-5px; position:absolute; z-index:5; } .JCLRgrip .JColResizer{position:absolute;background-color:red;filter:alpha(opacity=1);opacity:0;width:10px;height:100%;cursor: e-resize;top:0px} .JCLRLastGrip{position:absolute; width:1px; } .JCLRgripDrag{ border-left:1px dotted black; } .JCLRFlex{width:auto!important;} .JCLRgrip.JCLRdisabledGrip .JColResizer{cursor:default; display:none;}</style>");
 
     
    /**
     * Function to allow column resizing for table objects. It is the starting point to apply the plugin.
     * @param {DOM node} tb - reference to the DOM table object to be enhanced
     * @param {Object} options - some customization values
     */
    var init = function( tb, options){
        var t = $(tb); //the table object is wrapped
        t.opt = options; //each table has its own options available at anytime
        t.mode = options.resizeMode; //shortcuts
        t.dc = t.opt.disabledColumns;
        if(t.opt.disable) return destroy(t); //the user is asking to destroy a previously colResized table
        var id = t.id = t.attr(ID) || SIGNATURE+count++; //its id is obtained, if null new one is generated
        t.p = t.opt.postbackSafe; //short-cut to detect postback safe
        if(!t.is("table") || tables[id] && !t.opt.partialRefresh) return; //if the object is not a table or if it was already processed then it is ignored.
        if (t.opt.hoverCursor !== 'e-resize') h.append("<style type='text/css'>.JCLRgrip .JColResizer:hover{cursor:"+ t.opt.hoverCursor +"!important}</style>"); //if hoverCursor has been set, append the style
        t.addClass(SIGNATURE).attr(ID, id).before('<div class="JCLRgrips"/>'); //the grips container object is added. Signature class forces table rendering in fixed-layout mode to prevent column's min-width
        t.g = []; t.c = []; t.w = t.width(); t.gc = t.prev(); t.f=t.opt.fixed; //t.c and t.g are arrays of columns and grips respectively
        if(options.marginLeft) t.gc.css("marginLeft", options.marginLeft); //if the table contains margins, it must be specified
        if(options.marginRight) t.gc.css("marginRight", options.marginRight); //since there is no (direct) way to obtain margin values in its original units (%, em, ...)
        t.cs = I(ie? tb.cellSpacing || tb.currentStyle.borderSpacing :t.css('border-spacing'))||2; //table cellspacing (not even jQuery is fully cross-browser)
        t.b = I(ie? tb.border || tb.currentStyle.borderLeftWidth :t.css('border-left-width'))||1; //outer border width (again cross-browser issues)
        // if(!(tb.style.width || tb.width)) t.width(t.width()); //I am not an IE fan at all, but it is a pity that only IE has the currentStyle attribute working as expected. For this reason I can not check easily if the table has an explicit width or if it is rendered as "auto"
        tables[id] = t; //the table object is stored using its id as key
        createGrips(t); //grips are created
     
    };
 
 
    /**
     * This function allows to remove any enhancements performed by this plugin on a previously processed table.
     * @param {jQuery ref} t - table object
     */
    var destroy = function(t){
        var id=t.attr(ID), t=tables[id]; //its table object is found
        if(!t||!t.is("table")) return; //if none, then it wasn't processed
        t.removeClass(SIGNATURE+" "+FLEX).gc.remove(); //class and grips are removed
        delete tables[id]; //clean up data
    };
 
 
    /**
     * Function to create all the grips associated with the table given by parameters
     * @param {jQuery ref} t - table object
     */
    var createGrips = function(t){
     
        var th = t.find(">thead>tr:first>th,>thead>tr:first>td"); //table headers are obtained
        if(!th.length) th = t.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td"); //but headers can also be included in different ways
        th = th.filter(":visible"); //filter invisible columns
        t.cg = t.find("col"); //a table can also contain a colgroup with col elements
        t.ln = th.length; //table length is stored
        if(t.p && S && S[t.id])memento(t,th); //if 'postbackSafe' is enabled and there is data for the current table, its coloumn layout is restored
        th.each(function(i){ //iterate through the table column headers
            var c = $(this); //jquery wrap for the current column
            var dc = t.dc.indexOf(i)!=-1; //is this a disabled column?
            var g = $(t.gc.append('<div class="JCLRgrip"></div>')[0].lastChild); //add the visual node to be used as grip
            g.append(dc ? "": t.opt.gripInnerHtml).append('<div class="'+SIGNATURE+'"></div>');
            if(i == t.ln-1){ //if the current grip is the las one
                g.addClass("JCLRLastGrip"); //add a different css class to stlye it in a different way if needed
                if(t.f) g.html(""); //if the table resizing mode is set to fixed, the last grip is removed since table with can not change
            }
            g.bind('touchstart mousedown', onGripMouseDown); //bind the mousedown event to start dragging
             
            if (!dc){
                //if normal column bind the mousedown event to start dragging, if disabled then apply its css class
                g.removeClass('JCLRdisabledGrip').bind('touchstart mousedown', onGripMouseDown);
            }else{
                g.addClass('JCLRdisabledGrip');
            }
 
            g.t = t; g.i = i; g.c = c; c.w =c.width(); //some values are stored in the grip's node data as shortcut
            t.g.push(g); t.c.push(c); //the current grip and column are added to its table object
            c.width(c.w).removeAttr("width"); //the width of the column is converted into pixel-based measurements
            g.data(SIGNATURE, {i:i, t:t.attr(ID), last: i == t.ln-1}); //grip index and its table name are stored in the HTML
        });
        t.cg.removeAttr("width"); //remove the width attribute from elements in the colgroup
 
        t.find('td, th').not(th).not('table th, table td').each(function(){
            $(this).removeAttr('width'); //the width attribute is removed from all table cells which are not nested in other tables and dont belong to the header
        });
        if(!t.f){
            t.removeAttr('width').addClass(FLEX); //if not fixed, let the table grow as needed
        }
        syncGrips(t); //the grips are positioned according to the current table layout
        //there is a small problem, some cells in the table could contain dimension values interfering with the
        //width value set by this plugin. Those values are removed
         
    };
     
     
    /**
     * Function to allow the persistence of columns dimensions after a browser postback. It is based in
     * the HTML5 sessionStorage object, which can be emulated for older browsers using sessionstorage.js
     * @param {jQuery ref} t - table object
     * @param {jQuery ref} th - reference to the first row elements (only set in deserialization)
     */
    var memento = function(t, th){
        var w,m=0,i=0,aux =[],tw;
        if(th){ //in deserialization mode (after a postback)
            t.cg.removeAttr("width");
            if(t.opt.flush){ S[t.id] =""; return;} //if flush is activated, stored data is removed
            w = S[t.id].split(";"); //column widths is obtained
            tw = w[t.ln+1];
            if(!t.f && tw){ //if not fixed and table width data available its size is restored
                t.width(tw*=1);
                if(t.opt.overflow) { //if overfolw flag is set, restore table width also as table min-width
                    t.css('min-width', tw + PX);
                    t.w = tw;
                }
            }
            for(;i<t.ln;i++){ //for each column
                aux.push(100*w[i]/w[t.ln]+"%"); //width is stored in an array since it will be required again a couple of lines ahead
                th.eq(i).css("width", aux[i] ); //each column width in % is restored
            }
            for(i=0;i<t.ln;i++)
                t.cg.eq(i).css("width", aux[i]); //this code is required in order to create an inline CSS rule with higher precedence than an existing CSS class in the "col" elements
        }else{ //in serialization mode (after resizing a column)
            S[t.id] =""; //clean up previous data
            for(;i < t.c.length; i++){ //iterate through columns
                w = t.c[i].width(); //width is obtained
                S[t.id] += w+";"; //width is appended to the sessionStorage object using ID as key
                m+=w; //carriage is updated to obtain the full size used by columns
            }
            S[t.id]+=m; //the last item of the serialized string is the table's active area (width),
                                                //to be able to obtain % width value of each columns while deserializing
            if(!t.f) S[t.id] += ";"+t.width(); //if not fixed, table width is stored
        }
    };
     
     
    /**
     * Function that places each grip in the correct position according to the current table layout
     * @param {jQuery ref} t - table object
     */
    var syncGrips = function (t){
        t.gc.width(t.w); //the grip's container width is updated
        for(var i=0; i<t.ln; i++){ //for each column
            var c = t.c[i];
            t.g[i].css({ //height and position of the grip is updated according to the table layout
                left: c.offset().left - t.offset().left + c.outerWidth(false) + t.cs / 2 + PX,
                height: t.opt.headerOnly? t.c[0].outerHeight(false) : t.outerHeight(false)
            });
        }
    };
     
     
     
    /**
    * This function updates column's width according to the horizontal position increment of the grip being
    * dragged. The function can be called while dragging if liveDragging is enabled and also from the onGripDragOver
    * event handler to synchronize grip's position with their related columns.
    * @param {jQuery ref} t - table object
    * @param {number} i - index of the grip being dragged
    * @param {bool} isOver - to identify when the function is being called from the onGripDragOver event
    */
    var syncCols = function(t,i,isOver){
        var inc = drag.x-drag.l, c = t.c[i], c2 = t.c[i+1];
        var w = c.w + inc; var w2= c2.w- inc; //their new width is obtained
        c.width( w + PX);
        t.cg.eq(i).width( w + PX);
        if(t.f){ //if fixed mode
            c2.width(w2 + PX);
            t.cg.eq(i+1).width( w2 + PX);
        }else if(t.opt.overflow) { //if overflow is set, incriment min-width to force overflow
            t.css('min-width', t.w + inc);
        }
        if(isOver){
            c.w=w;
            c2.w= t.f ? w2 : c2.w;
        }
    };
 
     
    /**
    * This function updates all columns width according to its real width. It must be taken into account that the
    * sum of all columns can exceed the table width in some cases (if fixed is set to false and table has some kind
    * of max-width).
    * @param {jQuery ref} t - table object
    */
    var applyBounds = function(t){
        var w = $.map(t.c, function(c){ //obtain real widths
            return c.width();
        });
        t.width(t.w = t.width()).removeClass(FLEX); //prevent table width changes
        $.each(t.c, function(i,c){
            c.width(w[i]).w = w[i]; //set column widths applying bounds (table's max-width)
        });
        t.addClass(FLEX); //allow table width changes
    };
     
     
    /**
     * Event handler used while dragging a grip. It checks if the next grip's position is valid and updates it.
     * @param {event} e - mousemove event binded to the window object
     */
    var onGripDrag = function(e){
        if(!drag) return;
        var t = drag.t; //table object reference
        var oe = e.originalEvent.touches;
        var ox = oe ? oe[0].pageX : e.pageX; //original position (touch or mouse)
        var x = ox - drag.ox + drag.l; //next position according to horizontal mouse position increment
        var mw = t.opt.minWidth, i = drag.i ; //cell's min width
        var l = t.cs*1.5 + mw + t.b;
        var last = i == t.ln-1; //check if it is the last column's grip (usually hidden)
        var min = i? t.g[i-1].position().left+t.cs+mw: l; //min position according to the contiguous cells
        var max = t.f ? //fixed mode?
            i == t.ln-1?
                t.w-l:
                t.g[i+1].position().left-t.cs-mw:
            Infinity; //max position according to the contiguous cells
        x = M.max(min, M.min(max, x)); //apply bounding
        drag.x = x; drag.css("left", x + PX); //apply position increment
        if(last){ //if it is the last grip
            var c = t.c[drag.i]; //width of the last column is obtained
            drag.w = c.w + x- drag.l;
        }
        if(t.opt.liveDrag){ //if liveDrag is enabled
            if(last){
                c.width(drag.w);
                if(!t.f && t.opt.overflow){ //if overflow is set, incriment min-width to force overflow
                   t.css('min-width', t.w + x - drag.l);
                }else {
                    t.w = t.width();
                }
            }else{
                syncCols(t,i); //columns are synchronized
            }
            syncGrips(t);
            var cb = t.opt.onDrag; //check if there is an onDrag callback
            if (cb) { e.currentTarget = t[0]; cb(e); } //if any, it is fired
        }
        return false; //prevent text selection while dragging
    };
     
 
    /**
     * Event handler fired when the dragging is over, updating table layout
     * @param {event} e - grip's drag over event
     */
    var onGripDragOver = function(e){
         
        d.unbind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE).unbind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE);
        $("head :last-child").remove(); //remove the dragging cursor style
        if(!drag) return;
        drag.removeClass(drag.t.opt.draggingClass); //remove the grip's dragging css-class
        if (!(drag.x - drag.l == 0)) {
            var t = drag.t;
            var cb = t.opt.onResize; //get some values
            var i = drag.i; //column index
            var last = i == t.ln-1; //check if it is the last column's grip (usually hidden)
            var c = t.g[i].c; //the column being dragged
            if(last){
                c.width(drag.w);
                c.w = drag.w;
            }else{
                syncCols(t, i, true); //the columns are updated
            }
            if(!t.f) applyBounds(t); //if not fixed mode, then apply bounds to obtain real width values
            syncGrips(t); //the grips are updated
            if (cb) { e.currentTarget = t[0]; cb(e); } //if there is a callback function, it is fired
            if(t.p && S) memento(t); //if postbackSafe is enabled and there is sessionStorage support, the new layout is serialized and stored
        }
        drag = null; //since the grip's dragging is over
    };
     
     
    /**
     * Event handler fired when the grip's dragging is about to start. Its main goal is to set up events
     * and store some values used while dragging.
     * @param {event} e - grip's mousedown event
     */
    var onGripMouseDown = function(e){
        var o = $(this).data(SIGNATURE); //retrieve grip's data
        var t = tables[o.t], g = t.g[o.i]; //shortcuts for the table and grip objects
        var oe = e.originalEvent.touches; //touch or mouse event?
        g.ox = oe? oe[0].pageX: e.pageX; //the initial position is kept
        g.l = g.position().left;
        g.x = g.l;
         
        d.bind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE, onGripDrag ).bind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE, onGripDragOver); //mousemove and mouseup events are bound
        h.append("<style type='text/css'>*{cursor:"+ t.opt.dragCursor +"!important}</style>"); //change the mouse cursor
        g.addClass(t.opt.draggingClass); //add the dragging class (to allow some visual feedback)
        drag = g; //the current grip is stored as the current dragging object
        if(t.c[o.i].l) for(var i=0,c; i<t.ln; i++){ c=t.c[i]; c.l = false; c.w= c.width(); } //if the colum is locked (after browser resize), then c.w must be updated
        return false; //prevent text selection
    };
     
     
    /**
     * Event handler fired when the browser is resized. The main purpose of this function is to update
     * table layout according to the browser's size synchronizing related grips
     */
    var onResize = function(){
        for(var t in tables){
            if( tables.hasOwnProperty( t ) ) {
                t = tables[t];
                var i, mw=0;
                t.removeClass(SIGNATURE); //firefox doesn't like layout-fixed in some cases
                if (t.f) { //in fixed mode
                    t.w = t.width(); //its new width is kept
                    for(i=0; i<t.ln; i++) mw+= t.c[i].w;
                    //cell rendering is not as trivial as it might seem, and it is slightly different for
                    //each browser. In the beginning i had a big switch for each browser, but since the code
                    //was extremely ugly now I use a different approach with several re-flows. This works
                    //pretty well but it's a bit slower. For now, lets keep things simple...
                    for(i=0; i<t.ln; i++) t.c[i].css("width", M.round(1000*t.c[i].w/mw)/10 + "%").l=true;
                    //c.l locks the column, telling us that its c.w is outdated
                }else{ //in non fixed-sized tables
                    applyBounds(t); //apply the new bounds
                    if(t.mode == 'flex' && t.p && S){ //if postbackSafe is enabled and there is sessionStorage support,
                        memento(t); //the new layout is serialized and stored for 'flex' tables
                    }
                }
                syncGrips(t.addClass(SIGNATURE));
            }
        }
         
    };
 
 
    //bind resize event, to update grips position
    $(window).bind('resize.'+SIGNATURE, onResize);
 
 
    /**
     * The plugin is added to the jQuery library
     * @param {Object} options - an object that holds some basic customization values
     */
    $.fn.extend({
        colResizable: function(options) {
            var defaults = {
             
                //attributes:
                 
                resizeMode: 'fit', //mode can be 'fit', 'flex' or 'overflow'
                draggingClass: 'JCLRgripDrag', //css-class used when a grip is being dragged (for visual feedback purposes)
                gripInnerHtml: '', //if it is required to use a custom grip it can be done using some custom HTML
                liveDrag: false, //enables table-layout updating while dragging
                minWidth: 15, //minimum width value in pixels allowed for a column
                headerOnly: false, //specifies that the size of the the column resizing anchors will be bounded to the size of the first row
                hoverCursor: "e-resize", //cursor to be used on grip hover
                dragCursor: "e-resize", //cursor to be used while dragging
                postbackSafe: false, //when it is enabled, table layout can persist after postback or page refresh. It requires browsers with sessionStorage support (it can be emulated with sessionStorage.js).
                flush: false, //when postbakSafe is enabled, and it is required to prevent layout restoration after postback, 'flush' will remove its associated layout data
                marginLeft: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ...
                marginRight: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ...
                disable: false, //disables all the enhancements performed in a previously colResized table
                partialRefresh: false, //can be used in combination with postbackSafe when the table is inside of an updatePanel,
                disabledColumns: [], //column indexes to be excluded
 
                //events:
                onDrag: null, //callback function to be fired during the column resizing process if liveDrag is enabled
                onResize: null //callback function fired when the dragging process is over
            }
            var options = $.extend(defaults, options);
             
            //since now there are 3 different ways of resizing columns, I changed the external interface to make it clear
            //calling it 'resizeMode' but also to remove the "fixed" attribute which was confusing for many people
            options.fixed = true;
            options.overflow = false;
            switch(options.resizeMode){
                case 'flex': options.fixed = false; break;
                case 'overflow': options.fixed = false; options.overflow = true; break;
            }
 
            return this.each(function() {
                 init( this, options);
            });
        }
    });
})(jQuery);
 
   
</script>
   
  <script type="text/javascript">
    $(function(){
 
     
        $("#normal").colResizable({
            liveDrag:true,
            gripInnerHtml:"<div class='grip'></div>",
            draggingClass:"dragging",
            resizeMode:'fit'
        });
         
        $("#flex").colResizable({
            liveDrag:true,
            gripInnerHtml:"<div class='grip'></div>",
            draggingClass:"dragging",
            resizeMode:'flex'
        });
 
 
      $("#overflow").colResizable({
          liveDrag:true,
          gripInnerHtml:"<div class='grip'></div>",
          draggingClass:"dragging",
          resizeMode:'overflow'
      });
         
 
      $("#disabled").colResizable({
          liveDrag:true,
          gripInnerHtml:"<div class='grip'></div>",
          draggingClass:"dragging",
          resizeMode:'overflow',
          disabledColumns: [2]
      });
 
         
    });
  </script>
  
 <script>
function myFunction() {
  // Declare variables
  var input, filter, table, tr, td1,td2,td3,td4,td5,td6,td7,td8,td9, i;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("normal");
  //tr = table.getElementsByTagName("tr");
  tr = table.getElementsByClassName("trrow");
 
  // Loop through all table rows, and hide those who don't match the search query
  for (i = 0; i < tr.length; i++) {
   td1 = tr[i].getElementsByClassName("EntityName")[0];
     td2 = tr[i].getElementsByClassName("EntityId")[0];
     td3 = tr[i].getElementsByClassName("Path")[0];
     td4 = tr[i].getElementsByClassName("ClassName")[0];
     td5 = tr[i].getElementsByClassName("ClassId")[0];
     td6 = tr[i].getElementsByClassName("WorkflowName")[0];
     td7 = tr[i].getElementsByClassName("WorkflowDisplayName")[0];
     td8 = tr[i].getElementsByClassName("WorkflowType")[0];
     td9 = tr[i].getElementsByClassName("WorkflowId")[0];
 
    if (td1) {
      if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td3.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1) || (td7.innerHTML.toUpperCase().indexOf(filter) > -1) || (td8.innerHTML.toUpperCase().indexOf(filter) > -1) || (td9.innerHTML.toUpperCase().indexOf(filter) > -1) ) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}
</script>
  
    </head>
<body>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for text..">
<div class="center" >
     
<table id="normal" width="400%" border="0" cellpadding="5" cellspacing="0">
 
 
'@



    $HTML += @"
$headerHTML
"@


    $i =1

    #Build rows
    ForEach ($WF in $arrWFs) {
      $rows += @"
  <tr class="trrow">
 
"@


      ForEach ($header in $arrOrderedHeaders) {
        If ($header -eq 'Index') { 
          $rows += @"
      <td class="$($Header)">$($i)</td>
 
"@
    
        }
        Else {
          $rows += @"
      <td class="$($Header)">$($WF.$Header)</td>
 
"@

        }
      } #end ForEach Header
      $i++
    } #end ForEach WF


    $rows += @"
  </tr>
 
"@



    $HTML += @"
$rows
</table>
</div>
</body>
</html>
"@


    Return $HTML
  
  } #End Function Build-HTMLFancyResize
  ###################################################################
  
  Function Export-HTMLFile {
    [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
        SupportsShouldProcess=$true, 
    PositionalBinding=$false)]
    Param (
      #array of workflows to be exported to the Path
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=0,
      ParameterSetName='Parameter Set 1')]
      [System.Object[]]$arrWFs,

      #export file path
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=1,
      ParameterSetName='Parameter Set 1')]
      [string]$Path,

      #title of HTML doc
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=2,
      ParameterSetName='Parameter Set 1')]
      [string]$Title = "",

      #which formatting type
      [Parameter(Mandatory=$false, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
          Position=3,
      ParameterSetName='Parameter Set 1')]
      [ValidateSet("Fancy", "Basic")]
      [string]$Format = 'Fancy'
    )
    Write-Host "Building HTML document..." -ForegroundColor Gray
    Switch ($Format){
      "Fancy" {
        $HTML = Build-HTMLFancyResize -arrWFs $arrWFs -Title $Title
      }
      "Basic" {
        $HTML = Build-HTMLBasic -arrWFs $arrWFs -Title $Title
      }

      Default {
        Throw "Problem with Export-HTMLFile. Exiting..."
      }

    }

    Try {
      $HTML | Set-Content -Path $Path -Force
      Write-Verbose "`nFiles written to directory [$(Split-Path $Path -Parent)]"
      Return $Path
    }Catch {
      Write-Error "Unable to write content to HTML file: [$Path]. `n$($Error[0].Message)"
    }
  }
  ###################################################################
  Function Export-CSVFile {
    Param (
      #export file path
      [string]$Path,

      #array of workflows to be exported to the Path
      [System.Object[]]$arrWFs
    )

    Try {
      $arrWFs | Export-Csv -Path $Path -NoTypeInformation -Force
      Write-Verbose "`nFiles written to directory [$(Split-Path $Path -Parent)]"
      Return $Path
    }Catch {
      Write-Error "Unable to write content to CSV file: [$Path]. `n$($Error[0].Message)"
    }
  }

  ###################################################################
  Function FormatObj-ForCSVExport {
    Param (
      # New custom workflow object being built
      [System.Object]$obj,

      # Workflow (from effective config method)
      [System.Object]$WF
    )

    $Params = ''

    Try {
      Switch ($WorkFlow.Type){
        "Rule" {
          $Obj.WorkflowDisplayName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].DisplayName)
          If ($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration) {
            $Params = (([xml]"<Parameters>$($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration)</Parameters>").ChildNodes )
          }
          $Obj.WorkflowMPName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0])
        }

        "Monitor" {
          $Obj.WorkflowDisplayName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].DisplayName)
          If ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration) {
            $Params = (([xml]"<Parameters>$($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration)</Parameters>").ChildNodes )
          }
          $Obj.WorkflowMPName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0])
        }
        default {
          $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName)
          Write-Host "WorkFlow.Type: '$($WorkFlow.Type)' not found!" -F Yellow
        }
      }
    } Catch { 
      $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName)
    }

    If ($Params) {
      $propertyNames = ''
      $propertyNames = $Params | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name
      $ParamsFormattedCSV =''
      ForEach ($Name in $propertyNames) {
        $ParamsFormattedCSV += @"
$($Name): $($Params.$Name)
 
"@
    
      }

      $Obj.Parameters = $ParamsFormattedCSV
    }

    $OverridesFormattedCSV = ''
    If ($WorkFlow.Overridden) {
      ForEach ($Override in $WorkFlow.OverrideableParameters) {
        $propertyNames = $Override | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name
        $OverridesFormattedCSV += @"
ParameterName: $($Override.ParameterName)
DefaultValue: $($Override.DefaultValue)
EffectiveValue: $($Override.EffectiveValue)
<><><><><><><><><><><><><><><><><>
 
"@
    
      } #end ForEach Override

      $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value $OverridesFormattedCSV
    }
    Else {
      #No Overrides
      $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value ''
    }

    Return $Obj

  }
  ###################################################################
  <# This function is for formatting the Parameters and Overrides fields specifically for HTML.
  #>

  Function FormatObj-ForHTMLExport {
    Param (
      # New custom workflow object being built
      [System.Object]$Obj
    )
    $Params = ''

    Try {
      Switch ($WorkFlow.Type){
        "Rule" {
          $Obj.WorkflowDisplayName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].DisplayName)
          If ($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration) {
            $Params = (([xml]"<Parameters>$($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration)</Parameters>").ChildNodes )
          }
          $Obj.WorkflowMPName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0])
        }

        "Monitor" {
          $Obj.WorkflowDisplayName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].DisplayName)
          If ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration) {
            $Params = (([xml]"<Parameters>$($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration)</Parameters>").ChildNodes )
          }
          $Obj.WorkflowMPName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0])
        }
        default {$Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName)}
      }
    } Catch { 
      $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName)
    }

    If ($Params) {
      $propertyNames = ''
      $propertyNames = $Params | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name
      $embedTable = @"
    <table border=1 cellpadding="5">
        <tr style="font-weight:bold">
        <td>Name</td>
        <td>Value</td>
        </tr>
     
"@

      ForEach ($Name in $propertyNames) {
        $value = ( ( (($Params.$Name -Replace '&','&amp;') -Replace '<', '&lt;' ) -Replace '>', '&gt;' ) -Replace '\n', $BR_Token)
        $embedTable += @"
        <tr>
        <td>$($Name)</td>
        <td>$($value)</td>
        </tr>
     
"@
    
      }
      $embedTable += @"
    </table>
     
"@

      $Obj.Parameters = $embedTable
    }

    If ($WorkFlow.Overridden) {
      $embedTable = @"
          <table border=1 cellpadding="5">
              <tr style="font-weight:bold">
                <td>ParameterName</td>
                <td>DefaultValue</td>
              <td>EffectiveValue</td>
              </tr>
     
"@

      ForEach ($Override in $WorkFlow.OverrideableParameters) {
        $propertyNames = $Override | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name
        $embedTable += @"
              <tr>
                <td>$($Override.ParameterName)</td>
                <td>$($Override.DefaultValue)</td>
                <td>$($Override.EffectiveValue)</td>
            </tr>
     
"@
    
      } #end ForEach Override

      $embedTable += @"
          </table>
     
"@

      $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value $embedTable
    }
    Else {
      #No Overrides
      $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value ''
    }

    Return $Obj
  }
  ###################################################################

  Function Get-CleanNames {
    Param (
      # Should be a MonitoringObject
      [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject]$MonitoringObject
    )
  
    $objCleanNames = New-Object -TypeName pscustomobject
    $objCleanNames | Add-Member -MemberType NoteProperty -Name "Name" -Value (Clean-Name $MonitoringObject.Name)
    If ($MonitoringObject.DisplayName) {
      $objCleanNames | Add-Member -MemberType NoteProperty -Name "DN" -Value (Clean-Name $MonitoringObject.DisplayName)
    }
    Else {
      $objCleanNames | Add-Member -MemberType NoteProperty -Name "DN" -Value (Clean-Name $MonitoringObject.Name)
    }
    $objCleanNames | Add-Member -MemberType NoteProperty -Name "Path" -Value (Clean-Name $MonitoringObject.Path)

    Return $objCleanNames
  }
  ###################################################################

  Function Get-SCOMRelatedObjects {
    [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
    SupportsShouldProcess=$true)]
                    
    Param (
  
      # This should be a SCOM MonitoringObject
      [Parameter(Mandatory=$true, 
          ValueFromPipeline=$true,
          ValueFromPipelineByPropertyName=$true, 
          ValueFromRemainingArguments=$true, 
          Position=0,
      ParameterSetName='Parameter Set 1')]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject[]]$object
    )
  
    #region Begin
    Begin {
      $arr = @()

      Function Dig-Object {
        [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
        SupportsShouldProcess=$true)]
        Param (
  
          # Param1 help description
          [Parameter(Mandatory=$true, 
              ValueFromPipeline=$true,
              ValueFromPipelineByPropertyName=$true, 
              ValueFromRemainingArguments=$true, 
              Position=0,
          ParameterSetName='Parameter Set 1')]
          [ValidateNotNull()]
          [ValidateNotNullOrEmpty()]
          [system.object]$object,
          [string]$indent
        )

      
        Begin{
          $arr = @()
          $related = @()
        }
        Process {
          Write-Verbose "$indent $($_.DisplayName) , $($_.GetLeastDerivedNonAbstractClass())"
          $arr += $object
          $tmpRelated = $object.GetRelatedMonitoringObjects()
          If ($tmpRelated.Count -ge 1) { 
            $related += ($tmpRelated | Dig-Object -indent ($indent + "--")) 
            If ($related){ 
              $arr += $related
            }
          }
        }
        End {
          Return ($arr )
        }
      }

    }#endregion Begin
    Process{
      ForEach ($item in $object){
        $arr += $item | Dig-Object -indent "--" 
      }
    }  
    End{
      Return ($arr | Select-Object -Unique)
    }
    
  }#end Get-SCOMRelatedObjects

  ###################################################################
  Function Load-Cache {
    # Load all Monitors
    Write-Host "Getting monitors..." -F Gray
    $AllMonitors = Get-SCOMMonitor
    $hashAllMonitors = @{}
    $i =1
    ForEach ($Mon in $AllMonitors) {
      $percent = [math]::Round((($i / $AllMonitors.Count) *100),0)
      Write-Progress -Activity "** What's happening? **" -status "Loading monitors. Be patient! [Percent: $($percent)]" -PercentComplete $percent
      $hashAllMonitors.Add($Mon.Id.Guid, $Mon)
      $i++
    }
    Write-Progress -Activity "** What's happening? **" -status "Complete" -Completed
    # Load all rules
    Write-Host "Getting rules..." -F Gray
    $AllRules = Get-SCOMRule
    $hashAllRules = @{}
    $i=1
    ForEach ($Rule in $AllRules) {
      $percent = [math]::Round((($i / $AllRules.Count) *100),0)
      Write-Progress -Activity "** What's happening? **" -status "Loading Rules. Be patient! [Percent: $($percent)]" -PercentComplete $percent
      $hashAllRules.Add($Rule.Id.Guid, $Rule)
      $i++
    }
    Write-Progress -Activity "** What's happening? **" -status "Complete" -Completed
  }
  ###################################################################
  

  #---------------------------------------------------------------
  #--------------------------- MAIN ------------------------------

  #Build headers
  $arrOrderedHeaders = @"
Index
EntityName
EntityId
Path
ClassName
ClassId
WorkflowName
WorkflowDisplayName
WorkflowType
WorkflowId
WorkflowMPName
Enabled
GeneratesAlert
AlertSeverity
AlertPriority
Overridden
Description
Parameters
Overrides
"@
 -split '\r\n'

  Switch ($SortOrder ) {
    'Ascending' {
      [BOOL]$SortOrder = $false
    }
    'Descending' {
      [BOOL]$SortOrder = $true
    }
    Default {
      [BOOL]$SortOrder = $false
    }
  }

  $BR_Token = '<BR />'


  #Validate arguments/paths
  If ( (-NOT($ExportCSVPath)) -AND (-NOT($ExportHTMLPath)) ) {
    Throw 'You must provide at least one valid export path/directory with -ExportHTMLPath or -ExportCSVPath. Exiting...'
  }
  If ($ExportCSVPath) {
    Try{
      If (-NOT(Test-Path -Path $ExportCSVPath -PathType Container)){
        Write-Host "Path: [$($ExportCSVPath)] does not exist. Creating folder now..."
        New-Item -Path $ExportCSVPath -ItemType Directory -ErrorAction Stop
      }
    }
    Catch{
      Write-Error "Invalid path: [$($ExportCSVPath)]. Must be valid directory. Unable to create directory."
      Return 1
    }
  }
  If ($ExportHTMLPath) {
    Try{
      If (-NOT(Test-Path -Path $ExportHTMLPath -PathType Container)){
        Write-Host "Path: [$($ExportHTMLPath)] does not exist. Creating folder now..."
        New-Item -Path $ExportHTMLPath -ItemType Directory -ErrorAction Stop
      }
      Else {
        $SearchIconPath = Join-Path -Path $ExportHTMLPath -ChildPath 'searchicon.png'
      }
    }
    Catch{
      Write-Error "Invalid path: [$($ExportHTMLPath)]. Must be valid directory. Unable to create directory."
      Return 1
    }
  }

  If (-NOT $TESTING){
    Write-Output "Loading all rules and monitors..."
    . Load-Cache
  }

  #---------------------------------------------------------------


  #Thanks to Craig Pero for pointing me in the right direction here!
  $omMg = Get-SCOMManagementGroup | Select-Object -First 1

  #array to contain master list of all workflows of all related objects
  [System.Collections.ArrayList]$arrAllWorkflowsCSV = @()
  [System.Collections.ArrayList]$arrAllWorkflowsHTML = @()

  $timer = [System.Diagnostics.Stopwatch]::StartNew()
  #array to contain all related objects
  $arrObjects = Get-SCOMRelatedObjects -object $MonitoringObject
  Write-Host "$($timer.Elapsed.TotalSeconds) second to retrieve all related objects" -ForegroundColor Gray
  Write-Host "`n`n`n`n"   #Start below Write-Progress bar
  
  $i=1 #index for the progress bar
  $timer = [System.Diagnostics.Stopwatch]::StartNew()
  [bool]$SkipFixOR = $false
  
  #region
  ForEach ($obj in ($arrObjects |Sort-Object -Property DisplayName) ) {
    $CleanNames = ''
    $CleanName = Get-CleanNames -MonitoringObject $obj

    Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; `
    Write-Host "[" -ForegroundColor Red -NoNewline; `
    Write-Host "$($obj.Path)" -ForegroundColor Yellow -NoNewline; `
    Write-Host "]" -ForegroundColor Red -NoNewline; `
    Write-Host " $($obj.FullName)"  -ForegroundColor Green
  
    $percent = [math]::Round((($i / $arrObjects.Count) *100),0)
    Write-Progress -Activity "** What's happening? **" -status "Getting your data. Be patient! [Percent: $($percent)]" -PercentComplete $percent
    Try{
      $Workflows = $omMg.EffectiveMonitoring.GetAllEffectiveMonitoringWorkflows($obj.Id,$true)
    }
    Catch{
      # It's possible to have "not lowercase" true|false values for the Enabled property which causes this report/method to puke
      Write-Verbose ('Unable to execute: $Workflows = $omMg.EffectiveMonitoring.GetAllEffectiveMonitoringWorkflows($obj.id,$true)' + " for instance of class: Microsoft.Windows.Computer. $($obj.FullName) , ID: $($obj.Id)")
      Write-Error $Error[0].Exception
      Write-Host "Skipping item: $($obj.FullName) , ID: $($obj.Id)" -F Gray
      
      #<#
      If ( ($Error[0].Exception -match 'true|false') -AND ($Error[0].Exception -cmatch '[^true|^false]') -AND (-NOT $SkipFixOR) ) { 
        $choice = ''
        $choice = $null
        While ($choice -notmatch '^n$|^a$') {
          Write-Host "Problem detected with `"Enabled`" property value. This value must be all lowercase. Would you like to attempt to fix overrides?"  -ForegroundColor Yellow -BackgroundColor Red
          $choice = (Read-Host "fix [a]ll or [n]o ?").ToLower()
        }
        # Fix overrides
        If ($choice -eq 'a') {
          If ($ExportCSVPath){ $tmpPath = $ExportCSVPath }
          Else { $tmpPath = $ExportHTMLPath}
          Set-OverrideEnabledCase -ExportPath $tmpPath
          Write-Host "`nOverride Values should be fixed. Try to run this report again. `n`nExiting..." -F Yellow
          Exit
        }
           
        #Do not fix overrides. Don't prompt again.
        Else {
          Write-Host "Will not prompt again to repair overrides. " -F Gray
          $SkipFixOR = $true #user will not be prompted again to fix
        }
      }
      
      Continue;
    } #end Catch
    #>
    

    [System.Collections.ArrayList]$arrThisObjWorkflowsCSV = @()
    [System.Collections.ArrayList]$arrThisObjWorkflowsHTML = @()
    [int]$w = 1
    #region
    ForEach ($WorkFlow in ($Workflows|Sort-Object -Property $SortBy -Descending:$SortOrder )) {
      $tmpObj = New-Object -TypeName PSCUSTOMOBJECT
      # $tmpObj | Add-Member -MemberType NoteProperty -Name "NAME" -Value "VALUE"
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Index" -Value $w
      $tmpObj | Add-Member -MemberType NoteProperty -Name "EntityName" -Value ($WorkFlow.EntityName)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "EntityId" -Value ($WorkFlow.EntityId.Guid) 
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Path" -Value ($obj.Path) 
      $tmpObj | Add-Member -MemberType NoteProperty -Name "ClassName" -Value ($WorkFlow.EntityTypeName)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "ClassId" -Value ($WorkFlow.EntityTypeId.Guid)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowName" -Value ($WorkFlow.WorkflowName)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowDisplayName" -Value "PLACEHOLDER"
      $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value ($WorkFlow.Type)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowId" -Value ($WorkFlow.WorkflowId.Guid)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowMPName" -Value "PLACEHOLDER" 
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Enabled" -Value ($WorkFlow.Enabled.ToString())
      $tmpObj | Add-Member -MemberType NoteProperty -Name "GeneratesAlert" -Value ($WorkFlow.GeneratesAlert.ToString())
      $tmpObj | Add-Member -MemberType NoteProperty -Name "AlertSeverity" -Value ($WorkFlow.AlertSeverity)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "AlertPriority" -Value ($WorkFlow.AlertPriority)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Overridden" -Value ($WorkFlow.Overridden.ToString())
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Description" -Value ($WorkFlow.Description)
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Parameters" -Value ''

      If ($ExportCSVPath){
        $tmpObjCSV = FormatObj-ForCSVExport -Obj $tmpObj.PsObject.Copy()
        $arrAllWorkflowsCSV.Add($tmpObjCSV) | Out-Null
        $arrThisObjWorkflowsCSV.Add($tmpObjCSV) | Out-Null
      }
      If ($ExportHTMLPath){
        $tmpObjHTML = FormatObj-ForHTMLExport -Obj $tmpObj.PsObject.Copy()
        $arrAllWorkflowsHTML.Add($tmpObjHTML) | Out-Null
        $arrThisObjWorkflowsHTML.Add($tmpObjHTML) | Out-Null
      }
      $w++
    }
    #endregion ForEach Workflow (of the Obj)
  
    If ($ExportCSVPath -AND $IndividualFiles){
      Try {
        $ObjExportFilePath = (Join-Path $ExportCSVPath "($($CleanName.Path))_$($CleanName.DN).csv" )
        Export-CSVFile -arrWFs $arrThisObjWorkflowsCSV -Path $ObjExportFilePath -NoTypeInformation -Force
      }Catch {
        Write-Error "Problem exporting to CSV [$ObjExportFilePath]. $($Error[0].Message)"
      }
    }
  
    If ($ExportHTMLPath -AND $IndividualFiles) {
      Try {
        $ObjExportFilePath = (Join-Path $ExportHTMLPath "($($CleanName.Path))_$($CleanName.DN).html" )
        Export-HTMLFile -arrWFs $arrThisObjWorkflowsHTML -Path $ObjExportFilePath -Title $obj.Name -Format $HTMLFormat
      }Catch {
        Write-Error "Problem exporting to HTML [$ObjExportFilePath]. $($Error[0].Message)"
      }
    }
    $i++
  }
  #endregion ForEach Obj

  Write-Progress -Activity "** What's happening? **" -status "Completed" -Completed
  $arrAllWorkflowsCSV  = ($arrAllWorkflowsCSV | Sort-Object -Property $SortBy -Descending:$SortOrder)
  $arrAllWorkflowsHTML  = ($arrAllWorkflowsHTML | Sort-Object -Property $SortBy -Descending:$SortOrder)
  $CleanNames = ''
  # Output master files if appropriate
  If ($ExportCSVPath){
    Try {
      If ($MonitoringObject.Count -eq 1){
        $CleanName = Get-CleanNames -MonitoringObject $MonitoringObject[0]
        $ObjExportFilePath = (Join-Path $ExportCSVPath "($($CleanName.Path))_$($CleanName.DN).csv"  )
      }
      Else {
        $ObjExportFilePath = (Join-Path $ExportCSVPath "MERGED.CSV" )
      }
      Export-CSVFile -arrWFs $arrAllWorkflowsCSV -Path $ObjExportFilePath -NoTypeInformation -Force
    }Catch {
      Write-Error "Problem exporting to CSV [$ObjExportFilePath]. $($Error[0].Message)"
    }
  }
  
  If ($ExportHTMLPath) {
    Try {
      If ($MonitoringObject.Count -eq 1){
        $CleanName = Get-CleanNames -MonitoringObject $MonitoringObject[0]
        $ObjExportFilePath = (Join-Path $ExportHTMLPath "($($CleanName.Path))_$($CleanName.DN).html" )
      }
      Else {
        $ObjExportFilePath = (Join-Path $ExportHTMLPath "MERGED.HTML" )
      }
      Export-HTMLFile -arrWFs $arrAllWorkflowsHTML -Path $ObjExportFilePath -Title 'All Workflows' -Format $HTMLFormat
    }Catch {
      Write-Error "Problem exporting to HTML [$ObjExportFilePath]. $($Error[0].Message)"
    }
  }

  Write-Host "$($timer.Elapsed.TotalSeconds) seconds to complete workflow export for $($i) objects." -ForegroundColor Gray

  #---------------------------------------------------------------
} #End Function
#######################################################################

<#
    .SYNOPSIS
    Will export Operations Manager event log events to CSV
 
    .EXAMPLE
    Export-SCOMEventsToCSV
 
    The above example will output the newest 1000 SCOM events (default 1000) to a .CSV at C:\<computername>_OpsManEvents.csv
 
    .EXAMPLE
    Export-SCOMEventsToCSV -Newest 1500 -Path c:\Temp\SCOMlog.csv
 
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Original Date: 2018.03.22
    History:
#>

Function Export-SCOMEventsToCSV {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$true, 
      PositionalBinding=$false,
  ConfirmImpact='Medium')]
  Param (
    [int]$Newest = 1000,

    # Param1 help description
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [Alias("Path")] 
    [string]$OutFileCSV = "C:\$($env:COMPUTERNAME)_OpsManEvents.csv"
  )
  Get-EventLog -LogName 'Operations Manager' -Newest $Newest | Export-Csv -Path $OutFileCSV -NoTypeInformation -Force
  Return (Get-Item $OutFileCSV)
}
#######################################################################

<#
    .SYNOPSIS
    This script will get all rule and monitor knowledge article content and output the information to separate files (Rules.html and Monitors.html) in the output folder path specified.
 
    .PARAMETER OutFolder
    The output folder path where output files will be created. Must be a container/directory, not a file.
 
    .PARAMETER ManagementPack
    A collection/array of one or more management pack objects.
 
    .PARAMETER MgmtServerFQDN
    Alias: ManagementServer
    Fully Qualified Domain Name of the SCOM management server.
 
    .PARAMETER NoKnowledgeExclude
    By default ALL workflows will be included in the output files. Enable this switch to exclude workflows which have no Knowledge Article content.
 
    .PARAMETER ExportCSV
    Export results to CSV files instead of default format (.html)
 
    .PARAMETER Topic
    This will customize the name of the output files. Useful when dumping multiple sets to the same output folder.
 
    .PARAMETER ShowResult
    Will open the Windows file Explorer to show output files.
 
    .EXAMPLE
    (Get-SCOMManagementPack -Name *.AD.*) | Export-SCOMKnowledge -OutFolder 'C:\MyReports' -Topic "AD_" -ShowResult
    In the example above the variable will be assigned a collection of all management pack objects with ".AD." in the name.
    That subset/collection of management pack objects will be passed into the script. The script will output workflow details for all Rules and Monitors contained in ALL management packs within that set.
    The output file names will be: "AD_Rules.html" and "AD_Monitors.html". Finally the script will open Windows File Explorer to the location of the output directory.
 
    .Example
    PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '201[0-6]'
    The command above will output all rules/monitors from management packs which contain 'ad.' in the Name and which contain '201x' in the Name or DisplayName of the workflow where 'x' represents any single digit 0-6 .
 
    .EXAMPLE
    PS C:\> Export-SCOMKnowledge -OutFolder "C:\Temp" -ManagementPack (Get-SCOMManagementPack -Name *SQL*) -Topic "SQL_Packs_"
    The command above will output workflow details for all Rules and Monitors contained in ALL management packs with "SQL" in the management pack Name.
    The output file names will be: "SQL_Packs_Rules.html" and "SQL_Packs_Monitors.html"
 
    .EXAMPLE
    PS C:\> Export-SCOMKnowledge -OutFolder "C:\MyReports" -ManagementServer "ms01.contoso.com" -NoKnowledgeExclude
    In the example above, the command will connect to management server: "ms01.contoso.com", will output workflow details for all Rules and Monitors (only if they contain a Knowledge Article) to two separate files in the specified folder. The output file names will be: "Rules.html" and "Monitors.html"
 
    .Example
    PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '(?=200[0-8])((?!Monitoring).)*$'
    The command above will output all rules/monitors from management packs which contain 'ad' in the Name and which contain '200x' in the Name or DisplayName of the workflow where 'x' is numbers 1-8 but excluding workflows that contain 'monitoring'.
 
    .Example
    #Export SCOM Knowledge for rules/monitors
    Get-SCOMManagementGroupConnection | Remove-SCOMManagementGroupConnection -Verbose
    New-SCOMManagementGroupConnection -ComputerName YOUR_SERVERNAME
    $SQLMPs = Get-SCOMManagementPack -Name *sql*
    $Outfolder = 'D:\SCOMReports'
 
    # Workflows to separate files
    ForEach ($MP in $SQLMPs){
    $rules = $MP.GetRules().Count
    $mons = $MP.GetMonitors().Count
    If (($rules + $mons) -gt 0){
    Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $MP -Topic "$($MP.Name)_"
    }
    Else{
    Write-Host "No monitor/rule workflows found: " -NoNewline;
    Write-Host "$($MP.DisplayName)" -f Yellow
    }
    }
 
    #All MPs combined
    Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $SQLMPs -Topic "ALL_SQL_"
 
 
    .LINK
    https://monitoringguys.com/
 
    .NOTES
    Version: 1.12
    Author: Tyson Paul
    Date: 2016/5/3
    Blog: https://monitoringguys.com/
 
    History:
    2017/12/05: Added Topic parameter to customize output file names. Added ShowResult switch.
    2017/10/16: Added support for regex filtering on DisplayName or Name of monitors/rules.
    2016/05/03: Added option to export to CSV. Although embedded tables in the KnowledgeArticle don't convert very well.
    2016/04/26: Improved formatting of output files. Now includes embedded parameter tables and restored article href links.
    2016/04/26: Fixed script to accept ManagementPack pipeline input
 
#>

Function Export-SCOMKnowledge {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$false,
      SupportsPaging = $true,
  PositionalBinding=$false)]

  Param(
    #1
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OutFolder,

    #2
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [System.Object[]]$ManagementPack,

    #3
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("ManagementServer")]
    [string]$MgmtServerFQDN,

    #4
    [Parameter(Mandatory=$false,
        Position=3,
    ParameterSetName='Parameter Set 1')]
    [Switch]$NoKnowledgeExclude,

    #5
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=4,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OpsDBServer,

    #6
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=5,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OpsDBName,

    #7
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=6,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$Topic,

    #8
    [Parameter(Mandatory=$false,
        Position=7,
    ParameterSetName='Parameter Set 1')]
    [Alias("WorkflowFilter")] 
    [string[]]$Filter,

    #9
    [Parameter(Mandatory=$false,
        Position=8,
    ParameterSetName='Parameter Set 1')]
    [switch]$ExportCSV,

    #10
    [Parameter(Mandatory=$false,
        Position=9,
    ParameterSetName='Parameter Set 1')]
    [switch]$ShowResult=$false
  )

  #### UNCOMMENT FOR TESTING ####
  #$OutFolder = "c:\Test"

  #region Functions
  Begin {
    #------------------------------------------------------------------------------------
    Function ProcessArticle {
      Param(
        $article
      )
      If ($article -ne "None")    #some rules don't have any knowledge articles
      {
        #Retrieve and format article content
        $MamlText = $null
        $HtmlText = $null

        If ($null -ne $article.MamlContent)
        {
          $MamlText = $article.MamlContent
          $articleContent = fnMamlToHtml($MamlText)
        }

        If ($null -ne $article.HtmlContent)
        {
          $HtmlText = $article.HtmlContent
          $articleContent = CleanHTML($HtmlText)
        }
      }

      If ($null -eq $articleContent)
      {
        $articleContent = "No resolutions were found for this alert."
      }

      Return $articleContent
    }

    #------------------------------------------------------------------------------------
    Function ProcessWorkflows {
      Param(
        $Workflows,
        [string]$WFType
      )
      $thisWFType = $WFType
      $myWFCollectionObj = @()
      [int]$row=1
      ForEach ($thisWF in $Workflows) {
        Write-Progress -Activity "Processing $WFType" -status "Getting Alert [$($row)]: $($thisWF.DisplayName) " -percentComplete ($row / $($Workflows.count) * 100)
        $ErrorActionPreference = 'SilentlyContinue'
        $article = $thisWF.GetKnowledgeArticle($cultureInfo)
        If ($? -eq $false){
          $error.Remove($Error[0])
          $article = "None"
          If ($NoKnowledgeExclude){ Continue; }
        }
        Else{
          $articleContent = ProcessArticle $article
        }

        If ($ExportCSV) {
          $WFName = $($thisWF.Name)
          $WFDisplayName = $($thisWF.DisplayName)
        }
        Else {
          $WFName = '<name>' + $($thisWF.Name) + '</name>'
          $WFDisplayName = '<displayname>' + $($thisWF.DisplayName) + '</displayname>'
        }
        $WFDescription = $thisWF.Description
        If ($WFDescription.Length -lt 1) {$WFDescription = "None"}
        #region Get_alert_name
        If ($WFType -like "Rule") {
          $thisWFType = "$($WFType): " + "$($thisWF.WriteActionCollection.Name)"
          # Proceed only if rule is an "alert" rule.
          If ($thisWF.WriteActionCollection.Name -Like "GenerateAlert"){
            $AlertMessageID = $($thisWF.WriteActionCollection.Configuration).Split('"',3)[1]
            $Query = @"
Select [LTValue]
FROM [OperationsManager].[dbo].[LocalizedText]
WHERE ElementName like '$($AlertMessageID)'
AND LTStringType = '1'
 
"@


            $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName
            If ($ExportCSV) { $AlertName = $AlertDisplayName }
            Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' }
          }
          Else {
            If ($ExportCSV) { $AlertName = 'N/A' }
            Else { $AlertName = '<noalertname>N/A</noalertname>' }
          }
        }
        Else {
          # Workflow is not a rule, therefore it is a monitor.
          $Query = @"
Select LocalizedText.LTValue
From [OperationsManager].[dbo].[LocalizedText]
INNER JOIN [OperationsManager].[dbo].[Monitor]
on LocalizedText.LTStringId=Monitor.AlertMessage
Where Monitor.MonitorID like '$($thisWF.ID)'
AND LocalizedText.LTStringType = '1'
 
"@

          $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName
          # Not all monitors generate an alert.
          If ($AlertDisplayName -like "EMPTYRESULT") {
            If ($ExportCSV) { $AlertName = 'N/A' }
            Else { $AlertName = '<noalertname>N/A</noalertname>' }
          }
          Else {
            If ($ExportCSV) { $AlertName = $AlertDisplayName }
            Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' }
          }
        } #endregion Get_alert_name


        $WFID = $thisWF.ID
        $WFMgmtPackID = $thisWF.GetManagementPack().Name
        # Build the custom object to represent the workflow, add properties/values.
        $myWFObj = New-Object -TypeName System.Management.Automation.PSObject
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Row" -Value $Row
        $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "AlertName" -Value $AlertName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ManagementPackID" -Value $WFMgmtPackID
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $thisWFType
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID

        $myWFCollectionObj += $myWFObj
        $Row++
      }

      Return $myWFCollectionObj
    }

    #------------------------------------------------------------------------------------
    Function fnMamlToHTML{
      
      param
      (
        $MAMLText
      )
      $HTMLText = "";
      $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"');

      $HTMLText = $HTMLText -replace ("<maml:section>");
      $HTMLText = $HTMLText -replace ("<maml:section >");
      $HTMLText = $HTMLText -replace ("</maml:section>");
      $HTMLText = $HTMLText -replace ("<section >");

      #ui = underline. Not going to bother with this html conversion
      $HTMLText = $HTMLText -replace ("<maml:ui>", "");
      $HTMLText = $HTMLText -replace ("</maml:ui>", "");

      #Only convert the maml tables if not exporting to CSV
      IF ($ExportCSV) {
        $HTMLText = $HTMLText -replace ("<maml:para>", " ");
        $HTMLText = $HTMLText -replace ("<maml:para />", " ");
        $HTMLText = $HTMLText -replace ("</maml:para>", " ");
        $HTMLText = $HTMLText -replace ("<maml:title>", "");
        $HTMLText = $HTMLText -replace ("</maml:title>", "");
        $HTMLText = $HTMLText -replace ("<maml:list>", "");
        $HTMLText = $HTMLText -replace ("</maml:list>", "");
        $HTMLText = $HTMLText -replace ("<maml:listitem>", "");
        $HTMLText = $HTMLText -replace ("</maml:listitem>", "");
        $HTMLText = $HTMLText -replace ("<maml:table>", "");
        $HTMLText = $HTMLText -replace ("</maml:table>", "");
        $HTMLText = $HTMLText -replace ("<maml:row>", "");
        $HTMLText = $HTMLText -replace ("</maml:row>", "");
        $HTMLText = $HTMLText -replace ("<maml:entry>", "");
        $HTMLText = $HTMLText -replace ("</maml:entry>", "");
        $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', 'Name Description DefaultValue');
      }
      Else {
        $HTMLText = $HTMLText -replace ("maml:para", "p");
        $HTMLText = $HTMLText -replace ("<maml:table>", "<table>");
        $HTMLText = $HTMLText -replace ("</maml:table>", "</table>");
        $HTMLText = $HTMLText -replace ("<maml:row>", "<tr>");
        $HTMLText = $HTMLText -replace ("</maml:row>", "</tr>");
        $HTMLText = $HTMLText -replace ("<maml:entry>", "<td>");
        $HTMLText = $HTMLText -replace ("</maml:entry>", "</td>");
        $HTMLText = $HTMLText -replace ("<maml:title>", "<h3>");
        $HTMLText = $HTMLText -replace ("</maml:title>", "</h3>");
        $HTMLText = $HTMLText -replace ("<maml:list>", "<ul>");
        $HTMLText = $HTMLText -replace ("</maml:list>", "</ul>");
        $HTMLText = $HTMLText -replace ("<maml:listitem>", "<li>");
        $HTMLText = $HTMLText -replace ("</maml:listitem>", "</li>");
        $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', '<th>Name</th><th>Description</th><th>Default Value</th>');
      }
      # Replace all maml links with html href formatted links
      while ($HTMLText -like "*<maml:navigationLink>*"){
        If ($HtmlText -like "*uri condition*" ) {
          $HTMLText = Fix-HREF -mystring $HTMLText -irregular
        }
        Else {
          $HTMLText = Fix-HREF -mystring $HTMLText
        }
      }
      Return $HTMLText;
    }
    #------------------------------------------------------------------------------------
    Function CleanHTML{
      
      param
      (
        $HTMLText
      )
      $TrimedText = "";
      $TrimedText = $HTMLText -replace ("&lt;", "<")
      $TrimedText = $TrimedText -replace ("&gt;", ">")
      $TrimedText = $TrimedText -replace ("&quot;", '"')
      $TrimedText = $TrimedText -replace ("&amp;", '&')
      $TrimedText;
    }

    #------------------------------------------------------------------------------------
    #Remove maml link formatting, replace with HTML
    Function Fix-HREF{
      Param(
        [string]$mystring,
        [switch]$irregular
      )
      If ($irregular){
        $href_link_tag_begin = '<maml:uri condition'
      }
      Else {
        $href_link_tag_begin = '<maml:uri href="'

        $href_name_tag_begin = '<maml:navigationLink><maml:linkText>'
        $href_name_tag_end = '</maml:linkText>'
        $href_link_tag_end = '" /></maml:navigationLink>'

        $href_name_length = ($mystring.IndexOf($href_name_tag_end)) - ( $mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length)
        $href_name = $mystring.Substring( ($mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length ), $href_name_length)

        $href_link_length = ($mystring.IndexOf($href_link_tag_end)) - ( $mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length -1)
        $href_link = $mystring.Substring( ($mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length ), $href_link_length -1)
      }
      $Chunk_Name = $mystring.Substring( $mystring.IndexOf($href_name_tag_begin), (($mystring.IndexOf($href_name_tag_end) + $href_name_tag_end.Length - 1) - $mystring.IndexOf($href_name_tag_begin) +1 ) )
      $Chunk_HREF = $mystring.Substring( $mystring.IndexOf($href_link_tag_begin), (($mystring.IndexOf($href_link_tag_end) + $href_link_tag_end.Length - 1) - $mystring.IndexOf($href_link_tag_begin) +1 ) )

      If ($irregular){
        $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), '' )
      }
      Else {
        #Example: <a href="http://www.bing.com">here</a> to go to Bing.
        $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), ('<a href="' + $href_link + '">' + "$href_name" + '</a>') )
      }
      Return $newstring
    }

    #------------------------------------------------------------------------------------
    Function Invoke-CLSqlCmd {

      param(
        [string]$Server,
        [string]$Database,
        [string]$Query,
        [int]$QueryTimeout = 30, #The time in seconds to wait for the command to execute. The default is 30 seconds.
        [int]$ConnectionTimeout = 15  #The time (in seconds) to wait for a connection to open. The default value is 15 seconds.
      )
      BEGIN {
      }
      PROCESS {

        try {
          $sqlConnection = New-Object System.Data.SqlClient.SqlConnection
          $sqlConnection.ConnectionString = "Server=$Server;Database=$Database;Trusted_Connection=True;Connection Timeout=$ConnectionTimeout;"
          $sqlConnection.Open()
          try {
            $sqlCmd = New-Object System.Data.SqlClient.SqlCommand
            $sqlCmd.CommandText = $Query
            $sqlCmd.CommandTimeout = $QueryTimeout
            $sqlCmd.Connection = $SqlConnection
            try {
              $Value = @()
              $sqlReader = $sqlCmd.ExecuteReader()
              #The default position of the SqlDataReader is before the first record.
              #Therefore, you must call Read to begin accessing any data.
              #Return Value: true if there are more rows; otherwise false.
              If ($sqlReader.Read()) { # $NUL #function returns true. pipe to nowhere.
                $Value = ($sqlReader.GetValue(0), $sqlReader.GetName(0))
                If ( ($Value[1].ToString().Length -eq 0) -and ($sqlReader.Fieldcount -eq 2) ) {
                  #Write-Host "NoName!" -foreground Yellow -background Red
                  try{
                    $ResultName = $sqlReader.GetValue(1)
                  } finally {
                  }
                  If ($ResultName) {
                    $Value[1]=$ResultName
                  }
                  Else{
                    $Value[1]='Name'
                  }
                }
                #Write-Host "Value2: $Value2"
              }
              Else{
                $Value = ("EMPTYRESULT",'Result')
              }
            }
            finally {
              $sqlReader.Close()
            }
          } finally {
            $sqlCmd.Dispose()
          }
        } finally {
          $sqlConnection.Close()
        }
      }
      END {
        Return $Value[0]
      }
    } #endregion Invoke-CLSqlCmd
    #------------------------------------------------------------------------------------


    #####################################################################################
    $ThisScript = $MyInvocation.MyCommand.Path
    $Rules=@()
    $Monitors=@()
    $InstallDirectory =  (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory
    $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell
    $env:PSModulePath += (";" + "$PoshModulePath")

    # If Topic is provided, make sure it contains an underscore.
    If (($Topic.Length -ge 2) -and (($Topic.IndexOf('_')+1) -ne $Topic.Length) ) {
      $Topic = "$Topic"+"_"
    }

    #Get OpsDB Server name if not provided
    If (!($OpsDBServer)){
      $OpsDBServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseServerName").DatabaseServerName
    }
    #Get OpsDB name if not provided
    If (!($OpsDBName)){
      $OpsDBName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseName").DatabaseName
    }
    # Add 2012 Functionality/cmdlets
    . Import-SCOMPowerShellModule

    If ($Error) {
      $modulepaths=$env:PSModulePath.split(';')
      $Error.Clear()
    }

    # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot.
    If ($MgmtServerFQDN -eq "") {
      #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools)
      $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
      If ($null -eq $objIPProperties.DomainName) {
        $MgmtServerFQDN = $objIPProperties.HostName
      }
      Else {
        $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName
      }
    }
    $Error.Clear()
    #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self?
    Write-Host "Connecting to: $MgmtServerFQDN ..." -ForegroundColor Gray -BackgroundColor Black
    New-SCManagementGroupConnection -Computer $MgmtServerFQDN # | Out-Null
    If ($Error) {
      Write-Host "Failed to connect to: $MgmtServerFQDN ! Exiting. " -ForegroundColor Red -BackgroundColor Yellow
      Exit
    }
    Else {
      Write-Host "Connected to: $MgmtServerFQDN " -ForegroundColor Magenta -BackgroundColor Black
    }

    #Set Culture Info
    $cultureInfo = [System.Globalization.CultureInfo]'en-US'

    $cssHead = @"
<style>
displayname {
    color: blue;
    font: 16px Arial, sans-serif;
}
alertname {
    color: red;
    font: 16px Arial, sans-serif;
}
noalertname {
    color: grey;
    font: 16px Arial, sans-serif;
}
 
name {
    font: 10px Arial, sans-serif;
}
body {
    font: normal 14px Verdana, Arial, sans-serif;
}
 
table, th, td {
    border-collapse: collapse;
    border: 1px solid black;
}
 
th, td {
    padding: 10px;
    text-align: left;
}
tr:hover {background-color: #ccffcc}
 
th {
    background-color: #4CAF50;
    color: white;
}
 
 
</style>
 
"@


    # Make sure output folder exists
    If (-not (Test-Path -Path $OutFolder -PathType Container )) {
      New-Item -Path $OutFolder -ItemType Directory -Force -ErrorAction Stop
    }

    # If output files already exist, remove them
    "Rules.html","Monitors.html" | ForEach-Object {
      If (Test-Path -PathType Leaf (Join-Path $OutFolder $_) ) {
        Remove-Item -Path (Join-Path $OutFolder $_) -Force
      }
    }

  } #endregion Begin

  #region
  Process {

    # If a set of MPs is specified, only process workflows contained in the MPs.
    If ($ManagementPack){

      $ManagementPack | Select-Object DisplayName,Name,ID,Version | Format-Table -AutoSize

      # If filter keyword(s) exist, then filter the results according to the regex patterns submitted in the parameter value.
      If ($Filter) {
        Foreach ($ThisRegex in $Filter) {
          Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow
          $Rules += (Get-SCOMRule -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
        }
      }
      # If no filter(s) exist, then return all rules/mons from the designated MP.
      Else {
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        $Rules += (Get-SCOMRule -ManagementPack $ManagementPack )
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack )
      }
    }
    # Else, get ALL workflows in ALL MPs
    Else {
      # If filter(s) exist, then filter the results according to the regex patterns submitted in the parameter value.
      If ($Filter) {
        Foreach ($ThisRegex in $Filter) {
          Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow
          $Rules += (Get-SCOMRule | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          $Monitors += (Get-SCOMMonitor | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
        }
      }
      Else{
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        $Rules += (Get-SCOMRule)
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        $Monitors += (Get-SCOMMonitor)

      }
    }

  } #endregion

  #region
  End {

    Write-Host "`nTotal Rules Found: $($Rules.Count)" -BackgroundColor Black -ForegroundColor Green
    $myRulesObj = ProcessWorkflows -Workflows $Rules -WFType "Rule"
    If (($ExportCSV)) {
      $myRulesObjTemp = $myRulesObj | ConvertTo-Csv
      $myRulesObj = CleanHTML $myRulesObjTemp | ConvertFrom-CSV
      Write-Host "Exporting rules to CSV: "$(Join-Path $OutFolder ($Topic +"Rules.csv")) -F Cyan
      #Export to CSV
      $myRulesObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic +"Rules.csv")) -NoTypeInformation -Encoding UTF8
    }
    Else {
      $RulesTempContent = $myRulesObj | ConvertTo-HTML -Title "SCOM Rules" -Head $cssHead
      $RulesTempContent =  CleanHTML $RulesTempContent
      $RulesTempContent = $RulesTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      #$RulesTempContent >> (Join-Path $OutFolder ($Topic +"Rules.html") )
      $RulesTempContent | Set-Content (Join-Path $OutFolder ($Topic +"Rules.html")) -Encoding UTF8
    }
    Write-Host "Total Monitors Found: $($Monitors.Count)" -BackgroundColor Black -ForegroundColor Green
    $myMonitorObj = ProcessWorkflows -Workflows $Monitors -WFType "Monitor"
    If (($ExportCSV)) {
      $myMonitorObjTemp = $myMonitorObj | ConvertTo-CSV
      $myMonitorObj = CleanHTML $myMonitorObjTemp | ConvertFrom-CSV
      Write-Host "Exporting monitors to CSV: "$(Join-Path $OutFolder ($Topic + "Monitors.csv")) -F Cyan
      #Export to CSV
      $myMonitorObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic + "Monitors.csv")) -NoTypeInformation -Encoding UTF8
    }
    Else {
      $MonitorTempContent = $myMonitorObj | ConvertTo-HTML  -Title "SCOM Monitors" -Head $cssHead
      $MonitorTempContent =  CleanHTML $MonitorTempContent
      $MonitorTempContent = $MonitorTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      #$MonitorTempContent >> (Join-Path $OutFolder ($Topic + "Monitors.html") )
      $MonitorTempContent | Set-Content (Join-Path $OutFolder ($Topic + "Monitors.html")) -Encoding UTF8
    }
    Write-host "Output folder: $OutFolder" -BackgroundColor Black -ForegroundColor Yellow

    If ($ShowResult) {
      Explorer.exe $OutFolder
    }
  } #endregion
}#End Function#######################################################################

<#
    .Synopsis
    This script will export all SCOM overrides to various file formats.
 
    .DESCRIPTION
    Will export a generous amount of information about overrides. Output may be filtered by management pack and exported to html, csv, xml, and json file formats.
 
 
    .PARAMETER OutDir
    Directory where output files will be created. Folder will get created if it doesn't already exist.
 
    .PARAMETER ExportFormats
    File format. TypeInformation is excluded. "CSV","JSON","HTML","XML"
 
    .PARAMETER CSS
    Any custom CSS for the HTML report. This will be inserted into the <Head> tag of the document. See examples.
 
    .PARAMETER FilterMPNames
    This can be a comma-separated string of management pack names or it can be an array of single names or a combination of both. See examples. The report(s) will only include overrides that exist in these MPs.
 
    .PARAMETER SortBy
    Sort rows by column name.
 
    .PARAMETER SortOrder
    Ascending or descending based on SortBy parameter.
 
    .PARAMETER DateStampFile
    By default the output files will include a datestamp in the name. Specify $false to produce a consistent filename. See examples.
 
 
    .PARAMETER WriteToEventLog
    Will write debugging info to Opsman event log.
 
    .EXAMPLE
    Export-SCOMOverrides -OutDir C:\Reports -ExportFormats HTML -FilterMPNames 'URLGenie.OVERRIDES,OpsMgr.Self.Maintenance.Overrides','PortGenie.OVERRIDES','WindowsService.OVERRIDES' -Verbose
 
    This example demonstrates that you can supply a comma-separated list of MP Names or an array of names or both.
 
    .EXAMPLE
    Export-SCOMOverrides -SortBy ORMPName -SortOrder Ascending -DateStampFile:$false -Verbose
 
 
    .EXAMPLE
    #Example
    PS C:\> [string]$CSS = @'
    <style>
    table, th, td {
    border: 1px solid black;
    border-collapse: collapse;
    }
 
    th, td {
    padding: 5px;
    }
 
    td {
    text-align: center;
    }
    </style>
    '@
     
    PS C:\> Export-SCOMOverrides -SortBy ORMPName -ExportFormats HTML -CSS $CSS -OutDir "C:\Reports"
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/2019/11/12/scomhelper/
    History:
    2020.05.13 - First version
     
    .INPUTS
    Inputs to this cmdlet (if any)
 
    .OUTPUTS
    Will output to std output or up to 4 different output file formats.
 
#>

Function Export-SCOMOverrides { 

  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$true, 
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]

  Param (

    # Folder for exported files (HTML and CSV)
    [Parameter(Mandatory=$true, ParameterSetName='FileExport',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=0)]
    [string]$OutDir,



    # Export Formats: <html | csv | xml | json | all>
    [Parameter(Mandatory=$false, ParameterSetName='FileExport',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=1)]
    [ValidateSet(
        "All",
        "CSV",
        "JSON",
        "HTML",
    "XML")]
    $ExportFormats ,


    # Optional CSS for HTML file export. Will appear in <Head> section of output file.
    [Parameter(Mandatory=$false, ParameterSetName='FileExport',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=2)]
    [string]$CSS = @'
<style>
table, th, td {
  border: 1px solid black;
  border-collapse: collapse;
}
 
th, td {
  padding: 3px;
}
 
td {
  text-align: left;
}
</style>
'@

    ,

    # Comma-separated list of management pack Names or array of names (or both) for which to include overrides from only THESE MPs. Otherwise ALL ORs from ALL MPs will be exported.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=3)]
    [string[]]$FilterMPNames = '',


    #Report rows will be sorted by this Workflow property
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=4)]
    [ValidateSet("ContextDN",
        "ContextInstanceDN",
        "ContextIsGroup",
        "Enforced",
        "ORMPName",
        "ORName",
        "Parameter",
        "Property",
        "Value",
        "Workflow",
        "WorkflowDN",
        "WorkflowMPName",
    "WorkflowType")]
    [string]$SortBy = "WorkflowDN",

    #Formatting of HTML export
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=5)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Ascending", "Descending")]
    [string]$SortOrder = 'Ascending',


    # Will export files with unique datestamp name. <true|false>
    [Parameter(Mandatory=$false, ParameterSetName='FileExport',
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=6)]
    [switch]$DateStampFile = $true,


    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=7)]
    [switch]$WriteToEventLog
  )


  [int]$maxLogLength = 31000 #max chars to allow for event log messages
  [string]$ScriptName = 'Export-Overrides.ps1'
  $ScriptNameInstanceGUID = (New-Guid).Guid.Substring(((New-Guid).Guid.Length ) -6).ToUpper()
  [string]$whoami = whoami.exe

  ########################################################
  Function LogIt {
    Param(
      [switch]$Display,
      [int]$EventID,
      [int]$Line,
      $maxLogLength=31000,
      [string]$msg = '<No message provided>',
      [bool]$Proceed,
      [int]$Type = 2
    ) 

    $output = @"
 
 
Message: $msg
 
ThisScriptInstanceGUID: $ScriptNameInstanceGUID
ScriptLine: $Line
Running As: $whoami
This script: $ScriptName
 
 
Any Errors: $Error
 
"@


    If ($Proceed) 
    {
      $oEvent = New-Object -ComObject 'MOM.ScriptAPI'
      If ($output.Length -gt $maxLogLength){
        $output = ($output.Substring(0,([math]::Min($output.Length,$maxLogLength) )) + '...TRUNCATED...')
      }
      $oEvent.LogScriptEvent($ScriptName,$EventID,$Type,$output )
      #Display output, appropriate for an agent task.
    
    }
    If ($Display ) {
      Write-Verbose $msg
    }
  }
  ###############################################################################
  Function Load-Cache {

    LogIt -msg "Building cache..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display

    # Cache all classes
    LogIt -msg "Getting all Classes..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $AllClasses = Get-SCOMClass
    LogIt -msg "$($AllClasses.Count) found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $hashAllClasses = @{}
    $hashAllClassesID = @{}
    ForEach ($Class in $AllClasses) {
      $hashAllClasses.Add($Class.Name, $Class)
      $hashAllClassesID.Add($Class.ID.GUID,$Class)
    }

    # Cache all monitors
    LogIt -msg "Getting all Monitors..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $AllMonitors = Get-SCOMMonitor
    LogIt -msg "$($AllMonitors.Count) Monitors found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $hashAllMonitors = @{}
    ForEach ($Mon in $AllMonitors) {
      $hashAllMonitors.Add($Mon.Id.Guid, $Mon)
    }

    # Cache all rules
    LogIt -msg "Getting all Rules..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $AllRules = Get-SCOMRule
    LogIt -msg "$($AllRules.Count) Rules found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $hashAllRules = @{}

    ForEach ($Rule in $AllRules) {
      $hashAllRules.Add($Rule.Id.Guid, $Rule)
    }

    # Cache all discoveries
    LogIt -msg "Getting all Discoveries..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $AllDiscoveries = Get-SCOMDiscovery
    LogIt -msg "$($AllDiscoveries.Count) Discoveries found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $hashAllDiscoveries = @{}
    ForEach ($Disc in $AllDiscoveries) {
      $hashAllDiscoveries.Add($Disc.Id.Guid, $Disc)
    }

    # Cache all groups
    LogIt -msg "Getting all Groups..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $AllGroups = Get-SCOMGroup
    LogIt -msg "$($AllGroups.Count) Groups found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    $hashAllGroups = @{}
    ForEach ($Group in $AllGroups) {
      $hashAllGroups.Add($Group.FullName, $Group)
    }
  }

  ########################################################
  Function Load-MPs {
    Param(
      [ValidateSet("Sealed","Unsealed","All")]
      [string]$type="All"
    )
  
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    If ($Reload -eq $true ){ $type = 'All'}
    switch ($type)
    {
      'All' {
        LogIt -msg "Getting ALL MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        $AllMPs = Get-SCOMManagementPack | Where-Object {$_.Name -notin $ExcludedMPs.Values}
        $hashAllMPs = @{}
        $AllMPs | ForEach-Object {
          $hashAllMPs.Add($_.Name,$_)
        }
        $tmpmsg = "$($AllMPs.Count ) MPs found..."
      }

      {$_ -in 'All','Sealed' } {
        LogIt -msg "Getting Sealed MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        $SealedMPs = $AllMPs | Where-Object -FilterScript {$_.Sealed -eq $True } | Where-Object {$_.Name -notin $ExcludedMPs.Values}
        $hashSealedMPs = @{}
        $SealedMPs | ForEach-Object {
          $hashSealedMPs.Add($_.Name,$_)
        }
        $tmpmsg += " $($SealedMPs.Count ) Sealed MPs found..."
      }

      {$_ -in 'All','Unsealed' } {
        LogIt -msg "Getting unsealed MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        $UnsealedMPs = $AllMPs | Where-Object -FilterScript {$_.Sealed -eq $False } | Where-Object {$_.Name -notin $ExcludedMPs.Values}
        $hashUnsealedMPs = @{}
        $UnsealedMPs | ForEach-Object {
          $hashUnsealedMPs.Add($_.Name,$_)
        }      
        $tmpmsg += " $($UnsealedMPs.Count ) unsealed MPs found..."
      }

      Default {Write-Host "Default switch. Problem." -F Yellow}
    }

    [double]$elapsed = $stopwatch.Elapsed.TotalSeconds
    $stopwatch.Stop()
    LogIt -msg "$tmpmsg Operation took $($elapsed) seconds." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display

  }
  ########################################################
  Function Select-Overrides {
    Param(
      [Parameter( Mandatory=$false, 
      ParameterSetName='1')]
      [System.Object[]]$MPs,
    
      [System.Object[]]$Override,
      [Switch]$silent
    )
  
    [System.Collections.ArrayList]$arrayOR = @()

    If ([bool]$MPs) {
      LogIt -msg "`nGetting overrides from $($MPs.Count) unsealed MP(s). This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
      # Return only the overrides from the previously selected MPs
      ForEach ($MP in $MPs) {
        Try{
          $arrayOR += ( ($AllMPs | Where-Object { $_.Name -eq $MP.Name}).GetOverrides() ) 
        }Catch{
          Write-Host "No valid overrides found in MP: $($MP.Name)"
        }
      }
    }
    Else {
      LogIt -msg "`nGetting overrides from $($AllMPs.Count) Management Packs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
      $arrayOR = $AllMPs | ForEach-Object {$_.GetOverrides()}
    }

    If ($arrayOR.Count -eq 0){
      LogIt -msg "No valid overrides found in MP(s). To be valid for selection, ORs must not reference locally defined targets or workflows (in the same unsealed pack)." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    }
  
    # Just set variable here. Function should be dot-sourced when called.
    $ORs = $arrayOR
  }
  ########################################################
  Function Format-OR {
    Param(
      $ORs
    )
    [System.Collections.ArrayList]$arrObject = @()
    [int]$i=0

    LogIt -msg "Begin formatting ORs..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    # 'MonitorPropertyOverride', 'RulePropertyOverride', 'DiscoveryPropertyOverride'
    # 'MonitorConfigurationOverride', 'RuleConfigurationOverride', 'DiscoveryConfigurationOverride'
    # I'm going to have to do this the old-fashioned way
    ForEach ($item in $ORs) {
      $i++
      #Write-Progress -Activity "Formatting Data" -status "$i of ($ORs.Count)" -percentComplete ($i / ($ORs.Count*100))
      $Object = New-Object PSObject

      # Will address Diagnostic and Recovery overrides at a later time. Maybe.
      If ($item.XMLTag -match 'Diagnostic|Recovery' ) { Continue; }

      $Object | add-member Noteproperty ORName ([string]$item.Name)
      $Object | add-member Noteproperty ORMPName ([string]$item.Identifier.Domain[0])
    
      # is Monitor
      If ([bool]$item.Monitor.Identifier.Path.Count) {
        $Object | add-member Noteproperty WorkflowMPName ($item.Monitor.Identifier.Domain[0].ToString())
        $Object | add-member Noteproperty WorkflowType "Monitor"
        $Object | add-member Noteproperty Workflow ($item.Monitor.Identifier.Path[0].ToString())
        #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMMonitor -Id $item.Monitor.Id.Guid).DisplayName )
        $Object | add-member Noteproperty WorkflowDN (($hashAllMonitors[$item.Monitor.Id.Guid]).DisplayName )
        $Object | add-member Noteproperty WorkflowId ([string]$item.Monitor.Id.Guid)
      }
      # is Rule
      ElseIf (($item.Rule.Identifier.Path.Count)) { 
        $Object | add-member Noteproperty WorkflowMPName ($item.Rule.Identifier.Domain[0].ToString())
        $Object | add-member Noteproperty WorkflowType "Rule"
        $Object | add-member Noteproperty Workflow ($item.Rule.Identifier.Path[0].ToString() )
        #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMRule -Id $item.Rule.Id.Guid).DisplayName )
        $Object | add-member Noteproperty WorkflowDN (($hashAllRules[$item.Rule.Id.Guid]).DisplayName )
        $Object | add-member Noteproperty WorkflowId ([string]$item.Rule.Id.Guid)
      } 
      # is Discovery
      ElseIf ([bool]($item.Discovery.Identifier.Path.Count)) { 
        $Object | add-member Noteproperty WorkflowMPName ($item.Discovery.Identifier.Domain[0].ToString())
        $Object | add-member Noteproperty WorkflowType "Discovery"
        $Object | add-member Noteproperty Workflow ($item.Discovery.Identifier.Path[0].ToString()) 
        #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMDiscovery -Id $item.Discovery.Id.Guid).DisplayName )
        $Object | add-member Noteproperty WorkflowDN (($hashAllDiscoveries[$item.Discovery.Id.Guid]).DisplayName )
      
        $Object | add-member Noteproperty WorkflowId ([string]$item.Discovery.Id.Guid)
      } 
      Else {
        $Object | add-member Noteproperty Workflow "ERROR"
        $Object | add-member Noteproperty WorkflowDN "ERROR"
        $Object | add-member Noteproperty WorkflowType "ERROR"
      }

      
      # To be added at some point
      # Diagnostic Task
      # Recovery Task
      # Note: $item.Target.Identifier.Path[0]
      

      If ([bool]($item.Property.Count)) { 
        $Object | add-member Noteproperty Property ($item.Property.ToString() )
        $Object | add-member Noteproperty Parameter ""
      }
      Else { 
        $Object | add-member Noteproperty Property ""
        $Object | add-member Noteproperty Parameter $item.Parameter 
      }
        
      $Object | add-member Noteproperty Value $item.Value

      If ([bool]($hashAllClassesID[$item.Context.Id.Guid])){
        $Object | add-member Noteproperty Context ( $hashAllClassesID[$item.Context.Id.Guid].Name )
        $Object | add-member Noteproperty ContextID ($item.Context.Id.Guid.ToString())
        $Object | add-member Noteproperty ContextDN ( $hashAllClassesID[$item.Context.Id.Guid].DisplayName ) 
      
        If ( $hashAllGroups.ContainsKey($hashAllClassesID[$item.Context.Id.Guid].Name) ) {
          $Object | add-member Noteproperty ContextIsGroup 'True'
        }
        Else {
          $Object | add-member Noteproperty ContextIsGroup 'False'
        }
      }
      #If the Context/Target does not exist, then add the ORName to a dirty list
      Else { 
        Try{
          # Need to figure out what to do with this collection of duds.
          $StaleORNames.Add($item.Name,'Stale')
        } 
        Catch { #typically it's a bad idea to have an empty Catch but it's fine in this situation
        }
        $Object | add-member Noteproperty Context "DOES NOT EXIST - STALE REFERENCE"
        $Object | add-member Noteproperty ContextID ""
        $Object | add-member Noteproperty ContextDN ""
      }

      If ([Bool]($item.ContextInstance.Count)) { 
        $Object | add-member Noteproperty ContextInstanceID ($item.ContextInstance.Guid.ToString() )
        If ([bool]($DN = (Get-SCOMClassInstance -Id $item.ContextInstance.Guid).DisplayName) ) {
          # This might prove to be very time consuming
          $Object | add-member Noteproperty ContextInstanceDN $DN
        } Else{
          $Object | add-member Noteproperty ContextInstanceDN ""
        }

      } 
      Else {
        $Object | add-member Noteproperty ContextInstanceID ""
        $Object | add-member Noteproperty ContextInstanceDN ""
      }

      $Object | add-member Noteproperty Enforced $item.Enforced
      $Object | add-member Noteproperty OR_MPVersion $item.Identifier.Version.ToString()
      $arrObject.Add($Object) | Out-Null
    }
    If ($Object) {
      Remove-Variable -Name Object -ErrorAction SilentlyContinue
    }
    LogIt -msg "Done formatting ORs." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
  
    # Just set variable here. Function should be dot-sourced when called.
    $ORs_F = $arrObject
  }
  ########################################################

  Function Verify-OutDir {
    Param (
      [string]$OutDir
    )
  
    If (-NOT (Test-Path -Path $OutDir -PathType Container )) { 
      Try {
        New-Item -Path $OutDir -ItemType Directory -Force -ErrorAction Stop
        Return $true
      }Catch{
        Write-Error "Unable to verify output directory."
        Return $false
      }
    }
    Else {
      Return (Test-Path -Path $OutDir -PathType Container )
    }
  }

  ########################################################
  Function __LINE__ { 
    $MyInvocation.ScriptLineNumber 
  }
  ########################################################


  #====================================================================================
  #====================================== MAIN ======================================

  [int]$info=0
  [int]$critical=1
  [int]$warn=2

  # This will help control output sort order command
  Switch ($SortOrder ) {
    'Ascending' {
      [BOOL]$SortOrder = $false
    }
    'Descending' {
      [BOOL]$SortOrder = $true
    }
    Default {
      [BOOL]$SortOrder = $false
    }
  }

  # These are MPs that shouldn't be tampered with or consulted.
  $ExcludedMPs =  @{            
    1 = 'Microsoft.SystemCenter.SecureReferenceOverride'
    2 = 'Microsoft.SystemCenter.GlobalServiceMonitor.SecureReferenceOverride'
    3 = 'Microsoft.SystemCenter.Notifications.Internal'
    4 = 'Microsoft.SystemCenter.NetworkDiscovery.Internal'
    5 = 'Microsoft.SystemCenter.Advisor.SecureReferenceOverride'
  } 


  If ($DateStampFile){
    [string]$FileName = "overrides_$(Get-Date -F yyyyMMdd_HHmmss)"
  }
  Else {
    [string]$FileName = "overrides"
  }

  If (-NOT (Verify-OutDir -OutDir $OutDir)) {
    LogIt -msg "Unable to verify export directory [$($OutDir)]. Please run this task with permissions to create the directory or specify a valid path. Exiting." -Type $critical -EventID 9995 -Proceed $true -Line $(__LINE__) -Display
    Exit
  }

  # Functions are dot-sourced (which is perfectly fine) because output statements in the functions are meant to be seen in agent task.
  # Output statements in functions will contaminate function return values.
  . Load-Cache
  
  . Load-MPs -type 'All'

  [System.Collections.ArrayList]$FilteredMPs = @()
  ForEach ($Item in $FilterMPNames) {
    ForEach ($Name in @($Item.Split(',').Split(';').Split(':')) )
    {
      $null = $FilteredMPs.Add($hashAllMPs[$Name])
    }
  }

  #. Load-MPs -type 'All'
  . Select-Overrides -MPs $FilteredMPs

  # Result will populate '$ORs_F'
  . Format-OR -ORs $ORs

 
  If ($ORs_F.Count) {
    $ORs_F = $ORs_F | Sort-Object -Property $SortBy -Descending:$SortOrder 
  }

  If (-NOT $ExportFormats){
    LogIt -msg "Returning overrides to std out." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
    Return $ORs_F
  }
  Else {
    Switch ($ExportFormats) {
      {$_ -match 'all|csv'} { 
        Try{
          $ORs_F | Export-Csv -Path (Join-Path $OutDir "$($FileName).csv") -NoTypeInformation -Force -Encoding UTF8
          LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).csv"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        } Catch {
          LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).csv"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        }
      }

      {$_ -match 'all|html'} { 
        Try{
          $ORs_F | ConvertTo-Html -Head $css | Set-Content -Path (Join-Path $OutDir "$($FileName).html") -Encoding UTF8 -Force
          LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).html"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        } Catch {
          LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).html"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        }
      }
  
      {$_ -match 'all|json'} { 
        Try{
          $ORs_F | ConvertTo-Json | Set-Content -Path (Join-Path $OutDir "$($FileName).json") -Force -Encoding UTF8
          LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).json"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        } Catch {
          LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).json"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        }
      }

      {$_ -match 'all|xml'} { 
        Try{
          $ORs_F | Export-Clixml -Path (Join-Path $OutDir "$($FileName).xml") -Force -Encoding UTF8
          LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).xml"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        } Catch {
          LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).xml"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
        }
      }
    
      default      { 
        LogIt -msg "FAILED Exporting overrides! Something is wrong with `$ExportFormats [$($ExportFormats)]" -Type $critical -EventID 9995 -Proceed $true -Line $(__LINE__) -Display
      }
    }#end switch
  }
  LogIt -msg "Script end ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display
}


#######################################################################
<#
    .DESCRIPTION
    Super fast ping results for large quantities of hosts.
    .EXAMPLE
    Fast-Ping "myserver.domain.com","yourserver.domain.com"
    .EXAMPLE
    Get-Content C:\test\RandomHosts.txt | Select-Object -First 100 | Fast-Ping
    .NOTES
    Author: Tyson Paul
    Original Author: unknown
    Original script: https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/final-super-fast-ping-command
    I modifed this a bit. I added the ability to determine the max number of hostnames to process without
    choking the Get-WmiObject cmdlet, then recursively probe the chunks of the input hosts array.
#>

Function Fast-Ping
{
  param
  (
    # make parameter pipeline-aware
    [Parameter(Mandatory,ValueFromPipeline)]
    
    # one or more computer names (array)
    [string[]]$ComputerName,
        
    $TimeoutMillisec = 4000
  )

  begin
  {
    [int]$maxParam = 16384
    # use this to collect computer names that were sent via pipeline
    [Collections.ArrayList]$bucket = @()
    
    # hash table with error code to text translation
    $StatusCode_ReturnValue = 
    @{
      0 = 'Success'
      11001 = 'Buffer Too Small'
      11002 = 'Destination Net Unreachable'
      11003 = 'Destination Host Unreachable'
      11004 = 'Destination Protocol Unreachable'
      11005 = 'Destination Port Unreachable'
      11006 = 'No Resources'
      11007 = 'Bad Option'
      11008 = 'Hardware Error'
      11009 = 'Packet Too Big'
      11010 = 'Request Timed Out'
      11011 = 'Bad Request'
      11012 = 'Bad Route'
      11013 = 'TimeToLive Expired Transit'
      11014 = 'TimeToLive Expired Reassembly'
      11015 = 'Parameter Problem'
      11016 = 'Source Quench'
      11017 = 'Option Too Big'
      11018 = 'Bad Destination'
      11032 = 'Negotiating IPSEC'
      11050 = 'General Failure'
    }
    
    
    # hash table with calculated property that translates
    # numeric return value into friendly text

    $statusFriendlyText = @{
      Name       = 'Status'
      Expression = { 
        # take status code and use it as index into
        # the hash table with friendly names
        # make sure the key is of same data type (int)
        $StatusCode_ReturnValue[([int]$_.StatusCode)]
      }
    }

    # calculated property that returns $true when status -eq 0
    $IsOnline = @{
      Name       = 'Online'
      Expression = {
        $_.StatusCode -eq 0 
      }
    }

    # do DNS resolution when system responds to ping
    $DNSName = @{
      Name       = 'DNSName'
      Expression = {
        if ($_.StatusCode -eq 0) 
        { 
          if ($_.Address -like '*.*.*.*') 
          {
            [Net.DNS]::GetHostByAddress($_.Address).HostName  
          } 
          else  
          {
            [Net.DNS]::GetHostByName($_.Address).HostName  
          } 
        }
      }
    }
  }
    
  process
  {
    # add each computer name to the bucket
    # we either receive a string array via parameter, or
    # the process block runs multiple times when computer
    # names are piped
    $ComputerName | ForEach-Object -Process {
      $name = $_ -replace ' ',''
      $null = $bucket.Add($name)
    }
  }
    
  end
  {
    # Recursively break down query strings that are too long.
    # Assemble the query with all addresses
    $query = $bucket -join "' or Address='"

        
    If ($query.length -gt $maxParam)
    {
      $tmpQuery = $query.Clone()
      $div = 1
      # If the query is too large, figure out what size chunk of the addresses will be under threshold/max size
      While ($tmpQuery.Length -gt $maxParam)
      {
        $div++
        $tmpQuery = $bucket[0..([math]::Floor(($bucket.Count / $div)))] -join "' or Address='"
      }
        
      # This size chunk of addresses should be small enough, acceptable as a parameter.
      $start = [math]::Floor(($bucket.Count / $div))
      Write-Verbose -Message "Starting size: $start"        
        
      $index = 0
      For($i = $start; $i -le ($bucket.Count -1); $i = ([math]::Min(($i + $start),($bucket.Count -1))) ) 
      {
        Write-Verbose -Message "Range:$($index)-$($i)" 
        $Result += Fast-Ping -ComputerName $bucket[($index)..$i] -TimeoutMillisec $TimeoutMillisec
        If ($i -eq ($bucket.Count -1)) 
        {
          break
        }
        $index = $i+1
      }
    }
        
    Else 
    {
      $Result += Get-WmiObject -Class Win32_PingStatus -Filter "(Address='$query') and timeout=$TimeoutMillisec" | Select-Object -Property Address, $DNSName, $IsOnline, $statusFriendlyText
    }

    Return $Result
  }
}

#######################################################################
<#
    .SYNOPSIS
    This script will get workflow details for one or more alerts, including the Knowledge Article content (neatly formatted in HTML).
 
    .DESCRIPTION
    Accepts one parameter, the alert object array of one or more SCOM alert objects: Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[]. This function can be useful with an automation/ticketing solution.
 
    .PARAMETER Alert
    The alert object(s) to be queried for the workflow details including the Knowledge article (if one exists).
 
    .PARAMETER MgmtServerFQDN
    Fully Qualified Domain Name of the SCOM management server.
    Alias: ManagementServer
 
    .PARAMETER OutputArticleOnly
    Will output only the Knowledge Article(s) content (neatly formatted in HTML). This may abe useful for situations where a service/ticketing connector is used to get alert properties.
 
    .EXAMPLE
    Get-SCOMAlert | Select-Object -First 1 | Get-SCOMAlertKnowledge
    Will display alert info (including Knowledge Article in HTML format) for the first alert object returned.
 
    .EXAMPLE
    PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert | Select-Object -First 3) -OutputArticleOnly
    Will output only the HTML Knowledge Articles for the first 3 alert objects returned.
 
    .EXAMPLE
    PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert -Name *sql*)
    Will output alert info (including Knowledge Article in HTML format) for any alerts with "sql" in the alert name.
 
    .NOTES
    Author: Tyson Paul
    Date: 2016/3/31
    Blog: https://monitoringguys.com/
#>

Function Get-SCOMAlertKnowledge {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      SupportsPaging = $true,
  PositionalBinding=$true)]
  Param(

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[]]$Alert,

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [Alias("ManagementServer")]
    [string]$MgmtServerFQDN = "",

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [Switch]$OutputArticleOnly = $false

  )


  Begin {

    #------------------------------------------------------------------------------------
    Function ProcessArticle {
      Param(
        $article
      )
      If ($article -ne "None")    #some rules don't have any knowledge articles
      {
        #Retrieve and format article content
        $MamlText = $null
        $HtmlText = $null

        If ($null -ne $article.MamlContent)
        {
          $MamlText = $article.MamlContent
          $articleContent = fnMamlToHtml($MamlText)

        }

        If ($null -ne $article.HtmlContent)
        {
          $HtmlText = $article.HtmlContent
          $articleContent = fnTrimHTML($HtmlText)
        }
      }

      If ($null -eq $articleContent)
      {
        $articleContent = "No resolutions were found for this alert."
      }

      Return $articleContent
    }

    #------------------------------------------------------------------------------------
    Function ProcessWorkflow {
      Param(
        $Workflows,
        [string]$WFType
      )
      $myWFCollectionObj = @()

      ForEach ($thisWF in $Workflows) {
        $ErrorActionPreference = 'SilentlyContinue'
        $article = $thisWF.GetKnowledgeArticle($cultureInfo)
        If ($? -eq $false){
          $error.Remove($Error[0])
          $article = "None"
        }
        Else{
          $articleContent = ProcessArticle $article
        }

        $WFName = $thisWF.Name
        $WFDisplayName = $thisWF.DisplayName
        $WFDescription = $thisWF.Description
        $WFID = $thisWF.ID
        If ($WFDescription.Length -lt 1) {$WFDescription = "None"}

        #Note: Only a small subset of alert properties are gathered here. Add additional Note Properties as needed using the format below.
        $myWFObj = New-Object -TypeName System.Management.Automation.PSObject
        $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $WFType
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID
        $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent
        $myWFCollectionObj += $myWFObj
      }

      Return $myWFCollectionObj
    }

    #------------------------------------------------------------------------------------
    Function fnMamlToHTML{
      
      param
      (
        $MAMLText
      )
      $HTMLText = "";
      $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"');
      $HTMLText = $HTMLText -replace ("maml:para", "p");
      $HTMLText = $HTMLText -replace ("maml:");
      $HTMLText = $HTMLText -replace ("</section>");
      $HTMLText = $HTMLText -replace ("<section>");
      $HTMLText = $HTMLText -replace ("<section >");
      $HTMLText = $HTMLText -replace ("<title>", "<h3>");
      $HTMLText = $HTMLText -replace ("</title>", "</h3>");
      $HTMLText = $HTMLText -replace ("<listitem>", "<li>");
      $HTMLText = $HTMLText -replace ("</listitem>", "</li>");
      $HTMLText;
    }
    #------------------------------------------------------------------------------------
    Function fnTrimHTML($HTMLText){
      $TrimedText = "";
      $TrimedText = $HTMLText -replace ("&lt;", "<")
      $TrimedText = $TrimedText -replace ("&gt;", ">")
      <# $TrimedText = $TrimedText -replace ("<html>")
          $TrimedText = $TrimedText -replace ("<HTML>")
          $TrimedText = $TrimedText -replace ("</html>")
          $TrimedText = $TrimedText -replace ("</HTML>")
          $TrimedText = $TrimedText -replace ("<body>")
          $TrimedText = $TrimedText -replace ("<BODY>")
          $TrimedText = $TrimedText -replace ("</body>")
          $TrimedText = $TrimedText -replace ("</BODY>")
          $TrimedText = $TrimedText -replace ("<h1>", "<h3>")
          $TrimedText = $TrimedText -replace ("</h1>", "</h3>")
          $TrimedText = $TrimedText -replace ("<h2>", "<h3>")
          $TrimedText = $TrimedText -replace ("</h2>", "</h3>")
          $TrimedText = $TrimedText -replace ("<H1>", "<h3>")
          $TrimedText = $TrimedText -replace ("</H1>", "</h3>")
          $TrimedText = $TrimedText -replace ("<H2>", "<h3>")
          $TrimedText = $TrimedText -replace ("</H2>", "</h3>")
      #>

      $TrimedText;
    }
    #------------------------------------------------------------------------------------

    ###########################################################################################


    $ThisScript = $MyInvocation.MyCommand.Path
    $InstallDirectory =  (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory
    $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell
    $env:PSModulePath += (";" + "$PoshModulePath")

    # Add 2012 Functionality/cmdlets
    . Import-SCOMPowerShellModule

    If ($Error) {
      $modulepaths=$env:PSModulePath.split(';')
      $Error.Clear()
    }

    # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot.
    If ($MgmtServerFQDN -eq "") {
      #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools)
      $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
      If ($null -eq $objIPProperties.DomainName) {
        $MgmtServerFQDN = $objIPProperties.HostName
      }
      Else {
        $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName
      }
    }

    #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self?
    New-SCManagementGroupConnection -Computer $MgmtServerFQDN | Out-Null
    # If ($Error) { LogIt -EventID 9995 -Type $warn -Message "Log any Errors..." ; $Error.Clear() }

    #Set Culture Info
    $cultureInfo = [System.Globalization.CultureInfo]'en-US'
    $objWFCollection = @()
  }

  Process{
    #Depending on how the alert object(s) are passed in, the ForEach may be needed. (parameter vs. piped)
    ForEach ($objAlert in $Alert)
    {
      $workflowID = $objAlert.MonitoringRuleId
      $bIsMonitorAlert = $objAlert.IsMonitorAlert
      If ($bIsMonitorAlert -eq $false) {
        $WFType = "Rule"
        $workflow = Get-SCOMRule -Id $workflowID
      }
      ElseIf ($bIsMonitorAlert -eq $true) {
        $WFType = "Monitor"
        $workflow = Get-SCOMMonitor -Id $workflowID
      }

      # The funciton being called is designed to accept one or more workflows.
      # It will return one or more custom objects with workfow details, including (most importantly) the KnowlegeArticle.
      $objWFCollection += ProcessWorkflow -Workflows $Workflow -WFType $WFType
    }
  } #End Process

  End{
    If ($OutputArticleOnly){
      Return $objWFCollection.KnowledgeArticle
    }
    Else{
      Return $objWFCollection
    }
  }
}#End Function
#######################################################################

<#
    .Synopsis
    Will display statistics about the total number of SCOM class instances and the management packs from which they originate.
    .EXAMPLE
    Get-SCOMClassInfo -Top 10
    .EXAMPLE
    Get-SCOMClassInfo -Top 30 -MPStats
    .EXAMPLE
    Get-SCOMClassInfo -MPStats -ShowGraph
    .INPUTS
    None
    .OUTPUTS
    Custom Object: {InstanceCount,ClassName,MPName}, {MPName,TotalInstances}
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Version: 1.0
    Original Date: 2018.05.25
    .FUNCTIONALITY
    Statistical reporting.
    .LINK
    Get-SCOMMPFileInfo
    New-SCOMClassGraph
    Unseal-SCOMMP
#>

Function Get-SCOMClassInfo {
  Param (
    #Will return top N results by InstanceCount
    [int]$Top=30,
    [switch]$MPStats,
    [switch]$ShowGraph
  )

  [System.Collections.ArrayList]$arrClasses = @()
  # Get all MPs that were not installed OotB.
  $MPs = Get-SCManagementPack
  $MPs = $MPs | Sort-Object TimeCreated | Select-Object -Last ($MPs.Count - 98)
  $Classes = Get-SCClass -ManagementPack $MPs | Where-Object {($_.Singleton -eq $false) -AND ($_.Abstract -eq $false) }
  Class ClassStats{
    [int]$Instances
    [string]$ClassName
    [string]$ClassDisplayName
    [string]$MPName    
  }
  
  ForEach ($Class in $Classes) {
    $Instances = $Class | Get-SCOMClassInstance
    [ClassStats]$obj = @{
      Instances = $Instances.Count
      ClassName = $Class.Name
      ClassDisplayName = $Class.DisplayName
      MPName = ($Class.Identifier.Domain[0])
    }
    $Null = $arrClasses.Add($obj)
  }
  If ($MPStats){
    $Results = (($arrClasses | Group-Object -Property MPName ) | Select-Object @{Name='MPName';E={$_.Name} },@{Name='TotalInstances';E={($_.Group.Instances | Measure-Object -Sum).Sum}} |Sort-Object TotalInstances -Descending | Select-Object -First $Top)
    If ([bool]$ShowGraph) {
      $Results | Out-ConsoleGraph -Property TotalInstances -Columns TotalInstances,MPName
    }
    Else {
      Return $Results
    }
  }
  Else{
    $Results = ($arrClasses | Sort-Object Instances -Descending | Select-Object -First $Top)
    If ([bool]$ShowGraph) {
      $Results | Out-ConsoleGraph -Property Instances -Columns Instances,ClassName
    }
    Else {
      Return $Results
    }
  }
} #End Function

#######################################################################

<#
    .Synopsis
 
    .DESCRIPTION
    Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script
    which can be found here:
    http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/
 
    I have been meaning to write something similiar for awhile so decided to take his wonderfully written
    script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful
    SQL Queries that I frequently find myself asking customers to run when they are experiencing performance
    issues with the Ops & DW DB's.
 
    I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim!
 
    MBullwin 11/3/2014
    www.OpsConfig.com
 
    [ https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec ]
    As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk.
 
    What this version of the script will give you:(Some of these are just features which are carried over from the original, many are added)
 
    01. Version/Service Pack/Edition of SQL for each SCOM DB Server
 
    02. Disk Space Info for Ops DB, DW DB, and associated Temp DB's
 
    03. Database Backup Status for all DB's except Temp.
 
    04. Top 25 Largest Tables for Ops DB and DW DB
 
    05. Number of Events Generated Per Day (Ops DB)
 
    06. Top 10 Event Generating Computers (Ops DB)
 
    07. Top 25 Events by Publisher (Ops DB)
 
    08. Number of Perf Insertions Per Day (Ops DB)
 
    09. Top 25 Perf Insertions by Object/Counter Name (Ops DB)
 
    10. Top 25 Alerts by Alert Count
 
    11. Alerts with a Repeat Count higher than 200
 
    12. Stale State Change Data
 
    13. Top 25 Monitors Changing State in the last 7 Days
 
    14. Top 25 Monitors Changing State By Object
 
    15. Ops DB Grooming History
 
    16. Snapshot of DW Staging Tables
 
    17. DW Grooming Retention
 
    18. Management Server checks (Works well on prem, seems to have some issues with gateways due to remote calls-if you see some errors flash by have no fear though I wouldn't necessarily trust the results coming back from a Gateway server in the report depending on firewall settings)
 
    19. Daily KPI
 
    20. MP's Modified in the Last 24 hours
 
    21. Overrides in Default MP Check
 
    22. Unintialized Agents
 
    23. Agent Stats (Healthy, Warning, Critical, Unintialized, Total)
 
    24. Agent Pending Management Summary
 
    25. Alert Summary
 
    26. Servers in Maintenance Mode
 
    For more details on this version checkout: www.OpsConfig.com
    .NOTES
    Author: MBullwin
    Blog: www.OpsConfig.com
    Version: v1.1
    Date: 11/3/2014
    Original Script: https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec
 
    Modifications by Tyson:
    23020.08.11 - Added ManagementServer param. It's not fancy, but better than hardcoded localhost.
 
#>

Function Get-SCOMHealthCheckOpsConfig {

  Param (
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$ManagementServer='Localhost'
  )
  ###################################################################################################################
  #
  # Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script
  # which can be found here:
  #
  # http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/
  #
  # I have been meaning to write something similiar for awhile so decided to take his wonderfully written
  # script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful
  # SQL Queries that I frequently find myself asking customers to run when they are experiencing performance
  # issues with the Ops & DW DB's.
  #
  # I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim!
  #
  #
  # MBullwin 11/3/2014
  # www.OpsConfig.com
  #
  # As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk.
  ######################################################################################################################
  $StartTime=Get-Date

  . Import-SCOMPowerShellModule

  # Connect to localhost when running on the management server or define a server to connect to.
  $connect = New-SCManagementGroupConnection -ComputerName $ManagementServer

  # The Name and Location of are we going to save this Report
  $CreateReportLocation = [System.IO.Directory]::CreateDirectory("c:\SCOM_Health_Check")
  $ReportLocation = "c:\SCOM_Health_Check"
  $ReportName = "$(get-date -format "yyyy-M-dd")-SCOM-HealthCheck.html"
  $ReportPath = "$ReportLocation\$ReportName"

  # Create header for HTML Report
  $Head = "<style>"
  $Head +="BODY{background-color:#CCCCCC;font-family:Calibri,sans-serif; font-size: small;}"
  $Head +="TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse; width: 98%;}"
  $Head +="TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#293956;color:white;padding: 5px; font-weight: bold;text-align:left;}"
  $Head +="TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#F0F0F0; padding: 2px;}"
  $Head +="</style>"


  # Retrieve the name of the Operational Database and Data WareHouse Servers from the registry.
  $reg = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup\"
  $OperationsManagerDBServer = $reg.DatabaseServerName
  $OperationsManagerDWServer = $reg.DataWarehouseDBServerName
  # If the value is empty in this key, then we'll use the Get-SCOMDataWarehouseSetting cmdlet.
  If (!($OperationsManagerDWServer))
  {$OperationsManagerDWServer = Get-SCOMDataWarehouseSetting | Select-Object -expandProperty DataWarehouseServerName}

  $OperationsManagerDBServer = $OperationsManagerDBServer.ToUpper()
  $OperationsManagerDWServer = $OperationsManagerDWServer.ToUpper()

  $ReportingURL = Get-SCOMReportingSetting | Select-Object -ExpandProperty ReportingServerUrl
  $WebConsoleURL = Get-SCOMWebAddressSetting | Select-Object -ExpandProperty WebConsoleUrl
  <#
      # The number of days before Database Grooming
      # These are my settings, I use this to determine if someone has changed something
      # Feel free to comment this part out if you aren't interested
      $AlertDaysToKeep = 2
      $AvailabilityHistoryDaysToKeep = 2
      $EventDaysToKeep = 1
      $JobStatusDaysToKeep = 1
      $MaintenanceModeHistoryDaysToKeep = 2
      $MonitoringJobDaysToKeep = 2
      $PerformanceDataDaysToKeep = 2
      $StateChangeEventDaysToKeep = 2
 
      # SCOM Agent Heartbeat Settings
      $AgentHeartbeatInterval = 180
      $MissingHeartbeatThreshold = 3
  #>


  # SQL Server Function to query the Operational Database Server
  function Run-OpDBSQLQuery
  {
    Param($sqlquery)

    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
    $SqlConnection.ConnectionString = "Server=$OperationsManagerDBServer;Database=OperationsManager;Integrated Security=True"
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
    $SqlCmd.CommandText = $sqlquery
    $SqlCmd.Connection = $SqlConnection
    $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
    $SqlAdapter.SelectCommand = $SqlCmd
    $DataSet = New-Object System.Data.DataSet
    $SqlAdapter.Fill($DataSet) | Out-Null
    $SqlConnection.Close()
    $DataSet.Tables[0]
  }


  # SQL Server Function to query the Data Warehouse Database Server
  function Run-OpDWSQLQuery
  {
    Param($sqlquery)

    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
    $SqlConnection.ConnectionString = "Server=$OperationsManagerDWServer;Database=OperationsManagerDW;Integrated Security=True"
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
    $SqlCmd.CommandText = $sqlquery
    $SqlCmd.Connection = $SqlConnection
    $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
    $SqlAdapter.SelectCommand = $SqlCmd
    $DataSet = New-Object System.Data.DataSet
    $SqlAdapter.Fill($DataSet) | Out-Null
    $SqlConnection.Close()
    $DataSet.Tables[0]
  }


  # Retrieve the Data for the Majority of the Report
  # Truth is we probably don't need all of this data, but even on a busy environment it only takes a couple of mins to run.
  Write-Host "Retrieving Agents"
  $Agents = Get-SCOMAgent
  Write-Host "Retrieving Alerts"
  $Alerts = Get-SCOMAlert
  Write-Host "Retrieving Groups"
  $Groups = Get-SCOMGroup
  Write-Host "Retrieving Management Group"
  $ManagementGroup = Get-SCOMManagementGroup
  Write-Host "Retrieving Management Packs"
  $ManagementPacks = Get-SCManagementPack
  Write-Host "Retrieving Management Servers"
  $ManagementServers = Get-SCOMManagementServer
  Write-Host "Retrieving Monitors"
  $Monitors = Get-SCOMMonitor
  Write-Host "Retrieving Rules"
  $Rules = Get-SCOMRule

  # Check to see if the Reporting Server Site is OK
  $ReportingServerSite = New-Object System.Net.WebClient
  $ReportingServerSite = [net.WebRequest]::Create($ReportingURL)
  $ReportingServerSite.UseDefaultCredentials = $true
  $ReportingServerStatus = $ReportingServerSite.GetResponse() | Select-Object -expandProperty statusCode
  # This code can convert the "OK" Result to an Integer, like 200
  # (($web.GetResponse()).Statuscode) -as [int]

  # Check to see if the Web Server Site is OK
  $WebConsoleSite = New-Object System.Net.WebClient
  $WebConsoleSite = [net.WebRequest]::Create($WebConsoleURL)
  $WebConsoleSite.UseDefaultCredentials = $true
  $WebConsoleStatus = $WebConsoleSite.GetResponse() | Select-Object -expandProperty statusCode

  # SQL Server Function to query Size of the Database Server
  $DatabaseSize = @"
select a.FILEID,
[FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)),
[SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)),
[FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) ,
[GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)),
NAME=left(a.NAME,15),
FILENAME=left(a.FILENAME,60)
from dbo.sysfiles a
"@


  #SQL Server Function to query Size of the TempDB
  $TempDBSize =@"
USE tempdb
select a.FILEID,
[FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)),
[SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)),
[FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) ,
[GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)),
NAME=left(a.NAME,15),
FILENAME=left(a.FILENAME,60)
from dbo.sysfiles a
"@


  #SQL Server Function to query the version of SQL
  $SQLVersion =@"
SELECT SERVERPROPERTY('productversion') AS "Product Version", SERVERPROPERTY('productlevel') AS "Service Pack", SERVERPROPERTY ('edition') AS "Edition"
"@


  # Run the Size Query against the Operational Database and Data Warehouse Database Servers
  $OPDBSize = Run-OpDBSQLQuery $DatabaseSize
  $DWDBSize = Run-OpDWSQLQuery $DatabaseSize
  $OPTPSize = Run-OpDBSQLQuery $TempDBSize
  $DWTPSize = Run-OpDWSQLQuery $TempDBSize
  $OPSQLVER = Run-OpDBSQLQuery $SQLVersion
  $DWSQLVER = Run-OpDWSQLQuery $SQLVersion

  # Insert the Database Server details into the Report
  $ReportOutput += "<h2>Database Servers</h2>"
  $ReportOutput += "<p>Operational Database Server : $OperationsManagerDBServer</p>"
  $ReportOutput += $OPSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment
  $ReportOutput += "<p>Data Warehouse Database Server : $OperationsManagerDWServer</p>"
  $ReportOutput += $DWSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment

  # Insert the Size Results for the Operational Database into the Report
  $ReportOutput += "<h3>$OperationsManagerDBServer Operations Manager DB</h4>"
  $ReportOutput += $OPDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment
  $ReportOutput += "<br></br>"
  $ReportOutput += "<h3>Operations Temp DB</h4>"
  $ReportOutput += $OPTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment

  # Insert the Size Results for the Data Warehouse Database and TempDB into the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<h3>$OperationsManagerDWServer Data Warehouse DB</h4>"
  $ReportOutput += $DWDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment
  $ReportOutput += "<br></br>"
  $ReportOutput += "<h3>Data Warehouse Temp DB</h4>"
  $ReportOutput += $DWTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment

  # SQL Query to find out how many State Changes there were yesterday
  $StateChangesYesterday = @"
-- How Many State Changes Yesterday?:
select count(*) from StateChangeEvent
where cast(TimeGenerated as date) = cast(getdate()-1 as date)
"@


  $StateChanges = Run-OpDBSQLQuery $StateChangesYesterday | Select-Object -ExpandProperty Column1 | Out-String

  $AllStats = @()

  $StatSummary = New-Object psobject
  $StatSummary | Add-Member noteproperty "Open Alerts" (($Alerts | Where-Object {$_.ResolutionState -ne 255}).count)
  $StatSummary | Add-Member noteproperty "Groups" ($Groups.Count)
  $StatSummary | Add-Member noteproperty "Monitors" ($Monitors.Count)
  $StatSummary | Add-Member noteproperty "Rules" ($Rules.Count)
  $StatSummary | Add-Member noteproperty "State Changes Yesterday" ($StateChanges | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()})

  $AllStats += $StatSummary


  #SQL Query Top 10 Event generating computers
  $TopEventGeneratingComputers = @"
SELECT top 10 LoggingComputer, COUNT(*) AS TotalEvents
FROM EventallView
GROUP BY LoggingComputer
ORDER BY TotalEvents DESC
"@


  #SQL Query number of Events Generated per day
  $NumberOfEventsPerDay = @"
SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeAdded, 101)) = 1)
THEN 'All Days'
ELSE CONVERT(VARCHAR(20), TimeAdded, 101) END AS DayAdded,
COUNT(*) AS NumEventsPerDay
FROM EventAllView
GROUP BY CONVERT(VARCHAR(20), TimeAdded, 101) WITH ROLLUP
ORDER BY DayAdded DESC
"@


  #SQL Query Most Common Events by Publishername
  $MostCommonEventsByPub = @"
SELECT top 25 Number AS "Event Number", Publishername, COUNT(*) AS TotalEvents
FROM EventAllView
GROUP BY Number, Publishername
ORDER BY TotalEvents DESC
"@


  #SQL Query the Number of Performance Insertions per Day
  $NumberofPerInsertsPerDay = @"
SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeSampled, 101)) = 1)
THEN 'All Days' ELSE CONVERT(VARCHAR(20), TimeSampled, 101)
END AS DaySampled, COUNT(*) AS NumPerfPerDay
FROM PerformanceDataAllView
GROUP BY CONVERT(VARCHAR(20), TimeSampled, 101) WITH ROLLUP
ORDER BY DaySampled DESC
"@


  #SQL Query the Most common perf insertions by perf counter name
  $MostCommonPerfByN = @"
select top 25 pcv.objectname, pcv.countername, count (pcv.countername) as total from
performancedataallview as pdv, performancecounterview as pcv
where (pdv.performancesourceinternalid = pcv.performancesourceinternalid)
group by pcv.objectname, pcv.countername
order by count (pcv.countername) desc
"@


  #SQL Query the Top 25 Alerts by Alert Count
  $MostCommonAByAC = @"
SELECT Top 25 AlertStringName, Name, SUM(1) AS
AlertCount, SUM(RepeatCount+1) AS AlertCountWithRepeatCount
FROM Alertview WITH (NOLOCK)
GROUP BY AlertStringName, Name
ORDER BY AlertCount DESC
"@


  #SQL Query for Stale State Change Data
  $StaleStateChangeData = @"
declare @statedaystokeep INT
SELECT @statedaystokeep = DaysToKeep from PartitionAndGroomingSettings WHERE ObjectName = 'StateChangeEvent'
SELECT COUNT(*) as 'Total StateChanges',
count(CASE WHEN sce.TimeGenerated > dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as 'within grooming retention',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> grooming retention',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-30,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 30 days',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-90,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 90 days',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-365,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 365 days'
from StateChangeEvent sce
"@


  #SQL Query Noisest monitors changing state in the past 7 days
  $NoisyMonitorData = @"
select distinct top 25
m.DisplayName as MonitorDisplayName,
m.Name as MonitorIdName,
mt.typename AS TargetClass,
count(sce.StateId) as NumStateChanges
from StateChangeEvent sce with (nolock)
join state s with (nolock) on sce.StateId = s.StateId
join monitorview m with (nolock) on s.MonitorId = m.Id
join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId
where m.IsUnitMonitor = 1
  -- Scoped to within last 7 days
AND sce.TimeGenerated > dateadd(dd,-7,getutcdate())
group by m.DisplayName, m.Name,mt.typename
order by NumStateChanges desc
"@


  #SQL Query Top 25 Monitors changing state by Object
  $NoisyMonitorByObject =@"
select distinct top 25
bme.DisplayName AS ObjectName,
bme.Path,
m.DisplayName as MonitorDisplayName,
m.Name as MonitorIdName,
mt.typename AS TargetClass,
count(sce.StateId) as NumStateChanges
from StateChangeEvent sce with (nolock)
join state s with (nolock) on sce.StateId = s.StateId
join BaseManagedEntity bme with (nolock) on s.BasemanagedEntityId = bme.BasemanagedEntityId
join MonitorView m with (nolock) on s.MonitorId = m.Id
join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId
where m.IsUnitMonitor = 1
   -- Scoped to specific Monitor (remove the "--" below):
   -- AND m.MonitorName like ('%HealthService%')
   -- Scoped to specific Computer (remove the "--" below):
   -- AND bme.Path like ('%sql%')
   -- Scoped to within last 7 days
AND sce.TimeGenerated > dateadd(dd,-7,getutcdate())
group by s.BasemanagedEntityId,bme.DisplayName,bme.Path,m.DisplayName,m.Name,mt.typename
order by NumStateChanges desc
"@


  #SQL Query Grooming Settings for the Operational Database
  $OpsDBGrooming =@"
SELECT
ObjectName,
GroomingSproc,
DaysToKeep,
GroomingRunTime,
DataGroomedMaxTime
FROM PartitionAndGroomingSettings WITH (NOLOCK)
"@



  #SQL Query DW DB Staging Tables
  $DWDBStagingTables = @"
select count(*) AS "Alert Staging Table" from Alert.AlertStage
"@


  $DWDBStagingTablesEvent =@"
select count (*) AS "Event Staging Table" from Event.eventstage
"@


  $DWDBStagingTablesPerf =@"
select count (*) AS "Perf Staging Table" from Perf.PerformanceStage
"@


  $DWDBStagingTablesState =@"
select count (*) AS "State Staging Table" from state.statestage
"@


  #SQL Query DW Grooming Retention
  $DWDBGroomingRetention =@"
select ds.datasetDefaultName AS 'Dataset Name', sda.AggregationTypeId AS 'Agg Type 0=raw, 20=Hourly, 30=Daily', sda.MaxDataAgeDays AS 'Retention Time in Days'
from dataset ds, StandardDatasetAggregation sda
WHERE ds.datasetid = sda.datasetid ORDER by "Retention Time in Days" desc
"@


  #SQL function to Query the Top 25 largest tables in a database
  $DWDBLargestTables =@"
SELECT TOP 25
a2.name AS [tablename], (a1.reserved + ISNULL(a4.reserved,0))* 8 AS reserved,
a1.rows as row_count, a1.data * 8 AS data,
(CASE WHEN (a1.used + ISNULL(a4.used,0)) > a1.data THEN (a1.used + ISNULL(a4.used,0)) - a1.data ELSE 0 END) * 8 AS index_size,
(CASE WHEN (a1.reserved + ISNULL(a4.reserved,0)) > a1.used THEN (a1.reserved + ISNULL(a4.reserved,0)) - a1.used ELSE 0 END) * 8 AS unused,
(row_number() over(order by (a1.reserved + ISNULL(a4.reserved,0)) desc))%2 as l1,
a3.name AS [schemaname]
FROM (SELECT ps.object_id, SUM (CASE WHEN (ps.index_id < 2) THEN row_count ELSE 0 END) AS [rows],
SUM (ps.reserved_page_count) AS reserved,
SUM (CASE WHEN (ps.index_id < 2) THEN (ps.in_row_data_page_count + ps.lob_used_page_count + ps.row_overflow_used_page_count)
ELSE (ps.lob_used_page_count + ps.row_overflow_used_page_count) END ) AS data,
SUM (ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
GROUP BY ps.object_id) AS a1
LEFT OUTER JOIN (SELECT it.parent_id,
SUM(ps.reserved_page_count) AS reserved,
SUM(ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
INNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id)
WHERE it.internal_type IN (202,204)
GROUP BY it.parent_id) AS a4 ON (a4.parent_id = a1.object_id)
INNER JOIN sys.all_objects a2 ON ( a1.object_id = a2.object_id )
INNER JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id)
WHERE a2.type <> N'S' and a2.type <> N'IT'
"@


  #SQL Function to query and check backup status of SQL Databases
  $SQLBackupStatus =@"
SELECT
d.name,
DATEDIFF(Day, COALESCE(MAX(b.backup_finish_date), d.create_date), GETDATE()) AS [DaysSinceBackup]
FROM
sys.databases d
LEFT OUTER JOIN msdb.dbo.backupset b
ON d.name = b.database_name
WHERE
d.is_in_standby = 0
AND source_database_id is null
AND d.name NOT LIKE 'tempdb'
AND (b.[type] IN ('D', 'I') OR b.[type] IS NULL)
GROUP BY
d.name, d.create_date
"@


  # Run additional SQL Queries against the Operational Database
  $OPTOPALERT = Run-OpDBSQLQuery $TopEventGeneratingComputers
  $OPNUMEPERDAY = Run-OpDBSQLQuery $NumberOfEventsPerDay
  $OPMOSTCOMEVENT = Run-OpDBSQLQuery $MostCommonEventsByPub
  $OPPERFIPERD = Run-OpDBSQLQuery $NumberofPerInsertsPerDay
  $OPPERFIBYN = Run-OpDBSQLQuery $MostCommonPerfByN
  $OPTOPALERTC = Run-OpDBSQLQuery $MostCommonAByAC
  $OPSTALESTD = Run-OpDBSQLQuery $StaleStateChangeData
  $OPNOISYMON = Run-OpDBSQLQuery $NoisyMonitorData
  $OPNOISYMONOBJ = Run-OpDBSQLQuery $NoisyMonitorByObject
  $OPDBGROOM = Run-OpDBSQLQuery $OpsDBGrooming
  $OPLARGTAB = Run-OpDBSQLQuery $DWDBLargestTables
  $OPDBBACKUP = Run-OpDBSQLQuery $SQLBackupStatus

  #Run additional SQL Queries against the DW DB
  $DWDBSGTB = Run-OpDWSQLQuery $DWDBStagingTables
  $DWDBSGTBEV = Run-OpDWSQLQuery $DWDBStagingTablesEvent
  $DWDBSGTBPE = Run-OpDWSQLQuery $DWDBStagingTablesPerf
  $DWDBSGTBST = Run-OpDWSQLQuery $DWDBStagingTablesState
  $DWDBGRET = Run-OpDWSQLQuery $DWDBGroomingRetention
  $DWDBLARGETAB = Run-OpDWSQLQuery $DWDBLargestTables
  $DWDBBACKUP = Run-OpDWSQLQuery $SQLBackupStatus

  #Output to HTML Report

  $ReportOutput += "<h2>Operational Database Health</h2>"
  $ReportOutput += "<h3>Operations Database Backup Status</h3>"
  $ReportOutput += $OPDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Operations Database Top 25 Largest Tables</h3>"
  $ReportOutput += $OPLARGTAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Number of Events Generated Per Day</h3>"
  $ReportOutput += $OPNUMEPERDAY | Select-Object NumEventsPerDay, DayAdded | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Top 10 Event Generating Computers</h3>"
  $ReportOutput += $OPTOPALERT | Select-Object LoggingComputer, TotalEvents | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Top 25 Events By Publisher</h3>"
  $ReportOutput += $OPMOSTCOMEVENT | Select-Object "Event Number", Publishername, TotalEvents | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Number of Perf Insertions Per Day</h3>"
  $ReportOutput += $OPPERFIPERD | Select-Object DaySampled, NumPerfPerDay | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Perf Insertions by Object/Counter Name</h3>"
  $ReportOutput += $OPPERFIBYN | Select-Object objectname, countername, total | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Alerts by Alert Count</h3>"
  $ReportOutput += $OPTOPALERTC | Select-Object AlertStringName, Name, AlertCount, AlertCountWithRepeatCount | ConvertTo-Html -Fragment



  # Get the alerts with a repeat count higher than the variable $RepeatCount
  $RepeatCount = 200

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Alerts with a Repeat Count higher than $RepeatCount</h3>"


  # Produce a table of all Open Alerts above the repeatcount and add it to the Report
  $ReportOutput += $Alerts | Where-Object {$_.RepeatCount -ge $RepeatCount -and $_.ResolutionState -ne 255} | Select-Object Name, Category, NetBIOSComputerName, RepeatCount | Sort-Object repeatcount -desc | ConvertTo-HTML -fragment

  #Output to HTML report
  $ReportOutput += "<h3>Stale State Change Data</h3>"
  $ReportOutput += $OPSTALESTD | Select-Object "Total StateChanges", "within grooming retention", "> grooming retention","> 30 days","> 90 days","> 365 days"| ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Monitors Changing State in the last 7 Days</h3>"
  $ReportOutput += $OPNOISYMON | Select-Object MonitorDisplayName, MonitorIdName, TargetClass, NumStateChanges | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Monitors Changing State By Object</h3>"
  $ReportOutput += $OPNOISYMONOBJ | Select-Object ObjectName, Path, MonitorDisplayName, MonitorIdName,TargetClass, NumStateChanges | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Operations Database Grooming History</h3>"
  $ReportOutput += $OPDBGROOM | Select-Object ObjectName, GroomingSproc, DaysToKeep, GroomingRunTime,DataGroomedMaxTime | ConvertTo-HTML -Fragment

  # SQL Query to find out what Grooming Jobs have run in the last 24 hours
  $DidGroomingRun = @"
-- Did Grooming Run?:
SELECT [InternalJobHistoryId]
      ,[TimeStarted]
      ,[TimeFinished]
      ,[StatusCode]
      ,[Command]
      ,[Comment]
FROM [dbo].[InternalJobHistory]
WHERE [TimeStarted] >= DATEADD(day, -2, GETDATE())
Order by [TimeStarted]
"@


  # Produce a table of Grooming History and add it to the Report
  $ReportOutput += "<h3>Grooming History From The Past 48 Hours</h3>"
  $ReportOutput += Run-OpDBSQLQuery $DidGroomingRun InternalJobHistoryId, TimeStarted, TimeFinished, StatusCode, Command, Comment | Select-Object | ConvertTo-HTML -fragment

  #Produce Table of DW DB Health
  $ReportOutput +="<h2>Data Warehouse Database Health</h2>"
  $ReportOutput +="<h3>Data Warehouse DB Backup Status</h3>"
  $ReportOutput +=$DWDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-Html -Fragment
  $ReportOutput +="<h3>Data Warehouse Top 25 Largest Tables</h3>"
  $ReportOutput +=$DWDBLARGETAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -fragment
  $ReportOutput +="<h3>Data Warehouse Staging Tables</h3>"
  $ReportOutput +=$DWDBSGTB | Select-Object "Alert Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBEV | Select-Object "Event Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBPE | Select-Object "Perf Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBST | Select-Object "State Staging Table", Table| ConvertTo-Html -Fragment
  $ReportOutput +="<h3>Data Warhouse Grooming Retention</h3>"
  $ReportOutput +=$DWDBGRET | Select-Object "Dataset Name", "Agg Type 0=raw, 20=Hourly, 30=Daily","Retention Time in Days"| ConvertTo-Html -Fragment

  # Insert the Results for the Number of Management Servers into the Report
  $ReportOutput += "<p>Number of Management Servers : $($ManagementServers.count)</p>"

  # Retrieve the data for the Management Servers
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Management Servers</h2>"

  $AllManagementServers = @()

  ForEach ($ManagementServer in $ManagementServers)
  {
    # Find out the Server Uptime for each of the Management Servers
    #Original query referenced -computer $ManagementServer.Name this was an error I modified to .Displayname to fix
    #TPaul: updated this to use CIM instead of WMI
    $lastboottime = (Get-CimInstance -ClassName win32_operatingsystem ).LastBootUpTime
    $sysuptime =New-TimeSpan $lastboottime (Get-Date)  
    $totaluptime = "" + $sysuptime.days + " days " + $sysuptime.hours + " hours " + $sysuptime.minutes + " minutes " + $sysuptime.seconds + " seconds"

    # Find out the Number of WorkFlows Running on each of the Management Servers
    $perfWorkflows = Get-Counter -ComputerName $ManagementServer.DisplayName -Counter "\Health Service\Workflow Count" -SampleInterval 1 -MaxSamples 1

    # The Performance Counter seems to return a lot of other characters and spaces...I only want the number of workflows, let's dump the rest
    [int]$totalWorkflows = $perfWorkflows.readings.Split(':')[-1] | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()}

    $ManagementServerProperty = New-Object psobject
    $ManagementServerProperty | Add-Member noteproperty "Management Server" ($ManagementServer.DisplayName)
    $ManagementServerProperty | Add-Member noteproperty "Health State" ($ManagementServer.HealthState)
    $ManagementServerProperty | Add-Member noteproperty "Version" ($ManagementServer.Version)
    $ManagementServerProperty | Add-Member noteproperty "Action Account" ($ManagementServer.ActionAccountIdentity)
    $ManagementServerProperty | Add-Member noteproperty "System Uptime" ($totaluptime)
    $ManagementServerProperty | Add-Member noteproperty "Workflows" ($totalWorkflows)
    $AllManagementServers += $ManagementServerProperty
  }

  # Insert the Results for the Management Servers into the Report
  $ReportOutput += $AllManagementServers | Select-Object "Management Server", "Health State", "Version", "Action Account", "System Uptime", "Workflows" | Sort-Object "Management Server" | ConvertTo-HTML -fragment

  # Insert the Results for the Stats and State Changes into the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Daily KPI</h2>"
  $ReportOutput += $AllStats | Select-Object "Open Alerts", "Groups", "Monitors", "Rules", "State Changes Yesterday" | ConvertTo-HTML -fragment

  # Retrieve and Insert the Results for the Management Packs that have been modified into the Report
  Write-Host "Checking for Management Packs that have been modified in the last 24 hours"

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Management Packs Modified in the Last 24 Hours</h2>"
  If (!($ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)}))
  {
    $ReportOutput += "<p>No Management Packs have been updated in the last 24 hours</p>"
  }
  Else
  {
    $ReportOutput += $ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)} | Select-Object Name, LastModified | ConvertTo-HTML -fragment
  }


  # Retrieve and Insert the Results for the Default Management Pack into the Report
  # This 'should be empty'...don't store stuff here!
  Write-Host "Checking for the Default Management Pack for Overrides"
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>The Default Management Pack</h2>"

  # Excluding these 2 ID's as they are part of the default MP for DefaultUser and Language Code Overrides
  $excludedID = "5a67584f-6f63-99fc-0d7a-55587d47619d", "e358a914-c851-efaf-dda9-6ca5ef1b3eb7"
  $defaultMP = $ManagementPacks | Where-Object {$_.Name -match "Microsoft.SystemCenter.OperationsManager.DefaultUser"}
  ##Changed below line for compat with PowerShell 2.0
  ##If (!($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID}))
  If (!($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID}))
  {
    $ReportOutput += "<p>There are no Overrides being Stored in the Default Management Pack</p>"
  }
  Else
  {

    ##Changed below line for compat with PowerShell 2.0
    #$foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID -AND $_.ContextInstance -ne $guid} | select -expandproperty ContextInstance -ea SilentlyContinue)
    $foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID -AND $_.ContextInstance -ne $guid} | Select-Object -expandproperty ContextInstance -ea SilentlyContinue)


    $ReportOutput += "<p>Found overrides against the following targets: $foundOverride.Displayname</p>"
    ##PowerShell 2.0 Compat
    ##$ReportOutput += $($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID} | Select Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment
    $ReportOutput += $($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID} | Select-Object -Property Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment

  }



  # Show all Agents that are in an Uninitialized State
  Write-Host "Checking for Uninitialized Agents"

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Uninitialized Agents</h2>"
  If (!($Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name))
  {
    $ReportOutput += "<p>No Agents are in the Uninitialized State</p>"
  }
  Else
  {
    $ReportOutput += $Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name | ConvertTo-HTML -fragment
  }


  # Show a Summary of all Agents States
  $healthy = $uninitialized = $warning = $critical = 0

  Write-Host "Checking Agent States"

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Agent Stats</h3>"

  switch ($Agents | Select-Object HealthState ) {
    {$_.HealthState -like "Success"} {$healthy++}
    {$_.HealthState -like "Uninitialized"} {$uninitialized++}
    {$_.HealthState -like "Warning"}  {$warning++}
    {$_.HealthState -like "Error"} {$critical++}
  }
  $totalagents = ($healthy + $warning + $critical + $uninitialized)

  $AllAgents = @()

  $iAgent = New-Object psobject
  $iAgent | Add-Member noteproperty -Name "Agents Healthy" -Value $healthy
  $iAgent | Add-Member noteproperty -Name "Agents Warning" -Value $warning
  $iAgent | Add-Member noteproperty -Name "Agents Critical" -Value $critical
  $iAgent | Add-Member noteproperty -Name "Agents Uninitialized" -Value $uninitialized
  $iAgent | Add-Member noteproperty -Name "Total Agents" -Value $totalagents

  $AllAgents += $iAgent

  $ReportOutput += $AllAgents | Select-Object -Property "Agents Healthy", "Agents Warning", "Agents Critical", "Agents Uninitialized", "Total Agents" | ConvertTo-HTML -fragment

  # Agent Pending Management States
  Write-Host "Checking Agent Pending Management States"

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Agent Pending Management Summary</h3>"

  $pushInstall = $PushInstallFailed = $ManualApproval = $RepairAgent = $RepairFailed = $UpdateFailed = 0

  $agentpending = Get-SCOMPendingManagement
  switch ($agentpending | Select-Object AgentPendingActionType ) {
    {$_.AgentPendingActionType -like "PushInstall"} {$pushInstall++}
    {$_.AgentPendingActionType -like "PushInstallFailed"} {$PushInstallFailed++}
    {$_.AgentPendingActionType -like "ManualApproval"}  {$ManualApproval++}
    {$_.AgentPendingActionType -like "RepairAgent"} {$RepairAgent++}
    {$_.AgentPendingActionType -like "RepairFailed"} {$RepairFailed++}
    {$_.AgentPendingActionType -like "UpdateFailed"} {$UpdateFailed++}

  }

  $AgentsPending = @()

  $AgentSummary = New-Object psobject
  $AgentSummary | Add-Member noteproperty "Push Install" ($pushInstall)
  $AgentSummary | Add-Member noteproperty "Push Install Failed" ($PushInstallFailed)
  $AgentSummary | Add-Member noteproperty "Manual Approval" ($ManualApproval)
  $AgentSummary | Add-Member noteproperty "Repair Agent" ($RepairAgent)
  $AgentSummary | Add-Member noteproperty "Repair Failed" ($RepairFailed)
  $AgentSummary | Add-Member noteproperty "Update Failed" ($UpdateFailed)

  $AgentsPending += $AgentSummary

  # Produce a table of all Agent Pending Management States and add it to the Report
  $ReportOutput += $AgentsPending | Select-Object "Push Install", "Push Install Failed", "Manual Approval", "Repair Agent", "Repair Failed", "Update Failed" | ConvertTo-HTML -fragment

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Alerts</h2>"

  $AlertsAll = ($Alerts | Where-Object {$_.ResolutionState -ne 255}).Count
  $AlertsWarning = ($Alerts | Where-Object {$_.Severity -eq "Warning" -AND $_.ResolutionState -ne 255}).Count
  $AlertsError = ($Alerts | Where-Object {$_.Severity -eq "Error" -AND $_.ResolutionState -ne 255}).Count
  $AlertsInformation = ($Alerts | Where-Object {$_.Severity -eq "Information" -AND $_.ResolutionState -ne 255}).Count
  $Alerts24hours = ($Alerts | Where-Object {$_.TimeRaised -ge (Get-Date).addhours(-24) -AND $_.ResolutionState -ne 255}).count

  $AllAlerts = @()


  $AlertSeverity = New-Object psobject
  $AlertSeverity | Add-Member noteproperty "Warning" ($AlertsWarning)
  $AlertSeverity | Add-Member noteproperty "Error" ($AlertsError)
  $AlertSeverity | Add-Member noteproperty "Information" ($AlertsInformation)
  $AlertSeverity | Add-Member noteproperty "Last 24 Hours" ($Alerts24hours)
  $AlertSeverity | Add-Member noteproperty "Total Open Alerts" ($AlertsAll)
  $AllAlerts += $AlertSeverity


  # Produce a table of all alert counts for warning, error, information, Last 24 hours and Total Alerts and add it to the Report
  $ReportOutput += $AllAlerts | Select-Object "Warning", "Error", "Information", "Last 24 Hours", "Total Open Alerts" | ConvertTo-HTML -fragment

  <#
      # Check if the Action Account is a Local Administrator on Each Management Server
      # This will only work if the account is a member of the Local Administrators Group directly.
      # If it has access by Group Membership, you can look for that Group instead
      # $ActionAccount = "YourGrouptoCheck"
      # Then replace all 5 occurrences below of $ManagementServer.ActionAccountIdentity with $ActionAccount
 
      Write-Host "Checking if the Action Account is a member of the Local Administrators Group for each Management Server"
 
      $ReportOutput += "<br>"
      $ReportOutput += "<h2>SCOM Action Account</h2>"
 
      ForEach ($ms in $ManagementServers.DisplayName | sort DisplayName)
      {
      $admins = @()
      $group =[ADSI]"WinNT://$ms/Administrators"
      $members = @($group.psbase.Invoke("Members"))
      $members | foreach {
      $obj = new-object psobject -Property @{
      Server = $Server
      Admin = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
      }
      $admins += $obj
      }
 
      If ($admins.admin -match $ManagementServer.ActionAccountIdentity)
      {
      # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms"
      $ReportOutput += "<p>The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms</p>"
      }
      Else
      {
      # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms"
      $ReportOutput += "<p><span style=`"color: `#CC0000;`">The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms</span></p>"
      }
      }
  #>




  # Objects in Maintenance Mode

  #SQL Query Servers in MMode
  $ServersInMM =@"
select DisplayName from dbo.MaintenanceMode mm
join dbo.BaseManagedEntity bm on mm.BaseManagedEntityId = bm.BaseManagedEntityId
where Path is NULL and IsInMaintenanceMode = 1
"@


  $OpsDBSIMM = Run-OpDBSQLQuery $ServersInMM

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Servers in Maintenance Mode</h2>"

  If (!($OpsDBSIMM))
  {
    $ReportOutput += "<p>There are no objects in Maintenance Mode</p>"
  }
  Else
  {
    $ReportOutput += $OpsDBSIMM | Select-Object DisplayName | ConvertTo-HTML -fragment
  }

  <#
      # Global Grooming Settings
      # Simple comparisons against the values set at the beginning of this script
      # I use this to see if anyone has changed the settings. So set the values at the top of this script to match the values that your environment 'should' be set to.
 
      $ReportOutput += "<br>"
      $ReportOutput += "<h2>SCOM Global Settings</h2>"
 
 
      $SCOMDatabaseGroomingSettings = Get-SCOMDatabaseGroomingSetting
 
 
      If ($SCOMDatabaseGroomingSettings.AlertDaysToKeep -ne $AlertDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Alert Days to Keep has been changed! Reset back to $AlertDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Alert Days is correctly set to $AlertDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.AvailabilityHistoryDaysToKeep -ne $AvailabilityHistoryDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Availability History Days has been changed! Reset back to $AvailabilityHistoryDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Availability History Days is correctly set to $AvailabilityHistoryDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.EventDaysToKeep -ne $EventDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Event Days has been changed! Reset back to $EventDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Event Days is correctly set to $EventDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.JobStatusDaysToKeep -ne $JobStatusDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Job Days (Task History) has been changed! Reset back to $JobStatusDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Job Days (Task History) is correctly set to $JobStatusDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.MaintenanceModeHistoryDaysToKeep -ne $MaintenanceModeHistoryDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Maintenance Mode History has been changed! Reset back to $MaintenanceModeHistoryDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Maintenance Mode History is correctly set to $MaintenanceModeHistoryDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.MonitoringJobDaysToKeep -ne $MonitoringJobDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Monitoring Job Data has been changed! Reset back to $MonitoringJobDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Monitoring Job Data is correctly set to $MonitoringJobDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.PerformanceDataDaysToKeep -ne $PerformanceDataDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Performance Data has been changed! Reset back to $PerformanceDataDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Performance Data is correctly set to $PerformanceDataDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.StateChangeEventDaysToKeep -ne $StateChangeEventDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">State Change Data has been changed! Reset back to $StateChangeEventDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>State Change Data is correctly set to $StateChangeEventDaysToKeep</p>"}
 
 
      # SCOM Agent Heartbeat Settings
      $HeartBeatSetting = Get-SCOMHeartbeatSetting
 
      If ($HeartBeatSetting.AgentHeartbeatInterval -ne 180 -AND $HeartBeatSetting.MissingHeartbeatThreshold -ne 3)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">The HeartBeat Settings have been changed! Reset back to $AgentHeartbeatInterval and $MissingHeartbeatThreshold</span></p>"}
      Else {$ReportOutput += "<p>The HeartBeat Settings are correctly set to Interval: $AgentHeartbeatInterval and Missing Threshold: $MissingHeartbeatThreshold</p>"}
  #>

  # How long did this script take to run?
  $EndTime=Get-Date
  $TotalRunTime=$EndTime-$StartTime

  # Add the time to the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<p>Total Script Run Time: $($TotalRunTime.hours) hrs $($TotalRunTime.minutes) min $($TotalRunTime.seconds) sec</p>"

  # Close the Body of the Report
  $ReportOutput += "</body>"

  Write-Host "Saving HTML Report to $ReportPath"

  # Save the Final Report to a File
  ConvertTo-HTML -head $Head -body "$ReportOutput" | Out-File $ReportPath

  Invoke-Item "$ReportPath"

  <#
      # Send Final Report by email...
 
      Write-Host "Emailing Report"
      $SMTPServer ="your.smtpserver.com"
      $Body = ConvertTo-HTML -head $Head -body "$ReportOutput"
      $SmtpClient = New-Object Net.Mail.SmtpClient($smtpServer);
      $mailmessage = New-Object system.net.mail.mailmessage
      $mailmessage.from = "sender@company.com"
      $mailmessage.To.add("recipient@company.com")
      # Want more recipient's? Just add a new line
      $mailmessage.To.add("anotherrecipient@company.com")
      $mailmessage.Subject = "Tims SCOM Healthcheck Report"
      $MailMessage.IsBodyHtml = $true
      $mailmessage.Body = $Body
      $smtpclient.Send($mailmessage)
  #>


  <#
      # Insert the Results for the Reporting Server URL into the Report
      $ReportOutput += "<h2>Reporting Server</h2>"
      $ReportOutput += "<p>Reporting Server URL : <a href=`"$ReportingURL/`">$ReportingURL</a></p>"
      $ReportOutput += "<p>The Reporting Server URL Status : $ReportingServerStatus</p>"
 
      # Insert the Results for the Web Console URL into the Report
      $ReportOutput += "<h2>Web Console Servers</h2>"
      $ReportOutput += "<p>Web Console URL : <a href=`"$WebConsoleURL/`">$WebConsoleURL</a></p>"
      $ReportOutput += "<p>The Web Console URL Status : $WebConsoleStatus</p>"
  #>


  # A bit of cleanup
  #Clear-Variable Agents, Alerts, Groups, ManagementGroup, ManagementPacks, ManagementServer, Monitors, Rules, ReportOutput, StartTime, EndTime, TotalRunTime, SCOMDatabaseGroomingSettings

}
#######################################################################

<#
    .Synopsis
    List version and file path information for all management pack files (*.mp, *.mpb, *.xml).
    .DESCRIPTION
    Will display MP file and version information.
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' -Verbose | Select Name,DisplayName,Version,FullName | Format-Table
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' | Select-Object Name,DisplayName,Version,FullName,KeyToken, @{Name="References"; Expression={($_.References.GetEnumerator() | Out-String) } } | Out-GridView -PassThru
 
    The above example will display all of the MP properties in Gridview with the References column displayed in a more friendly format.
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' | Out-GridView -PassThru | % {& explorer.exe (Split-Path -Parent $_.FullName) }
 
    The above example will present a list of your management packs in Gridview. Once you select one or more items-OK, the folder(s) containing the management pack(s) will be opened with File Explorer.
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\MySealedMPs' | Format-Table | ConvertTo-Html | Set-Content C:\mpfiles.html; C:\mpfiles.html
 
    The above example will create an HTML report of your MP files then open the HTML file with the default application.
 
    .INPUTS
    System.String, Switch
    .OUTPUTS
    Custom object
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    History:
    2020.02.27 - Improved efficiency. Added verbose output.
    2020.02.21 - Added more examples. Removed Gridview and Passthru switches.
    2019.07.29 - Added a few more MP properties to the returned object(s)
    2018.05.30 - Added GridView option as well as some other cleanup.
    .FUNCTIONALITY
    Useful for identifying specific versions of management packs in your local archive.
    .LINK
    Get-SCOMClassInfo
    New-SCOMClassGraph
    Unseal-SCOMMP
#>

Function Get-SCOMMPFileInfo {

  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
  PositionalBinding=$false)]
      
  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("Path")]
    [string]$inDir

    # deprecated
    # [switch]$GridView=$true,
    
    # deprecated
    #[switch]$passthru=$true
  )
 
  [System.Collections.ArrayList]$arrMaster = @()

  $MPFiles = Get-ChildItem -Path $inDir -Recurse -Include *.mp,*.mpb,*.xml
  ForEach ($File in $MPFiles.FullName) {
    Write-Verbose $File
    
    Switch ($File) {
      {$_ -Match '\.mp$|\.xml$'} {
        $MP = Get-SCManagementPack -ManagementPackFile $File
        break;
      }

      # Get MPB (bundles)
      {$_ -Match '\.mpb$'} {
        #Sometimes bundles cannot be retrieved
        Try {
          $MP = Get-SCManagementPack -BundleFile $File
        }Catch {
          Write-Host "Unable to retrieve file: [ $($File) ]. FullName.Length:[$($File.Length)]" -F Red
          break;
        }
      }
    }#end Switch
    $obj = New-Object PSCustomObject
    $obj | Add-Member -Type NoteProperty -Name Name -Value $MP.Name
    $obj | Add-Member -Type NoteProperty -Name DisplayName -Value $MP.DisplayName
    $obj | Add-Member -Type NoteProperty -Name Version -Value $MP.Version
    $obj | Add-Member -Type NoteProperty -Name FullName -Value $File
    $obj | Add-Member -Type NoteProperty -Name Description -Value $MP.Description
    $obj | Add-Member -Type NoteProperty -Name ID -Value $MP.ID.GUID
    $obj | Add-Member -Type NoteProperty -Name KeyToken -Value $MP.KeyToken
    $obj | Add-Member -Type NoteProperty -Name DefaultLanguageCode -Value $MP.DefaultLanguageCode
    $obj | Add-Member -Type NoteProperty -Name References -Value $MP.References

    Write-Verbose "$File"
    $Null = $arrMaster.Add($obj)
  }#end ForEach
  
  Return $arrMaster
}#End Function
#######################################################################

<#
    .Synopsis
    Will return the friendly name of a RunAs account.
    .DESCRIPTION
    Often times RunAs account SSIDs will appear in Operations Manager event logs which makes it difficult to determine which account is involved. This will correlate the friendly name with the SSID provided.
    .EXAMPLE
    PS C:\> Get-SCOMRunAsAccountName -SSID '0000F61CD9E515695ED4A018518C053E3CD87251D500000000000000000000000000000000000000'
    .Parameter -SSID
    The SSID as it is presented in the Operations Manager event log; an 80 character string of Hex digits [0-9A-F]
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Original Date: 2013.09.23
    History: I think I originally got most of this from a Technet blog: https://social.technet.microsoft.com/Forums/systemcenter/en-US/0b9bd679-a712-435e-9a27-8b3041cddac8/how-to-find-the-runasaccount-from-the-ssid?forum=operationsmanagergeneral
#>

Function Get-SCOMRunAsAccountName{
  param (
    [Parameter(
        Mandatory=$true,
    HelpMessage="Please enter the SSID")]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$SSID
  )

  Get-SCOMRunAsAccount | Sort-Object Name | ForEach-Object {
    $string = $null;$_.SecureStorageId | ForEach-Object {
      $string = $string + "{0:X2}" -f $_
    }

    $RunAsAccountName = $_.Name
    [string]$RunAsAccountSSID = $string
    If ($SSID -match $RunAsAccountSSID) {
      Write-Host "The Run As Account Name is: $RunAsAccountName"
    }
  }
}#end Function
#######################################################################


<#
    .DESCRIPTION
    Will output all RunAs accounts and any associated Security Profiles.
 
    .EXAMPLE
    Get-SCOMRunAsProfilesAccounts
 
    The above example will connect to localhost and output RunAs account and Seucrity Profile information.
 
    .EXAMPLE
    Get-SCOMRunAsProfilesAccounts -ManagementServer 'ms01.contoso.com'
 
    The above example will connect to the designated server and output RunAs account and Seucrity Profile information.
 
    .EXAMPLE
    Get-SCOMRunAsProfilesAccounts | Out-Gridview -Passthru
 
    The above example will display results in Gridview. Any selected rows can be passed through to the PowerShell Console for further review or manipulation.
 
    .INPUTS
    System.String
    .OUTPUTS
    Custom object
    .NOTES
    Author: Tyson Paul, derived from the works of Kevin Holman, Dirk Brinkman, Mihai (Sarbulescu?)
    Blog: https://monitoringguys.com/
    History:
    2020.02.25 - Revision of previous version (v1.0, Kevin Holman); improved efficiency, modified formatting.
    Ref: Kevin Holman: https://kevinholman.com/2017/07/27/document-your-scom-runas-account-and-profiles-script/
     
    .FUNCTIONALITY
    This function works best when piped to Out-Gridview or exported to CSV.
    .LINK
    Get-SCOMRunAsAccountName
#>

Function Get-SCOMRunAsProfilesAccounts {

  Param(
    [string]$ManagementServer = 'LOCALHOST'
  )


  ############# FUNCTIONS ###################
  Function Get-ClassfromInstance{
    [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
        SupportsShouldProcess=$true, 
        PositionalBinding=$false,
    ConfirmImpact='Medium')]
    [OutputType([tinyClass])]

    Param (
      $Instance
    )
    $class = (Get-SCOMClass -Id $instance.LeastDerivedNonAbstractMonitoringClassId)
    $tmpClass = [tinyClass]::new()
    $tmpClass.Id = $class.Id
    $tmpClass.Name = $class.Name
    If ($class.DisplayName.Length -ne 0) {
      $tmpClass.DisplayName = $class.DisplayName
    }
  
    Return $tmpClass
  }
  ########### END FUNCTIONS #################


  ########### BEGIN MAIN #################

  # My custom object
  Class Account {
    [string]$RunAsAccountName = ''
    [string]$Domain = ''
    [string]$Username = ''
    [string]$AccountType = ''
    [string]$ProfileName = ''
    [string]$ProfileDisplayName = ''
    [string]$TargetClassID = ''
    [string]$TargetClassName = ''
    [string]$TargetClassDisplayName = ''
    [string]$TargetID = ''
    [string]$TargetName = ''
    [string]$TargetDisplayName = ''
  }

  Class tinyClass {
    [string]$Id = ''
    [string]$Name = ''
    [string]$DisplayName = ''
  }

  # Main array for account collection
  [System.Collections.ArrayList]$AccountDataArray = @()

  # Load Modules and Connect to SCOM
  . Import-SCOMPowerShellModule

  # Remove any existing/default mgmt group connection
  Get-SCOMManagementGroupConnection | Remove-SCOMManagementGroupConnection

  # Connect to designated mgmt server (explicit)
  $MGConnection = New-SCManagementGroupConnection -ComputerName $ManagementServer
  
  $MG = Connect-OMManagementGroup -SDK $ManagementServer
  
  # Load Assembly and define ManagementGroup Object
  $CoreDLL = 'Microsoft.EnterpriseManagement.Core' 
  $null = [reflection.assembly]::LoadWithPartialName($CoreDLL) 
  $EMG = New-Object -TypeName Microsoft.EnterpriseManagement.EnterpriseManagementGroup -ArgumentList ($ManagementServer)

  #Process HealthService based Action Accounts Section
  #=======================================================
  ForEach ($RunAsProfile in (Get-SCOMRunAsProfile))
  {        
    # Get Health Service array associated with the profile
    $HSRef = $MG.GetMonitoringSecureDataHealthServiceReferenceBySecureReferenceId($RunAsProfile.ID)
    ForEach ($HS in $HSRef)
    {
      $TargetName = (Get-SCOMClassInstance -Id $HS.HealthServiceId).Displayname
      $MonitoringData = $HS.GetMonitoringSecureData()
      $tempAccount = [Account]::new()
      $tempAccount.RunAsAccountName = $MonitoringData.Name
      $tempAccount.Domain = $MonitoringData.Domain
      $tempAccount.Username = $MonitoringData.UserName
      $tempAccount.AccountType = $MonitoringData.SecureDataType
      $tempAccount.ProfileName = $RunAsProfile.Name
      $tempAccount.TargetID = $HS.HealthServiceId.Guid.ToString()
      $tempAccount.TargetName = $TargetName
      $tempAccount.TargetDisplayName = ''
      $tmpClass = Get-ClassfromInstance -Instance (Get-SCOMClassInstance -Id $HS.HealthServiceId)
      $tempAccount.TargetClassId = $tmpClass.Id
      $tempAccount.TargetClassName = $tmpClass.Name
      $tempAccount.TargetClassDisplayName = $tmpClass.DisplayName

      If ($null -ne $RunAsProfile.DisplayName ) 
      {
        $tempAccount.ProfileDisplayName = $RunAsProfile.DisplayName
      }    

      #$AccountDataArray += $tempAccount
      $null = $AccountDataArray.Add($tempAccount)
    }
  }
  #=======================================================
  # End ForEach RunAsProfile

  # Process all RunAsAccounts targeted at other targets
  #=======================================================
  #Get all RunAsAccounts
  $colAccounts = $EMG.Security.GetSecureData()

  #Loop through each RunAs account
  ForEach ($account in $colAccounts)
  {
    $secStorId = $account.SecureStorageId
    $stringBuilder = New-Object -TypeName System.Text.StringBuilder
    $secStorId | ForEach-Object {$null = $stringBuilder.Append($_.ToString('X2'))}
    $MPCriteria = "Value='{0}'" -f $stringBuilder.ToString()
    $moc = New-Object -TypeName Microsoft.EnterpriseManagement.Configuration.ManagementPackOverrideCriteria -ArgumentList ($MPCriteria)
    $overrides = $EMG.Overrides.GetOverrides($moc)

    If ($overrides.Count -eq 0)
    {
      $tempAccount = [Account]::new()
      $tempAccount.RunAsAccountName = $account.Name
      $tempAccount.Domain = $account.Domain
      $tempAccount.Username = $account.UserName
      $tempAccount.AccountType = $account.SecureDataType
      $tempAccount.ProfileName = 'No Profile Assigned'
    
      $null = $AccountDataArray.Add($tempAccount)
    }
    # Customizations/overrides exist for the account, process them all.
    Else
    {
      ForEach ($override in $overrides) 
      {
        # Every account will typically have these values
        $tempAccount = [Account]::new()
        $tempAccount.RunAsAccountName = $account.Name
        $tempAccount.Domain = $account.Domain
        $tempAccount.Username = $account.UserName
        $tempAccount.AccountType = $account.SecureDataType
        $tempAccount.TargetClassId = $override.Context.Id.Guid.ToString()
        $tmpClass = Get-SCClass -Id $tempAccount.TargetClassId
        $tempAccount.TargetClassName = $tmpClass.Name

        If ($null -ne $tmpClass.DisplayName) 
        {
          $tempAccount.TargetClassDisplayName = $tmpClass.DisplayName
        } 

      
        # If a ContextInstance (specific object) exists, add instance-specific data.
        If ($null -ne $override.ContextInstance)
        {
          $tempAccount.TargetID = $override.ContextInstance.Guid.ToString()
          $TargetClassInstance = Get-SCOMClassInstance -Id $tempAccount.TargetID
          $tempAccount.TargetName = $TargetClassInstance.Name
        
          If ($null -ne $TargetClassInstance.DisplayName) 
          {
            $tempAccount.TargetDisplayName = $TargetClassInstance.DisplayName
          }
        }
      
        $secRef = $EMG.Security.GetSecureReference($override.SecureReference.Id)
        $tempAccount.ProfileName = $secRef.Name
        If ($null -ne $secRef.DisplayName) 
        {
          $tempAccount.ProfileDisplayName = $secRef.DisplayName
        }
        
        # Add item to array
        $null = $AccountDataArray.Add($tempAccount)
      }
    } #End ForEach Override
  }
  #=======================================================
  # End ForEach account


  # Sort by RunAsAccountName. This is a nice touch for the user.
  Return ($AccountDataArray | Sort-Object -Property RunAsAccountName)

} #End Function
#######################################################################

<#
    .Synopsis
    Will return object that represents detailed task results for agent task: 'Microsoft.SystemCenter.GetAllRunningWorkflows'
    .DESCRIPTION
    This will gather a generous amount of very rich data about the workflows currently/actually running on one or more agents. It will output a complex object that contains all workflow objects and all related instance objects. This is very useful when you may not have the same MPs and instances in your own lab when analyzing data from a separate management group object. The Help content for the function demonstrates how to display a report with various statistics about classes instances and workflows.
 
    .EXAMPLE
    #
    #Example
    PS C:\> Get-SCOMRunningWorkflows -AgentName 'ad01.contoso.com','db01.contoso.com' -TimeoutSeconds 800
 
    .EXAMPLE
    #
    #Example
    PS C:\> Get-SCOMRunningWorkflows -AgentName 'ad01.contoso.com' -OutputType TasktResult
 
    The above example will return only the raw task result object.
 
    .Example
    #
    #Example
    PS C:\> Get-SCOMRunningWorkflows -AgentName 'ad01.contoso.com' | Export-Clixml -Path 'C:\Test\ad01.contoso.com.XML'
    $WFs = Import-Clixml -Path 'C:\Test\ad01.contoso.com.XML'
 
    # Total Instances
    $TOI = $WFs.instance.count
    Write-Host "Total object instances: $TOI"
 
    # Worflows types:
    $WFAT = $WFs.workflows.workflow | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty count
    Write-Host "Worflows types: $WFAT"
 
    # Total WF instances running, all types:
    $TWFAT = $WFs.wfcount | Measure-Object -Sum | Select-Object sum -ExpandProperty sum
    Write-Host "Total WF instances running, all types: $TWFAT"
 
    # Workflows of category type
    $Category = 'EventCollection'
    $WFoCT = $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')}
    $WFoCT_sum = $WFoCT | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty Count
    Write-Host "Workflows of category type [$($Category), Not Alert]: $WFoCT_sum"
 
    # Total WF instances of category type
    $TWFIoCT = $WFoCT | Group-Object Name | Measure-Object -sum count | Select-Object sum -ExpandProperty sum
    Write-Host "Total WF instances of category type [$($Category)]: $TWFIoCT"
 
    #Lists
    ###############################################
 
    # Classes
    $classes = Get-SCOMClass -ID (($wfs.instance).LeastDerivedNonAbstractManagementPackClassId | Group-Object).name
    Write-Host "$($Classes.count) class target types"
 
    # Class Names
    $Classes = ($wfs.instance).LeastDerivedNonAbstractManagementPackClassId`
    | Group-Object `
    | Select-Object count `
    , @{Name="Class"; Expression = {(Get-SCOMClass -id $_.Name).Name}} `
    , @{Name="Id";Expression = {$_.Name}}, @{Name="Identifier"; Expression = {(Get-SCOMClass -id $_.Name).Identifier}} `
    | Sort-Object count `
    | Format-Table
 
    $Classes
    $allWorkflows = @{}
    # List of workflows, instance count, category
    $WFs.workflows.workflow | % {$allWorkflows[($_.Name)] = $_}
    $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')} | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$Category}} | Sort-Object count | Format-Table
 
    # All workflow types with counts
    $WFs.workflows.workflow | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$allWorkflows[($_.Name)].Category}} | Sort-Object count | Format-Table
 
 
 
    .INPUTS
    [System.Object[]] FQDN of one or more agents.
    .OUTPUTS
    Custom object with task results data
    .NOTES
    Author: Tyson Paul (https://monitoringguys.com/)
    Version History:
    2020.10.13.1746 - Added detailed example for export/import XML.
    2020.09.22.1444 - Added detailed object formatting function
    2020.09.14.1811 - Initial version
#>


Function Get-SCOMRunningWorkflows {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$false, 
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]
  Param(
    # FQDN of one or more agents
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [System.Object[]]$AgentName,
    

    # Param1 help description
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [ValidateSet('TasktResult', 'Detailed')]
    $OutputType = 'Detailed',


    # Seconds before function will abandon waiting for task to complete
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [int]$TimeoutSeconds=600
  )
  
  
  ############################ FUNCTIONS ################################
  
  <#
      .NOTES
      Name: Get-SCOMRunningWorkflowsReport
      Author: Tyson Paul
      Blog: https://monitoringguys.com/
      Version History:
      2020.09.22.1433 - v1
 
      .DESCRIPTION
      Will output an array of objects containing helpful information on instances and the workflows running for those instances.
 
      .EXAMPLE
      #
      #Example
      PS C:\> $TaskResult = Get-SCOMRunningWorkflows -AgentName 'ad01.contoso.com','db01.contoso.com' -OutputType 'TasktResult'
      PS C:\> $TaskResult | Get-SCOMRunningWorkflowsReport
 
      .EXAMPLE
      #
      #Example
      PS C:\> Get-SCOMRunningWorkflowsReport -TaskResult (Get-SCOMRunningWorkflows -AgentName 'ad01.contoso.com' -OutputType TasktResult)
 
      .EXAMPLE
      #
      #Example
      PS C:\> Get-SCOMRunningWorkflowsReport -TaskResult $TaskResult
 
      .INPUTS
      Task result from 'Microsoft.SystemCenter.GetAllRunningWorkflows' agent task.
 
      .OUTPUTS
      [System.Object[]]
  #>

  Function Get-SCOMRunningWorkflowsReport {
    Param (
      [Parameter(Mandatory=$true, 
          ValueFromPipeline=$true,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false, 
      ParameterSetName='Parameter Set 1')]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]  
      [System.Object[]]$TaskResult
    )
  
    BEGIN {
  
      ############################ FUNCTIONS ################################
      Function Build-Obj {
        Param (
          $WF,
          [string]$Type ='NotDefined'
        )
        $tmp = New-Object PSCustomObject
        $tmp | Add-Member -MemberType NoteProperty -Name 'Type' -Value $type
        $tmp | Add-Member -MemberType NoteProperty -Name 'Workflow' -Value $wf
        Return $tmp
      }
      ######################## END FUNCTIONS ################################
    
      Remove-Variable -Name 'Instances','rpt1' -ErrorAction Ignore
      [System.Collections.ArrayList]$arrReports = @()

      # Load all workflow types into one hash. dot-source this function to speed up multiple runs.
      If (-NOT $allWFTypes) {
        $allWFTypes=@{}
        $allClasses=@{}
        Write-Warning "Loading all workflows into cache object. This may require a few minutes. dot-source this function next time to speed up consecutive uses."
        ForEach ($item in @(Get-SCOMMonitor)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Monitor) }
        ForEach ($item in @(Get-SCOMRule)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Rule) }
        ForEach ($item in @(Get-SCOMDiscovery)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Discovery) }
        
        # Build hash of all class types
        ForEach ($Class in (Get-SCOMClass) ){ $allClasses[$Class.Id.Guid] = $Class}        
      }
      Else {
        Write-Warning "'allWFTypes' object [$($allWFTypes.Count)] already cached. 'allClasses' object already cached [$($allClasses.Count)]. Will not rebuild; use existing. This will save time."
      }
    }#end BEGIN
  
    #----------------------------------------------------------------
  
    PROCESS {
  
      ForEach ($Result in $TaskResult) {
        # Get the instance objects involved
        $Instances = (($Result.Output | ForEach-Object {[xml]$_}).DataItem)
        $hashInstances = @{}
        $Instances.Details.Instance | ForEach-Object {$hashInstances[($_.ID -Replace '{|}' ,'' )] = $_ }
  
        # Get basic workflow data: Instance ID (of target object), Workflow count, Workflow list/array
        $InstanceDetails = ([xml]($Result.Output)).DataItem.Details.Instance | Select-Object @{Name='Id';Expression={(($_.ID -Replace '{|}','').ToString())}}, @{Name='WFCount';Expression={$_.Workflow.Count} }, @{Name='Workflow';Expression={$_.Workflow | Sort-Object} } | Sort-Object WFCount,Workflow -Descending 

        $InstanceInfo = [ordered]@{}
        # Build an object with lots of data about each Instance, and the workflows running on its behalf
        ForEach ($Obj in $InstanceDetails) {
          $thisInstance = (Get-SCOMClassInstance -Id $Obj.ID )
          $objHash = [Ordered]@{
            WFCount = $obj.WFCount
            InstanceID = $Obj.ID
            InstanceFullName = ($thisInstance.FullName)
            Instance = $thisInstance
            Class = $allClasses[($ThisInstance.LeastDerivedNonAbstractManagementPackClassId.Guid)]
            Workflows = [array]($obj.Workflow | ForEach-Object {$allWFTypes[$_] })
          }
          Try {
            $InstanceInfo[$objHash.InstanceFullName] = $objHash
          } Catch {
            Write-Warning "$_"
          }
        }

        # Report #1
        $rpt1 = ForEach ($key in @($InstanceInfo.Keys)) {
          $Key | Select-Object `
          -Property @{Name='WFCount';Expression={$InstanceInfo[$key].WFCount}} `
          ,@{Name='InstanceFullName';Expression={$InstanceInfo[$key].InstanceFullName}} `
          ,@{Name='InstanceID';Expression={$InstanceInfo[$key].InstanceID}} `
          ,@{Name='Instance';Expression={$InstanceInfo[$key].Instance}} `
          ,@{Name='Class';Expression={$InstanceInfo[$key].Class}} `
          ,@{Name='Workflows';Expression={$InstanceInfo[$key].Workflows}} `
        }
        $null = $arrReports.Add($rpt1)
      }#end FOREACH
    }#end PROCESS
  
    #----------------------------------------------------------------
  
    END {
      Return $arrReports
    }#end END
  }
  ######################## END FUNCTIONS ################################
  
  $TaskResult = $NULL
  $Results = $NULL
  $Instance = $NULL
  $TaskWF = $NULL
  $TaskName = 'Microsoft.SystemCenter.GetAllRunningWorkflows'
  $WaitSeconds = 5
  
  Try {
    Write-Verbose "Get SCOM task: $TaskName"
    $TaskWF = Get-SCOMTask -Name $TaskName
    Write-Verbose "Get class instances of Health Service with name(s): $AgentName"
    $Instance = Get-SCOMClass -DisplayName "Health Service" | Get-SCOMClassInstance | Where-Object {$_.Path -in $AgentName}
  } Catch {
    Write-Error "Failed to set up task prereqs for task named: $TaskName. $_`nExiting."
    Return $NULL
  }

  Try {
    If (-NOT $Instance) {Throw "No Instance found!" }
    Write-Verbose "Starting the task now $(Get-Date)..."
    $Task = Start-SCOMTask -Instance $Instance -Task $TaskWF -Verbose -ErrorAction Stop
  } Catch {
    Write-Error "Failed to start task: $TaskName. $_`nExiting. "
    Return $NULL
  }

  $ScriptTimer = [System.Diagnostics.Stopwatch]::StartNew()
  # Keep looping while tasks are still running (Scheduled, Queued, etc. (other than Failed/Succeeded))
  Do {
    $secondsRemain = "{0:N0}" -F $($TimeoutSeconds - $ScriptTimer.Elapsed.TotalSeconds)
    Write-Verbose "`n$(Get-Date): Waiting for task(s) to complete. [$($secondsRemain)] seconds remain until timeout. Sleeping for $($WaitSeconds) seconds."
    Start-Sleep -Seconds $WaitSeconds
    $Results = (Get-SCOMTaskResult -Id $Task.Id)
    ForEach ($TaskResult in $Results) {
      $ThisInstance = $Instance | Where-object {$_.Id.Guid -match $TaskResult.TargetObjectId.Guid} 
      Switch ($TaskResult.Status) {
        'Succeeded' {
          Write-Verbose "$($ThisInstance.Path): $($TaskResult.Status)"
          Break;
        }

        Default {
          Write-Warning "$($ThisInstance.Path): $($TaskResult.Status)"
        }
      }#end Switch
    } #end ForEach TaskResult
    
  } While ((([regex]::matches(($Results.Status),'Succeeded').Count + [regex]::matches(($Results.Status),'Failed').Count) -LT ($Instance.Count)) -AND ($ScriptTimer.Elapsed.TotalSeconds -le $TimeoutSeconds) )
  $ScriptTimer.Stop()
  If ($ScriptTimer.Elapsed.TotalSeconds -gt $TimeoutSeconds){
    Write-Warning "Timer expired. Check task status manually. TaskID: [$($Task.Id.Guid)]. Exiting"
    Return $NULL
  }
  Else {
    Switch ($OutputType) {
      'TasktResult' {
        Return $TaskResult
      }
      'Detailed' {
        # dot-sourced to potentially reduce multiple loading of the workflow cache in this function
        Return (. Get-SCOMRunningWorkflowsReport -TaskResult $TaskResult -Verbose:$VerbosePreference )
      }
    }
  }
}#End Get-SCOMRunningWorkflows


#######################################################################

<#
    .SYNOPSIS
    This is used to generate a unique hash string from an ordinary string value.
 
    .NOTES
    Author: Tyson Paul
    Date: 2018.05.30
    Blog: https://monitoringguys.com/
    History:
    Adapted from http://jongurgul.com/blog/get-stringhash-get-filehash/
#>

Function Get-StringHash{
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]

  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [String] $String,

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [ValidateSet("SHA", "SHA1", "MD5", "SHA256", "SHA384", "SHA512","MACTripleDES","RIPEMD160")]
    $Algorithm = "MACTripleDES" #SHA1 is default. https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=netframework-4.8
  )

  $StringBuilder = New-Object System.Text.StringBuilder
  [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{
    [Void]$StringBuilder.Append($_.ToString("x2"))
  }
  $StringBuilder.ToString()
} #End Function
#######################################################################


<#
    .Synopsis
    Will create a graphical structure (.png file) that represents SCOM class taxonomy; all SCOM class attributes, properties, hosting relationships, and discovery relationships for a SCOM class.
     
    .DESCRIPTION
    This function will use the GraphViz package to produce a graph-like structure (.png file) that will represent one or more SCOM
    classes and it's full hierarchy which includes parents, attributes, properties, hosting relationships (both hosted and hosting), and any discoveries which are capable of discovering the related classes.
    Any number of SCOM class objects or names may be piped to the function.
 
    This function relies on the following modules. User will be prompted for permission to install these:
    GraphViz: from the Chocolatey repo. (https://www.graphviz.org/)
    PSGraph: https://github.com/KevinMarquette/PSGraph
 
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2012.DBEngine'
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2014.AlwaysOn.DatabaseReplica' -Caching:$False
 
    The above example will generate a new graph even if one already exists in the default storage directory.
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.Windows.Server.6.2.LogicalDisk' -ShowDiscoveries:$false
 
    The above example will create a graph but will not include Discovery relationships.
    NOTE: If you are not seeing discovery data in your graphs it may be because the graphs are cached without discovery data.
    Try using the caching switch to force new graph files to be created. Discovery data will be included by default.
     
    Example: -Caching:$false
    .EXAMPLE
    PS C:\> Get-SCOMClass -Name *sql* | New-SCOMClassGraph -ShowGraph:$false
 
    The above example will create graph files for ALL SQL classes but will not open the graph files in the default application.
    Typically there are a tremendous number of SQL classes in the SQL management packs.
     
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ID 'ea99500d-8d52-fc52-b5a5-10dcd1e9d2bd'
     
    .EXAMPLE
    PS C:\> (Get-SCOMClassInstance -DisplayName "http://ms01.contoso.com:80/" ).getclasses().Name | New-SCOMClassGraph
 
    The above example will show the class graph for a specific instance of a class type.
     
    .EXAMPLE
    PS C:\> (Get-SCOMClassInstance -DisplayName "SQLEXPRESS" ).getclasses().Name | New-SCOMClassGraph
 
    The above example will show the class graph for a specific instance of a class type.
 
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ShowIndex
 
    The example above will display an html index page for all of the graph files. The html file will open with the default application.
 
    .EXAMPLE
    PS C:\> Get-SCOMClass | Select Name,Displayname, @{N='ManagementPackName';E={$_.Identifier.Domain[0]} }| Out-GridView -PassThru | New-SCOMClassGraph
 
    This example above is a much fancier way to select one or more class names with the help of GridView. This command will get all SCOM classes in the management group and present Name, DisplayName, and ManagementPackName in GridView for easy browsing, filtering, and selection by the user. Select one or more classes from the Grid View, click OK. The selected class name(s) gets piped into the function: New-SCOMClassGraph
 
    .EXAMPLE
    #Example Script
 
    $int=0
    $arr =@()
    ForEach ($class in (Get-SCOMClass)) {
    $obj = New-Object pscustomobject
    $obj | add-member -name 'Index' -Value $int -MemberType NoteProperty
    $obj | add-member -name 'Name' -Value ($Class.Name) -MemberType NoteProperty
    $obj | add-member -name 'DisplayName' -Value ($class.DisplayName) -MemberType NoteProperty
    $arr+=$obj
    $int++
    }
    $arr | Out-GridView -PassThru | New-SCOMClassGraph -Caching:$false -Combine
 
    This example will present a gridview list of classes for you to select. Multi-select supported to combine numerous classes on a single graph. Be careful, don't overdo it.
 
    .Parameter ClassName
    This is the name of a class. (not the DisplayName)
     
    .Parameter Class
    An Operations Manager class object.
 
    .Parameter ID
    ID of a class.
 
    .Parameter Caching
    This will enable/disable the use of preexisting graphs. If $false, a new graph will be created. If $true, the script will look for an existing graph for the class. If no graph exists, a new one will be created.
     
    .Parameter ShowGraph
    This allows the user to generate the graphs in the designated directory without opening them (with the default application associated to the .png file type)
     
    .Parameter ShowDiscoveries
    This will allow the user to omit the discovery relationships.
     
    .Parameter ShowIndex
    This will display an html index page for all of the graph files. The html file will open with the default application.
     
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
 
    History
    2020.12.30 - Improved GraphViz package detection.(Thanks, Mike N.)
    2020.12.08 - Added ability to sanitize class "Description" text; remove unwanted characters which can cause graphviz to puke.
    2020.06.19 - Added property types and additional info to classes and discovery nodes. Index will now get updated every run.
    2020.03.12 - Removed prompts to install GraphViz and Chocolatey packages. Instead provided suggestions on how to install them. Defeated Coronavirus with cigars and whiskey.
    2020.02.28 - Improved temp directory creation/verification
    2019.10.02 - Fixed Discovery nodes bug. Discoveries appear correctly now.
    2019.09.06 - Updated output dir to $env:Windir instead of $env:Temp
    2019.06.25 - Fixed -Combine flaw
    2019.06.24 - Added input field and search feature to index html
    2019.06.06 - Improved Combine functionality.
    Added -ShowIndex switch
    2019.04.29 - Added colored connected arrows to better identify relationships.
    2018.06.14 - Initial release.
 
    .LINK
    Get-SCOMMPFileInfo
    Get-SCOMClassInfo
#>

Function New-SCOMClassGraph {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$false,
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]
  Param (

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass[]]$Class,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 2')]
    [alias('Name', 'DisplayName')]
    [string[]]$ClassName,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 3')]
    [string[]]$ID,

    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [string]$outDir,

    # This will enable/disable the use of a previous graph file. Disable: will create a new/fresh graph file.
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$Caching=$false,

    # When combining multiple classes on the same graph the host node color might not appear correctly. However, any arrows will indicate the relationship type based on color. This will depend entirely on which classes get enumerated first.
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$Combine,

    # This will open the graph file with the default application (.png)
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$ShowGraph=$true,

    # This will include discovery nodes in the graph(s)
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$ShowDiscoveries=$true,

    # This will create/update, then open the index file with the default application (.html)
    [Parameter(Mandatory=$false,
        Position=0,
    ParameterSetName='Parameter Set 4')]
    [switch]$ShowIndex,

    # If foreign/old graphs exist in the output directory (but class no longer exists in this mgmt group), delete graph files.
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 4')]
    [switch]$RemoveForeignGraphs=$false

  )

  Begin{
    #######################################################################

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    . Import-SCOMPowerShellModule
    Import-Module PSGraph,PowerShellGet -ErrorAction SilentlyContinue

    If (-NOT ($PSVersionTable.PSVersion.Major -ge 5)){
      Write-Error "PowerShell version: $($PSVersionTable.PSVersion.Major) detected! "
      Write-Error "Upgrade to PowerShell version 5 or greater to use this function."
      Break
    }

    'OperationsManager' | ForEach-Object {
      If (-Not [bool](Get-Module -Name $_ -ErrorAction SilentlyContinue )) {
        Write-Error "Required module '$_' does not exist. Please install the module or run this function from a machine where the Operations Manager Console exists. Typically the required module will exists wherever the Console has been installed. Exiting."
        Break
      }
    }

    # Verify if GraphViz is present.
    If (-NOT ([bool](Get-Package -Name "GraphViz*" -ErrorAction SilentlyContinue))) {
      Write-Host "Required package 'GraphViz' does not exist." -F Red
      Try{
        Write-Host "`nIs 'Chocolately' Package Manager installed? ( https://chocolatey.org )" -F Gray
        (choco)[0]
        Write-Host "Yes!"

      }Catch {
        Write-Host "`n'Chocolatey' (The Package Manager for Windows at https://chocolatey.org) does not exist OR is outdated." -F Red
        Write-Host "Please install/update 'Chocolatey' to v0.10.15 or later from https://chocolatey.org ."  
        Write-host "`nOne way to install Chocolatey is with the following PowerShell commands:"
        Write-Host -F Cyan @'
Set-ExecutionPolicy Bypass -Scope Process -Force;
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
'@

      } #end Catch
      
      Write-Host "`nUse Chocolatey to install GraphViz with the following PowerShell command:" 
      Write-Host 'choco install graphviz -y' -F Cyan
      Write-Error "`nExiting." -ErrorAction Stop
    }
    
    # Make sure PSGraph is available/loaded
    # https://github.com/KevinMarquette/PSGraph
    If (-Not [bool](Get-Module -Name 'PSGraph' -ErrorAction SilentlyContinue  )) {
      Write-Error "Required module 'PSGraph' does not exist. Please install module."
      $choice = Read-Host "Install module: PSGraph (Y/N) ?"
      While ($choice -notmatch '[y]|[n]'){
        $choice = Read-Host "Y/N?"
      }
      Switch ($choice){
        'y' {
          # Install PSGraph from the Powershell Gallery
          Write-Host "Find-Module PSGraph | Install-Module -Verbose"
          Find-Module PSGraph -Verbose | Install-Module -Verbose
          If ($?){
            Write-Host "Module installed!"
            Import-Module PSGraph -Verbose
          }
          Else {
            Write-Error "Problem installing module.
              You may manually download from this location: 'https://github.com/KevinMarquette/PSGraph'. Additional information here: 'https://kevinmarquette.github.io/2017-01-30-Powershell-PSGraph/'.
            For information on how to install a PowerShell module, see this article: 'https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx'.`nExiting.`n"
;
            Break
          }
        }
        'n' {
          Write-Host "Module will not be installed.`nExiting.`n"
          Break
        }
      }
    }

    ###############################################################
    Function Load-hashBasicMPInfo {
      # Load all MPs into hash. Will need the MP names below
      $MPs = Get-SCOMManagementPack
      $hashMPBasicInfo = @{}
      
      ForEach ($MP in $MPs) {
        $obj = New-Object PSCUSTOMOBJECT
        $obj | Add-Member -MemberType NoteProperty -Name IsSealed -Value $($MP.Sealed.ToString())
        If ($MP.DisplayName.Length -gt 0){
          $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.DisplayName)
        }
        Else {
          $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.Name)
        }
        $obj | Add-Member -MemberType NoteProperty -Name Version -Value $($MP.Version.ToString() )

        $hashMPBasicInfo.Add($MP.Name,$obj)
      }
      
      Return $hashMPBasicInfo
    }
    
    ###############################################################
    # This is a customized version of the original function located in the PSGraph module here: https://github.com/KevinMarquette/PSGraph
    Function Record {
      <#
          .SYNOPSIS
          Creates a record object for GraphViz.
          This is a customized version of the "Record" function that exists in the PSGraph module from Kevin Marquette.
          I added some additional parameters to control font styles.
      #>

      [OutputType('System.String')]
      [cmdletbinding(DefaultParameterSetName = 'Script')]
      param(
        [Parameter(
            Mandatory=$true,
            Position = 0
        )]
        [alias('ID', 'Node')]
        [string]
        $Name,

        [Parameter(
            Position = 1,
            ValueFromPipeline=$true,
            ParameterSetName = 'Strings'
        )]
        [alias('Rows')]
        [Object[]]
        $Row,

        [Parameter(
            Position = 1,
            ParameterSetName = 'Script'
        )]
        [ScriptBlock]
        $ScriptBlock,

        [Parameter(
            Position = 2
        )]
        [ScriptBlock]
        $RowScript,

        [string]
        $Label,

        # Added for color customization of header
        [string]$FONTCOLOR = 'white',
        [string]$BGCOLOR = 'black',

        # Added for color customization of table
        [string]$FILLCOLOR = 'white',
        [string]$STYLE = 'filled',
        [string]$TABLECOLOR = 'black'

      )
      begin
      {
        $tableData = [System.Collections.ArrayList]::new()
        if ( [string]::IsNullOrEmpty($Label) )
        {
          $Label = $Name
        }
      }
      process
      {
        if ( $null -ne $ScriptBlock )
        {
          $Row = $ScriptBlock.Invoke()
        }

        if ( $null -ne $RowScript )
        {
          $Row = foreach ( $node in $Row )
          {
            @($node).ForEach($RowScript)
          }
        }

        $results = foreach ( $node in $Row )
        {
          Row -Label $node
        }

        foreach ( $node in $results )
        {
          [void]$tableData.Add($node)
        }
      }
      end
      {
        #$html = '<TABLE CELLBORDER="1" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="black" align="center"><font color="white"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '')
        $html = '<TABLE CELLBORDER="1" COLOR="'+$TABLECOLOR+'" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="'+$BGCOLOR+'" align="center"><font color="'+$FONTCOLOR+'"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '')
        #Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = "filled"; penwidth = 1; fillcolor = "white"}
        Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = $STYLE; penwidth = 1; fillcolor = $FILLCOLOR}
      }
    }#End Function
    ###############################################################

    Function Dig-Class {
      [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
          SupportsShouldProcess=$true,
          PositionalBinding=$false,
      ConfirmImpact='Medium')]
      [Alias()]
      [OutputType([System.Object[]])]

      Param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass]$Class,

        # Determines if this Class object is hosting another class. Will affect the shape/color
        [Parameter(Mandatory=$false,
            ValueFromPipeline=$false,
            Position=1,
        ParameterSetName='Parameter Set 1')]
        [switch]$IsHosting=$false,

        [System.Collections.Hashtable]$hashCollection=@{}
      )
      Begin {}
      Process{
        Write-Host $Class.Name -ForegroundColor Yellow
            
        $hashThisClass = @{}
        # If a base class exists, dig it
        If ([bool]$Class.Base.ID.Guid){
          $BaseClass = ($hashAllClassesID[$Class.Base.ID.Guid] )
          If (-NOT ($hashCollection[$BaseClass.Name])) {
            $hashCollection = (Dig-Class $BaseClass -hashCollection $hashCollection) 
          }
          Try{
            $hashCollection[$BaseClass.Name]["Edges"] += @{($Class.Name)=('child')} 
          }Catch{
            #Empty Catch is fine here
          }
        }

        # Make sure that if a class is hosting that its 'Hosting' flag gets set. This is important when multiple class targets are combined on a single graph.
        If (($IsHosting) -AND ($hashCollection[$Class.Name]) ) {
          $hashCollection[$Class.Name].Hosting = $True
        }
        
        # If a hosting class exists, dig it.
        If ($Class.Hosted){
          $hashGraph2= @{}
          $hostClass = $Class.FindHostClass()
          If (-NOT ($hashCollection[$hostClass.Name])) {
            $hashHosting = (Dig-Class -Class $hostClass -IsHosting -hashCollection $hashCollection)
            $hashGraph2 = (Merge-HashTables -hmaster $hashCollection -htnew $hashHosting)
            $hashCollection = $hashGraph2
          }
          Else {
            $hashCollection[$hostClass.Name].Hosting = $True
          }

          Try{
            $hashCollection[$hostClass.Name]["Edges"] += @{($Class.Name)=('hosted')}
          }Catch{
            #Empty Catch is fine here
          }
        }
        #endregion


        [System.Collections.ArrayList]$PropNames = @()
        If ([bool]$Class.DisplayName) {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "DisplayName: $($Class.DisplayName.ToString())"))
        }

        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "ID/GUID: $($Class.ID.Guid)"))
        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MP: $($Class.GetManagementPack().Name)"))
        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MPDisplayName: $(($Class.GetManagementPack()).DisplayName)"))
        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MPVersion: $(($Class.GetManagementPack()).Version)"))

        If ($Class.Hosted) {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "Hosted: $($Class.Hosted.ToString())"))
        }
        Else {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Hosted: $($Class.Hosted.ToString())"))
        }

        If ([bool]$Class.Abstract) {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $TRUE -thisString "Abstract: $($Class.Abstract.ToString())"))
        }
        Else{
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Abstract: $($Class.Abstract.ToString())"))
        }

        If ([bool]$Class.Extension) {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_ExtensionFont -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())"))
        }
        Else {
          $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())"))
        }
        
        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Singleton: $($Class.Singleton.ToString())"))
        $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Description: $(Customize-StringLength -String (Sanitize-String -String $Class.Description) -WordCount $LineBreak)"))

        # Get all properties of the class
        $classProps = $Class.GetProperties() | Sort-Object
        $tmpProps = @()
        ForEach ($Prop in $classProps) {
          $n = $Prop | Select-Object -Property Name -ExpandProperty Name
          If ($Prop.Key -eq $true){
            $tmpProps += "<Font Color=`"Red`"><B>[$($Prop.Type)] $($n)</B></Font>"
          }
          Else {
            $tmpProps += "[$($Prop.Type)] $($n)"
          }
        }
        $PropNames += $tmpProps
        
        $hashStyling = Get-DefaultStyling
        $hashThisClass = [Ordered]@{
          "PropNames" = $PropNames
          "Abstract" = $Class.Abstract
          "Singleton" = $Class.Singleton
          "Hosted" = $Class.Hosted
          "Hosting" = $IsHosting
          "Styling" = $hashStyling
        }

        # Keep any previous dig results, specifically Edges already identified.
        If ([bool]($hashCollection[$Class.Name].Edges)){
          $hashThisClass.Edges = $hashCollection[$Class.Name].Edges
        }

        $hashCollection[$Class.Name] = $hashThisClass
        Return $hashCollection
      }#end Process
      End {}
    }
    ###############################################################

    Function Merge-HashTables {
      param(
        $hmaster,

        $htnew
      )
      $hmaster.keys | ForEach-Object {
        $key = $_
        If (-NOT $htnew.containskey($key)) {
          $htnew.Add($key,$hmaster.$key)
        }
        Else {
          ForEach ($E in @($hmaster[$_].Edges.Keys)) {
            Try{
              If (-NOT [System.Object]$htnew[$_].Edges.ContainsKey($E) ){
                [System.Object]$htnew[$_].Edges.Add($E,$hmaster[$_].Edges.$E) 
              } 
            }Catch {
              # Empty catch is fine here
            }
          }
        }
      }
      #$htnew = $htold + $htnew
      return $htnew
    }
    ###############################################################

    Function Stylize-Record {
      # Modify colors for any hosted/hosting records
      Param(
        $hashtable
      )
      $hashtable.Keys | ForEach-Object{
        If ($hashtable[$_].Abstract -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_AbstHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_AbstFill
          $hashtable[$_].Styling.Style = $c_AbstStyle
          $hashtable[$_].Styling.TABLECOLOR = $c_AbstTable
        }
        
        If ($hashtable[$_].Singleton -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_SingletonHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_SingletonHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_SingletonFill
          $hashtable[$_].Styling.Style = $c_SingletonStyle
          $hashtable[$_].Styling.TABLECOLOR = $c_SingletonTable
        }
        
        
        If ($hashtable[$_].Hosted -eq $true) {
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_hostedHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_hostedHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_hostedFill
          $hashtable[$_].Styling.Style = $c_hostedStyling
          $hashtable[$_].Styling.TABLECOLOR = $c_hostedTable
        }

        If ($hashtable[$_].Hosting -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_hostingHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_hostingHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_hostingFill
          $hashtable[$_].Styling.Style = $c_hostingStyle #"filled"
          #$hashtable[$_].Styling.TABLECOLOR = $c_hostingTable
        }

        # Make sure the Abstract classes retain their header bg color
        If ($hashtable[$_].Abstract -eq $true){
          # Table Header
          $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG
        }

        If ($hashtable[$_].Discovery -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_DiscHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_DiscHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_DiscFill
          $hashtable[$_].Styling.Style = $c_DiscStyle
          $hashtable[$_].Styling.TABLECOLOR = $c_DiscTable
        }
      }
      Return $hashtable
    }
    ###############################################################

    Function Stylize-Row {
      [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
          SupportsShouldProcess=$true,
          PositionalBinding=$false,
      ConfirmImpact='Medium')]
      [Alias()]
      [OutputType([System.Object[]])]
      Param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$thisString,

        [string]$Color = 'black',
        [bool]$Bold = $false

      )
      Begin{}
      Process{
        ForEach ($string in $thisString){
          # Remove any existing styling
          $newString = ($String -replace '(<Font)|(Color="[a-zA-Z]*">)|(<B>)|(<\/B>)|(<\/Font>)','' )
          # Add styling
          $newString = $newString | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } | ForEach-Object -Process {"<Font Color=`"$($Color)`"><B>$($_)</B></Font>"}
          If (-NOT $Bold){
            $newString = ($newString -replace '(<B>)|(</B>)','' )
          }
          Return $newString
        }
      }
      End{}
    }
    ###############################################################

    #Remove undesireable characters
    Function Sanitize-String {
      Param (
        [string]$String
      )

      $string = ($string -Replace '“|”', '"' )
      Return $string

    }
    ###############################################################


    ###############################################################
    # A simple function to add a newline at every Nth word
    Function Customize-StringLength {
      Param (
        [string]$String,
        [int]$WordCount = 5
      )
      
      $nthWord = 0
      $finalString = ""
      $spaceRegex = [Regex] " "
      ForEach ($item in ($spaceRegex.split($String))) {
        $nthWord++
        if (($nthWord % $WordCount) -eq 0){
          $item = "<br/>$item"
        }
        $finalString += $item + " "
      }
      $finalString.TrimEnd().TrimStart()
    }
    ###############################################################

    Function Get-DefaultStyling {
      # Default Record style
      $hashStyling = [Ordered]@{
        # Table Header
        FONTCOLOR = $c_defaultHdrFont
        BGCOLOR = $c_defaultHdrBG

        # Table/Rows
        FILLCOLOR = $c_defaultFill
        Style = $c_defaultStyle
        TABLECOLOR = $c_defaultTable
      }
      Return $hashStyling
    }
    ###############################################################

    Function Get-Discoveries {
      Param(
        [hashtable]$hashMaster,
        [hashtable]$hashDiscoveries = @{}
      )

      #$discHash = @{}
      $discHash = $hashDiscoveries.Clone()
      # Get only discoveries which potentially create classes (not relationships)
      $discs = $hashAllDiscoveries.Values | Where-Object { [bool]$_.DiscoveryClassCollection}

      ForEach ($disc in $discs) {
        If ($disc.DiscoveryClassCollection.Count -gt 1){
          $c=1
        }
        Else {
          $c=$null
        }

        ForEach ($discCollection in $disc.DiscoveryClassCollection) {
          $thisHash = @{}
          If ($hashMaster.containsKey([string]$discCollection.typeid.Identifier.path)) {
            $PropNames = [System.Collections.ArrayList]@()
            $Null = $PropNames.Add( (Stylize-Row -thisString "DisplayName: $($Disc.DisplayName)" -Color $c_DiscDisplayName -Bold $true) )
            $Null = $PropNames.Add( (Stylize-Row -thisString "TargetName: $([string]$disc.target.Identifier.Path)" -Color $c_DiscTarget -Bold $true) )
            
            If ($hashAllClasses[$disc.target.Identifier.Path].DisplayName) {
              $Null = $PropNames.Add( (Stylize-Row -thisString "TargetDisplayName: $([string]$hashAllClasses[$disc.target.Identifier.Path].DisplayName)" -Color $c_DiscTarget -Bold $true) )
            }
            Else {
              $Null = $PropNames.Add( (Stylize-Row -thisString "TargetDisplayName: " -Color $c_DiscTarget -Bold $true) )
            }

            $Null = $PropNames.Add( (Stylize-Row -thisString "EnabledByDefault: $([string]$Disc.Enabled)" -Color $c_DiscTarget -Bold $true) )
            $Null = $PropNames.Add( (Stylize-Row -thisString "ID/GUID: $([string]$Disc.ID.Guid)" -Color $c_DiscTarget -Bold $true) )

            $Null = $PropNames.Add( (Stylize-Row -thisString "MP: $([string]$Disc.Identifier.Domain[0])" -Color $c_DiscTarget -Bold $true) )
            
            If ($hashMPBasicInfo[$Disc.Identifier.Domain[0]].DisplayName) {
              $Null = $PropNames.Add( (Stylize-Row -thisString "MPDisplayName: $([string]$hashMPBasicInfo[$Disc.Identifier.Domain[0]].DisplayName)" -Color $c_DiscTarget -Bold $true) )
            }
            
            $Null = $PropNames.Add( (Stylize-Row -thisString "MPVersion: $([string]$hashMPBasicInfo[$Disc.Identifier.Domain[0]].Version)" -Color $c_DiscTarget -Bold $true) )
            $NULL = $PropNames.Add((Stylize-Row -thisString "Description: $(Customize-StringLength -String (Sanitize-String -String $Disc.Description) -WordCount $LineBreak)" -Color $c_DiscTarget -Bold $true ))

            ForEach ($Item in ($discCollection.propertycollection | Sort-Object PropertyID) ) {
              $tmpClassName = [string]$Item.TypeID.Identifier.Path[0]
              $PropertyName = [string]$Item.PropertyID
              $PropType = (($hashAllClasses[$tmpClassName]).PropertyCollection | Where-Object {$_.Name -eq $PropertyName}).Type.ToString()
              $Null = $PropNames.Add( (Stylize-Row -thisString "[$($PropType)] $PropertyName" -Color $c_DiscRow) )
            }


            $thisHash.Add('PropNames',$PropNames)
            $thisHash.'Edges' += @([string]$discCollection.typeid.Identifier.path)
            $thisHash.'Label' = $disc.Name
            $thisHash.'Discovery' = $true
            $thisHash.'Styling' = Get-DefaultStyling
            $c++
            Try{
              $discHash.Add(([string]$disc.Name+$c),$thisHash) 
            }Catch{ 
              #empty Catch is fine here
            }
          }
        }
      }
      Return $discHash
    }
    ###############################################################

    Function New-Graph {

      # If all classes are meant to be combined into a single graph, then the filename will be calculated with a hash function.
      If ($Combine) {
        $hashstring = Get-StringHash -String ($hashGraph.GetEnumerator() | Out-String)
        $outPath = (Join-Path $OutDir ("MD5Hash_$($hashstring)" +".png" ))
      }

      Graph {
        #region Classes
        ForEach ($ClassKey in @($hashGraph.Keys)) {
          # These will determine if Hosted/Hosting nodes appear in the Legend at the top of the .png
          If ([bool]$hashGraph[$ClassKey].Hosted) {$HostedExists = $true}
          If ([bool]$hashGraph[$ClassKey].Hosting) {$HostingExists = $true}
          If ([bool]$hashGraph[$ClassKey].Singleton) {$SingletonExists = $true}

          $params = @{
            Name = $ClassKey
            Row = $hashGraph[$ClassKey].PropNames
          }
          $params += $hashGraph[$ClassKey].Styling
          Record @params
          ForEach ($E in @($hashGraph[$ClassKey].Edges.Keys | Where-Object {$_.Length -gt 0}) ) {
            If ([bool]$hashGraph.$E.Hosted) {
              $EdgeStyle = "bold"
            }
            Else {
              $EdgeStyle = "bold"
            }          
            [string]$edgeColor = ($hashEdgeColor[$hashGraph[$ClassKey].Edges.$E])
            Edge -From $ClassKey -To $E @{color=$edgeColor;style=$EdgeStyle}
          }
        }
        #endregion Classes

        If ($ShowDiscoveries) {
          #region Discoveries
          $hashDiscoveries.Keys | ForEach-Object {
            $Params = @{
              Name = $_
              Label = $hashDiscoveries[$_].Label
              Row = $hashDiscoveries[$_].PropNames
            }
            $Params += $hashDiscoveries[$_].Styling
            Record @Params
            [string]$edgeColor = ($hashEdgeColor['Discovery'])
            Edge -From $_ -To ($hashDiscoveries[$_].Edges | Select-Object -Unique) @{color=$edgeColor}
          }
          #endregion Discoveries
        }

        #region Legend
        SubGraph -Attributes @{label='Legend'} -ScriptBlock {

          If ($HostedExists) {
            # Output Legend nodes
            Record -Name "Hosted Class" `
            -Row `
            @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
              (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
            'Property') `
            -BGCOLOR $c_hostedHdrBG `
            -FONTCOLOR $c_hostedHdrFont `
            -FILLCOLOR $c_hostedFill `
            -STYLE $c_hostedStyling `
            -TABLECOLOR $c_hostedTable
          }

          If ($HostingExists) {
            Record -Name "Hosting Class" `
            -Row `
            @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
              (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
            'Property') `
            -BGCOLOR $c_hostingHdrBG `
            -FONTCOLOR $c_hostingHdrFont `
            -FILLCOLOR $c_hostingFill `
            -STYLE $c_hostingStyle `
            -TABLECOLOR $c_hostingTable
          }

          If ($SingletonExists) {
            Record -Name "Group (Class)" `
            -Row `
            (Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold)`
            -BGCOLOR $c_SingletonHdrBG `
            -FONTCOLOR $c_SingletonHdrFont `
            -FILLCOLOR $c_SingletonFill `
            -STYLE $c_SingletonStyle `
            -TABLECOLOR $c_SingletonTable
          }

          Record -Name "Abstract Class" `
          -Row `
          @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
            (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
          'Property') `
          -BGCOLOR $c_AbstHdrBG `
          -FONTCOLOR $c_AbstHdrFont `
          -FILLCOLOR $c_AbstFill `
          -STYLE $c_AbstStyle `
          -TABLECOLOR $c_AbstTable

          Record -Name "Class (ordinary)" `
          -Row `
          @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
            (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
          'Property') `
          -BGCOLOR $c_defaultHdrBG `
          -FONTCOLOR $c_defaultHdrFont `
          -FILLCOLOR $c_defaultFill `
          -STYLE $c_defaultStyle `
          -TABLECOLOR $c_defaultTable

          If ($ShowDiscoveries -and ([bool]$hashDiscoveries.Count)) {
            Record -Name "Discovery" `
            -Row `
            @((Stylize-Row -thisString 'Basic Discovery Info' -Color $c_DiscTarget -Bold $true ),`
            (Stylize-Row -thisString 'Discovered Property' -Color $c_DiscRow ))`
            -BGCOLOR $c_DiscHdrBG `
            -FONTCOLOR $c_DiscHdrFont `
            -FILLCOLOR $c_DiscFill `
            -STYLE $c_DiscStyle `
            -TABLECOLOR $c_DiscTable
          }
        } #end SubGraph
        #endregion Legend

      } | Export-PSGraph -DestinationPath $outPath -ShowGraph:$ShowGraph
      #--------------------------------

    }
    #######################################################################
    <#
        .Synopsis
        Will create an html index file for all of the graphs present in the default directory.
        .DESCRIPTION
        This function will enumerate all of the .png files in the default ClassGraph directory and build an html file with links to all of the graphs.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex -indexFileName 'C:\SCOMgraphs\MySCOMClassGraphs.html'
        The above example will generate the index file at the location specified.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex
        The above example will simply generate a new index at the default location.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex -ShowIndex
        The above example will generate a new index at the default location and then open it with the default program.
 
 
        .Parameter Dir
        The output directory where the .png files should be located. This is also where the html index file will be stored.
 
        .Parameter filterExt
        The type of files to locate in the output directory. The index will be built from files of this type.
 
        .Parameter indexFileName
        The name of the index file to create.
 
        .Parameter ShowIndex
        This will open the index file with the default program.
 
        .NOTES
        Author: Tyson Paul
        Blog: https://monitoringguys.com/
 
        History
        2021.05.13.1034 - Updated New-SCOMClassGraphIndex; better handling of foreign graphs (graphs for which no class exists in the mgmt group)
        2019.06.03 - Published to SCOMHelper module
 
        .LINK
        New-SCOMClassGraph
 
    #>

    Function New-SCOMClassGraphIndex {
      Param (
        [string]$outDir,
        [string]$filterExt='png',
        [string]$indexFileName='ClassGraphIndex.html',
        [bool]$RemoveForeignGraphs,
        [switch]$ShowIndex
      )

      If (-Not $outDir ) {
        Write-Verbose "No directory provided for index file. "
        Return
      }
      If (-NOT(Test-Path -Path $outDir -PathType Container)) {
        Write-Verbose "No directory found for index file. "
        Return
      }

      $SearchIconFileName = 'searchicon.png'
      [System.Byte[]]$SearchIcon = @(137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,21,0,0,0,21,8,6,0,0,0,169,23,165,150,0,0,0,1,115,82,71,66,0,174,206,28,233,0,0,0,4,103,65,77,65,0,0,177,143,11,252,97,5,0,0,0,9,112,72,89,115,0,0,18,116,0,0,18,116,1,222,102,31,120,0,0,0,2,98,75,71,68,0,255,135,143,204,191,0,0,0,9,118,112,65,103,0,0,1,42,0,0,1,41,0,80,22,101,49,0,0,0,37,116,69,88,116,100,97,116,101,58,99,114,101,97,116,101,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,142,65,137,81,0,0,0,37,116,69,88,116,100,97,116,101,58,109,111,100,105,102,121,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,255,28,49,237,0,0,0,25,116,69,88,116,83,111,102,116,119,97,114,101,0,119,119,119,46,105,110,107,115,99,97,112,101,46,111,114,103,155,238,60,26,0,0,0,17,116,69,88,116,84,105,116,108,101,0,115,101,97,114,99,104,45,105,99,111,110,194,131,236,125,0,0,2,42,73,68,65,84,56,79,165,148,73,171,234,64,16,133,43,237,172,32,142,224,74,17,87,46,149,44,4,17,197,127,237,74,112,4,5,193,165,162,162,91,39,156,16,231,251,238,41,210,33,209,168,240,238,7,33,73,119,245,169,234,170,234,86,126,126,33,3,247,251,157,166,211,41,173,86,43,58,30,143,60,230,118,187,41,24,12,82,34,145,224,239,111,152,68,7,131,1,141,199,99,114,58,157,36,132,32,69,81,120,28,38,143,199,131,174,215,43,197,98,49,202,100,50,60,254,14,93,180,211,233,208,102,179,33,135,195,193,19,136,24,66,0,226,118,187,157,191,49,14,155,98,177,200,255,86,176,40,34,156,205,102,28,161,20,75,165,82,20,14,135,57,98,56,27,141,70,60,7,241,219,237,70,129,64,128,84,85,213,100,204,40,191,6,63,149,74,133,60,30,15,139,33,103,249,124,94,155,54,211,235,245,104,189,94,179,240,233,116,162,92,46,199,226,207,8,20,5,17,2,136,190,19,4,217,108,150,109,97,135,20,32,255,86,8,84,25,91,196,214,176,229,111,164,211,105,182,197,154,229,114,169,141,154,17,104,27,20,2,222,35,145,136,54,252,158,80,40,196,182,88,35,187,226,25,161,21,159,145,45,244,9,68,104,196,82,84,54,51,4,183,219,45,127,127,2,54,50,74,32,91,205,136,192,73,129,55,155,205,198,109,243,141,225,112,200,66,16,181,170,60,16,241,120,156,46,151,11,111,11,253,215,239,247,181,169,87,224,20,61,43,11,155,76,38,181,25,51,194,235,245,82,52,26,101,65,68,48,159,207,169,217,108,114,63,74,118,187,29,117,187,93,154,76,38,220,82,136,18,15,214,89,161,31,211,106,181,170,159,24,164,3,78,100,17,16,25,210,131,7,230,50,167,216,97,185,92,214,143,182,196,116,161,180,219,109,46,4,162,121,238,4,152,193,41,222,16,193,60,156,226,146,41,149,74,228,114,185,52,203,39,81,176,88,44,248,164,32,119,82,24,139,253,126,63,31,14,116,11,156,227,45,35,62,159,207,124,193,224,168,131,23,81,35,48,6,198,40,0,118,211,106,181,94,132,11,133,2,249,124,190,207,162,159,216,239,247,212,104,52,76,194,184,100,16,241,127,139,130,195,225,64,245,122,93,23,70,154,80,232,63,137,2,220,29,181,90,77,191,233,208,41,127,22,5,200,39,250,24,157,160,170,42,253,3,11,167,101,180,126,138,179,206,0,0,0,0,73,69,78,68,174,66,96,130)

      # This is the cute, little 'search' icon that appears on the master class list search field
      $SearchIconPath = Join-Path $OutDir "CSS\$($SearchIconFileName)"
      If (-NOT(Test-Path -Path ($SearchIconPath) -PathType Leaf )) {
        New-Item -Path (Join-Path $OutDir "CSS") -ItemType Container -Force -ErrorAction SilentlyContinue | Out-Null
        $SearchIcon | Set-Content -Path $SearchIconPath -Encoding BYTE
      }

      # Load all classes into hash
      $SCOMClasses = Get-SCOMClass
      $hashClasses = @{}
      ForEach ($Class in $SCOMClasses) {
        $hashClasses.Add($Class.Name, $Class)
      }

      $head = @'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Class Catalog</title>
    </head>
    <body>
 
<style type="text/css">
 
a { text-decoration: none; }
a:link, a:visited {
    color: blue;
}
a:hover {
    color: red;
}
 
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .defaultstyle{border-color:inherit;vertical-align:center;text-align:left}
.tg .headerstyle{font-weight:bold;font-size:100%;border-color:inherit;vertical-align:center}
.tg .columnNameStyle{border-color:inherit;vertical-align:center;text-align:center}
 
 
.tg .classnamestyle{font-weight:bold;border-color:inherit;vertical-align:center}
.tg .keystyle{font-weight:bold;color:#fe0000;border-color:inherit;text-align:right;vertical-align:center}
.tg .keypropertystyle{font-weight:bold;color:#fe0000;border-color:inherit;vertical-align:center}
.tg .propertystyle{color:#22771a;border-color:inherit;vertical-align:center;horizontal-align:center}
propertystyle
@media screen and (max-width: 767px)
 
{
    .tg {width: auto !important;}
    .tg col {width: auto !important;}
    .tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;}
}
 
#myInput {
    background-image: url('./CSS/searchicon.png'); /* Add a search icon to input */
    background-position: 10px 12px; /* Position the search icon */
    background-repeat: no-repeat; /* Do not repeat the icon image */
    width: 100%; /* Full-width */
    font-size: 16px; /* Increase font-size */
    padding: 12px 20px 12px 40px; /* Add some padding */
    border: 1px solid #ddd; /* Add a grey border */
    margin-bottom: 6px; /* Add some space below the input */
}
 
#myTable tr {
    /* Add a bottom border to all table rows */
    border-bottom: 1px solid #ddd;
}
 
#myTable tr.header, #myTable tr:hover {
    /* Add a grey background color to the table header and on hover */
    background-color: #f1f1f1;
}
 
</style>
 
<script>
function myFunction() {
  // Declare variables
  var input, filter, table, tr, td1,td2,td4,td5,td6, i;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("myTable");
  tr = table.getElementsByTagName("tr");
 
  // Loop through all table rows, and hide those who don't match the search query
  for (i = 0; i < tr.length; i++) {
    td1 = tr[i].getElementsByTagName("td")[1];
     td2 = tr[i].getElementsByTagName("td")[2];
     td4 = tr[i].getElementsByTagName("td")[4];
     td5 = tr[i].getElementsByTagName("td")[5];
     td6 = tr[i].getElementsByTagName("td")[6];
    if (td1) {
      if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1)) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}
</script>
 
 
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
 
<div class="tg-wrap">
    <table id="myTable" class="tg">
 
      <tr>
        <th class="headerstyle">#</th>
        <th class="headerstyle">Class Name</th>
        <th class="headerstyle">DisplayName</th>
        <th class="headerstyle">IsAbstract</th>
        <th class="headerstyle">MP DisplayName</th>
        <th class="headerstyle">MP Name</th>
        <th class="headerstyle">MP Version</th>
        <th class="headerstyle">MP IsSealed</th>
      </tr>
 
'@


      $Rows = ''
      $thisRow = ''
      [int]$i = 0
      $graphFiles = (Get-ChildItem -Path $outDir -Filter *.$($filterExt) -Exclude "*MD5Hash_*","searchicon.png" -Recurse )
      ForEach ($file in $graphFiles) {
        $className = $file.Name.Replace(".$($filterExt)",'')

        # increase row count
        $i++

        #region ForeignGraphDetected
        If ($SCOMClasses.Name -Notcontains $ClassName) {
          
          # If foreign/old graph exists (but class no longer exists in this mgmt group), delete graph file
          If ($RemoveForeignGraphs) {
            $file | Remove-Item -Force
            # Skip to next class
            Continue
          }

          # Keep foreign graph, add it to the list of recognized graphs.
          Else {
            $thisRow = @"
            <tr>
              <td class="defaultstyle">$($i)</td>
              <td class="defaultstyle"><a href="./$($file.Name)">$($className)</a></td>
              <td class="defaultstyle">N/A</td>
              <td class="defaultstyle">N/A</td>
              <td class="defaultstyle">N/A</td>
              <td class="defaultstyle">N/A</td>
              <td class="defaultstyle">N/A</td>
              <td class="defaultstyle">N/A</td>
            </tr>
 
"@

          }
        } #endregion ForeignGraphDetected

        # Class exists in mgmt group so add all related info to list.
        Else {
          Try{
            $thisRow = @"
            <tr>
              <td class="defaultstyle">$($i)</td>
              <td class="defaultstyle"><a href="./$($file.Name)">$($className)</a></td>
              <td class="defaultstyle">$($hashClasses[$className].DisplayName)</td>
              <td class="defaultstyle">$($hashClasses[$className].Abstract)</td>
              <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).DisplayName)</td>
              <td class="defaultstyle">$($hashClasses[$className].Identifier.Domain[0])</td>
              <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).Version)</td>
              <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).IsSealed)</td>
            </tr>
 
"@

          }Catch {
            Write-Error $Error[0].Exception
          }
        }

        $Rows += $thisRow
      } #ForEach

      $end += @'
</table></div>
</body></html>
'@


      $indexFilePath = (Join-Path $outDir $indexFileName)
      Write-Verbose "Index File: $($indexFilePath)"
      ($head + $Rows + $end) | Set-Content $indexFilePath
      
      If ($ShowIndex) { 
        # Open the index file with default program
        & $indexFilePath
      }
    }

    #######################################################################

    $LineBreak = 5
    #region Styling/Colors
    $c_Key = 'Red'
    $s_KeyBold = $true

    $c_classAttribute = '#7503a5'#'#8e8a86'
    $s_ClasAttributeBold = $false #$true

    $c_ExtensionFont = 'green'

    $c_defaultHdrBG = 'black'
    $c_defaultHdrFont = 'white'
    $c_defaultFill = 'white'
    $c_defaultStyle = ''
    $c_defaultTable = 'black'

    $c_DiscHdrFont = 'white'
    $c_DiscHdrBG = '#206dea' #Blue
    $c_DiscFill = ''
    $c_DiscStyle = ''
    $c_DiscTable = '#002560' #DarkBlue

    $c_DiscRow = 'blue'
    $c_DiscDisplayName = '#0646ad'
    $c_DiscTarget = '#0646ad'

    $c_hostedHdrFont = '#d87d22' #'orange' #'#e0bc60'
    $c_hostedHdrBG = 'white' #gray
    $c_hostedFill = '' #'#e0bc60'
    $c_hostedStyling = ''
    $c_hostedTable = 'green' #'orange' #'#c66d00'

    $c_hostingHdrFont = '#cc6602'
    $c_hostingHdrBG = 'white'
    $c_hostingFill = '#ffd089'#'#f7be6a'
    $c_hostingStyle = 'filled'
    $c_hostingTable = 'black' #$c_hostingFill #'#c96d12'
    $hashEdgeColor = [ordered]@{
      Child = 'black'
      Discovery = $c_DiscDisplayName
      Hosted = $c_hostingHdrFont
    }

    $c_AbstHdrFont = '#706d72' #'gray'
    $c_AbstHdrBG = '#e9ccf9' 
    $c_AbstFill = ''
    $c_AbstStyle = ''
    $c_AbstTable = '#e9ccf9'
    
    $c_SingletonHdrFont = 'black' #'gray'
    $c_SingletonHdrBG = '#706d72' 
    $c_SingletonFill = ''
    $c_SingletonStyle = ''
    $c_SingletonTable = '#706d72'
    
    #endregion Styling/Colors

    # If multiple classes are meant to be combined on a single graph, the existence this variable will enable that functionality.
    If ($Combine){
      $hashGraph = @{}
    }

    # Ensure an output directory has been specified
    If (-Not [Bool]$outDir ) {
      $outDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'New-SCOMClassGraph' ) )
    }

    # Attempt to create temp folder if needed
    If (-NOT (Test-Path -Path $OutDir -PathType Container)){
      Write-Host "Attempting to create temp folder: $OutDir" -F Gray
      Try{
        New-Item -ItemType Directory -Path $OutDir -Force -ErrorAction Stop
      }Catch{
        Write-Host "Failed to create temp directory. " -F Yellow -B Red
        Write-Host "Run this tool as administrator." -F Yellow -B Red
        Write-Host "If that doesn't work, create this folder path manually: [$($OutDir)]" -F Yellow -B Red
        $null = Read-Host "Press any key to exit." 
        Exit
      }
    }

    If ($ShowIndex) { 
      # Create a new index file, then open it
      New-SCOMClassGraphIndex -outDir $outDir -ShowIndex -RemoveForeignGraphs $RemoveForeignGraphs
      Return
    }

    # Build basic MP info hash
    $hashMPBasicInfo = Load-hashBasicMPInfo
    If ($ShowDiscoveries){
      . Load-SCOMCache -LoadDiscoveries
    }
    . Load-SCOMCache -LoadClasses
  }#end Begin


  Process{
    #$hashGraph = @{}
    $hashDiscoveries = @{}
    If ([Bool]$ID){
      #[System.Collections.ArrayList]$Class = @()
      $Class = New-Object -TypeName System.Collections.ArrayList
      #Get any matching keys(ID) from $hashAllClassesID
      ForEach ($thisID in $ID) {
        $thisID = $_ -Replace '{|}',''
        If ($hashAllClassesID[$thisID]) {
          $NULL = $Class.Add(($hashAllClassesID[$thisID]))
        }
      }
    }
    ElseIf ([Bool]$ClassName){
      [System.Collections.ArrayList]$Class = @()
      
      ForEach ($thisClassName in $ClassName) {
        If (-NOT ($hashAllClasses.ContainsKey($thisClassName)) ){
          Write-Host "WARNING: Class with name: [$($thisClassName)] does not exist!" -F Red -BackgroundColor Yellow
          continue
        }

        $NULL = $class.Add(($hashAllClasses[$thisClassName]))
      }
    }
    
    ForEach ($thisClass in $Class) {
      $HostedExists = $false
      $HostingExists = $false
      $SingletonExists = $false
      $fileNameBase = $thisClass.Name

      If (-NOT $Combine) {
        $outPath = (Join-Path $OutDir ("$fileNameBase" +".png" ))
      }
      # If the graph has already been created, no sense creating it again. Open exising file.
      If ($caching) {
        If ($Combine) {
          Write-Error "Cannot use -Combine:true -Caching:true together"
          Break
        }
        ElseIf (Test-Path -Path $outPath -PathType Leaf) {
          Invoke-Item $outPath
          Break
        }
      }


      If (-NOT $Combine){
        # This will clear the variable for every iteration, start a fresh graph of class
        $hashGraph = @{}

        #This will dig into the Class taxonomy and identify the entire family tree, storing all parents/properties/relationships into a hash table.
        $hashGraph = (Dig-Class -Class $thisClass )

        If ($ShowDiscoveries) {
          # This will clear the variable for every iteration, start a fresh graph of discoveries
          $hashDiscoveries = @{}
          #This will get any discoveries related to any of the classes
          $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph 
        
          # This will stylize the Discovery nodes.
          $hashDiscoveries = Stylize-Record -hashtable $hashDiscoveries
        } 

        # This will stylize the class nodes (names, properties) based on their type (abstract, hosted, hosting, etc.)
        $hashGraph = Stylize-Record -hashtable $hashGraph
        # Will output individual graphs
        New-Graph 
      }
      Else {
        #This will dig into the Class taxonomy and identify the entire family tree, storing all parents/properties/relationships into a hash table.
        $hashGraph = (Dig-Class -Class $thisClass -hashCollection $hashGraph)
      }
    } #end ForEach Class

  }#end Process

  End{
    
    # Will output one graph to display all classes
    If ($Combine){ 
      If ($ShowDiscoveries) {
        #This will get any discoveries related to any of the classes.
        $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph -hashDiscoveries $hashDiscoveries
        
        # This will stylize the Discovery nodes.
        $hashDiscoveries = Stylize-Record -hashtable $hashDiscoveries
      }
      # This will stylize the class nodes (names, properties) based on their type (abstract, hosted, hosting, etc.)
      $hashGraph = Stylize-Record -hashtable $hashGraph
      New-Graph 
    }

    If (-NOT $ShowIndex) { 
      # Create a new index file, then open it
      New-SCOMClassGraphIndex -outDir $outDir -RemoveForeignGraphs $RemoveForeignGraphs
    }
  }#end End

}#end New-SCOMClassGraph

#######################################################################

<#
    .DESCRIPTION
    Will create an instance group containing Windows Computers [Microsoft.Windows.Computer] objects and optionally include related Health Service Watchers [Microsoft.SystemCenter.HealthServiceWatcher].
 
    .EXAMPLE
    "ms01.contoso.com","db01.contoso.com" | New-SCOMComputerGroup -MPName "Accounting Team Computer/HSW Group 2020" -NameSpace "Accounting" -Verbose
 
    .EXAMPLE
    $FQDNs = (Get-SCOMAgent | Where-Object Name -like "*DB*").Name
    New-SCOMComputerGroup -MPName "My Database Computers-HSW Group" -ComputerName $FQDNs -NameSpace "DBSERVERS" -AddHealthServiceWatchers -Verbose -PassThru
 
    The above example will get all agent names which match the pattern and add the Computer objects as well as the corresponding Health Service Watcher objects.
 
    .INPUTS
    Accepts array of computer FQDN names (fully qualified domain name)
 
    .OUTPUTS
    SCOM Management Pack [Microsoft.EnterpriseManagement.Configuration.ManagementPack]
 
    .NOTES
    Script: New-SCOMComputerGroup
    Author: Tyson Paul (https://monitoringguys.com/2019/11/12/scomhelper/)
    Version History:
    2020.08.18.1333 - Added MP and Group display name parameters
    2020.08.11.1309 - Polished a bit. HSW objects are optional.
    2020.06.17 - v1.0
#>

Function New-SCOMComputerGroup {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$false, 
      PositionalBinding=$false,
      HelpUri = 'http://www.microsoft.com/',
  ConfirmImpact='Medium')]
  [Alias()]
  [OutputType([Microsoft.EnterpriseManagement.Configuration.ManagementPack])]

  Param (
    # New MP custom name
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [string]$MPName='',

    # New MP custom DISPLAY name
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [string]$MPDisplayName='',


    # Name of computer to be added to the group (and associated Health Service Watcher instance)
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string[]]$ComputerName,

    # Name of mgmt server to use for SDK connection
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [string]$MgmtServerName,
    
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [string]$GroupName,

    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [string]$GroupDisplayName,

    # Namespace for group name. Example: "Lab" or "Accounting". This will affect only the hidden/ugly element "ID" in the MP.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [string]$NameSpace = "GROUP",

    # This will include corresponding Health Service Watcher objects in the group.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [switch]$AddHealthServiceWatchers,
    
    # This will return the new [Microsoft.EnterpriseManagement.Configuration.ManagementPack] object.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false, 
    ParameterSetName='Parameter Set 1')]
    [switch]$PassThru

  )

  Begin {

    ###################################### FUNCTIONS #######################################################
    ########################################################################################################
    Function Build-RefString {
      Param (
        $MP
      )
      Return ("$($MP.Name)$($MP.version.tostring())".Replace('.',''))
    }
    ########################################################################################################
    Function Clean-DisplayName {
      [OutputType([String])]
      Param(
        # Param1 help description
        [Parameter(Mandatory=$false, 
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false, 
            ValueFromRemainingArguments=$false, 
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [string]$Name=''
      )
      Begin{}
      Process{
        Return ($Name -replace '[^a-zA-Z0-9. \-()]', '')
      }
      End {}
    }
    ########################################################################################################
    Function Clean-ID {
      [OutputType([String])]
      Param(
        # Param1 help description
        [Parameter(Mandatory=$false, 
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false, 
            ValueFromRemainingArguments=$false, 
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [string]$Name=''
      )
      Begin{}
      Process{
        $tmp = $Name.Replace(' ','.').Replace('..','.').Replace('..','.')
        $tmp2 = ( ($tmp -replace '[^a-zA-Z0-9.]', '').Replace('..','.').Replace('..','.') )
        Return $tmp2
      }
      End{}
    }
    ########################################################################################################
    Function _LINE_ { 
      $MyInvocation.ScriptLineNumber 
    }
    ########################################################################################################
    ########################################################################################################
    Function Connect-MgmtGroup {
      # Connect to SCOM mgmt group
      Try{
        $MG = Get-SCOMManagementGroup -ErrorAction Stop -Verbose:$VerbosePreference
        If (-NOT $MG ) {
          Write-Verbose "No SCOMManagementGroupConnection"
          Throw
        }
        ElseIf($MgmtServerName) {
          If ($MG.ConnectionSettings.ServerName -ne $MgmtServerName){
            Write-Verbose "Existing MG connection, wrong server: [$($MG.ConnectionSettings.ServerName)]. Establish connection to new server: [$($MgmtServerName)]..."
            Throw
          }
        }
      } Catch {
        Try {
          $MG = Connect-OMManagementGroup -SDK $MgmtServerName
          If (-NOT $MG) {
            Throw "No SCOMManagementGroupConnection"
          }
        }Catch {
          Write-Error "$(_LINE_): Unable to establish connection to designated mgmt server: [$($MgmtServerName)]. Exiting."
          Exit
        }
      }
    }
    ########################################################################################################

    $DateStamp = (Get-Date -f "yyyyMMdd.hhmmss")
 
    # Clean names to remove invalid characters
    $CleanMPDisplayName = Clean-DisplayName -Name $MPDisplayName
    $CleanMPID = Clean-ID -Name $MPName
    If (-NOT $CleanMPID) {
      $CleanMPID = Clean-ID -Name $CleanMPDisplayName
    }

    If (-NOT $CleanMPID) {
      Write-Error "No valid MPName provided. Exiting."
      Return 
    }
    
    # Will try to accomodate both group Name and DisplayName
    If ($GroupDisplayName) {
      Write-Verbose "Group DisplayName provided. Will attempt to clean it..."
      $GroupDisplayName = $GroupDisplayName | Clean-DisplayName
    }
    $CleanGroupID = $GroupName | Clean-ID
    If (-NOT $CleanGroupID) {
      Write-Verbose "No valid GroupName. Attempt to use cleaned group DisplayName (if provided)..."
      $CleanGroupID = $GroupDisplayName | Clean-ID
    }
    If (-NOT $CleanGroupID) {
      $CleanGroupID = "$($CleanMPID).$($DateStamp).Group"
      Write-Verbose "No valid GroupName. Will use default group naming: [$($CleanGroupID)]"
    }

    Write-Verbose @"
New MP DisplayName provided by user:[$MPName]
DisplayName after being sanitized:[$($CleanMPDisplayName)]
MPID after being sanitized:[$($CleanMPID)]]
 
"@


    Write-Verbose "Attempt import of OperationsManager module"
    . Import-SCOMPowerShellModule
    
    Write-Verbose "Verify mgmt group connection..."
    . Connect-MgmtGroup

    Write-Verbose "Get necessary MP objects for building group definitions..."
    $Formula = ''
    # Get the necessary reference MPs
    $LibMp = Get-SCOMManagementPack -Name Microsoft.Windows.Library
    $InsMp = Get-SCOMManagementPack -Name Microsoft.SystemCenter.InstanceGroup.Library
    $SCmp = Get-SCOMManagementPack -Name Microsoft.SystemCenter.Library

    # Build the alias Strings
    $Libalias = Build-RefString -MP $LibMp
    $InsAlias = Build-RefString -MP $InsMp
    $SCAlias = Build-RefString -MP $SCmp
   
    # Check if the MP already exists if not we will create it
    $MPAlreadyExists = Get-SCOMManagementPack -Name $CleanMPID
    If($MPAlreadyExists)
    {
      Throw @"
 
Error: Management pack with name [$($MPName)] already exists.
New MP DisplayName provided by user:[$MPName]
DisplayName after being sanitized:[$($CleanMPDisplayName)]
MPID after being sanitized:[$($CleanMPID)]]
 
"@

      Try {$MG.Dispose()}Catch{<#Tidy up#>}
      Exit
    }

    [System.Collections.ArrayList]$arrWinComp = @()
    [System.Collections.ArrayList]$arrHSW = @()
  
    $WinCompClass = Get-SCOMClass -Name "Microsoft.Windows.Computer"
    $HSWClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthServiceWatcher"
  }#end Begin

  Process {
    # Create collection of Computers and HSW objects
    ForEach ($CName in $ComputerName) {
      $WinComp = $NULL
      $Criteria = "DisplayName = '$CName'"
    
      $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$WinCompClass)
      $WinComp = $MG.GetMonitoringObjects($objCriteria)
      If (-NOT $WinComp) {
        Write-Warning "Computer: [$($CName)] not found! Verify that the fully qualified domain name exists."
        Continue
      }
      $CompID = $WinComp.Id.Guid
      Write-Verbose "Found Computer:`t[$($CName)], [$($CompID)]"
      $Null = $arrWinComp.Add(" <MonitoringObjectId>$CompID</MonitoringObjectId>")
    
      If ($AddHealthServiceWatchers) {
        $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$HSWClass)
        $HSW = $MG.GetMonitoringObjects($objCriteria)  
        $WatchID = $HSW.Id.Guid
        Write-Verbose "Found HSW:`t`t[$($CName)], [$($WatchID)]"
        $Null = $arrHSW.Add(" <MonitoringObjectId>$WatchID</MonitoringObjectId>")
      }
    }
  }#end Process

  End {
    # Add Windows Computer GUIDs to explicit membership rule
    $Formula += @"
  <MembershipRule>
  <MonitoringClass>`$MPElement[Name="$($Libalias)!Microsoft.Windows.Server.Computer"]`$</MonitoringClass>
  <RelationshipClass>`$MPElement[Name="$($InsAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass>
  <IncludeList>
  $arrWinComp
  </IncludeList>
  </MembershipRule>
"@

    # Add HSW GUIDs to explicit membership rule, if appropriate
    If ($AddHealthServiceWatchers) {
      $Formula += @"
 
  <MembershipRule>
  <MonitoringClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.HealthServiceWatcher"]`$</MonitoringClass>
  <RelationshipClass>`$MPElement[Name="$($InsAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass>
  <IncludeList>
  $arrHSW
  </IncludeList>
  </MembershipRule>
"@

    }

    $store = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore
  
    # Create the MP Object
    Write-Verbose "Create new MP object (virtual)..."
    $MP = New-Object Microsoft.EnterpriseManagement.Configuration.ManagementPack($CleanMPID, $CleanMPID, (New-Object Version(1, 0, 0)), $store)

    # Set the MP Display Name
    $MP.DisplayName = $CleanMPDisplayName

    # Add MP references
    Write-Verbose "Add reverences to MP object..."
    $LibRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $LibMp.Name, $LibMp.KeyToken, $LibMp.Version)
    $InsRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $InsMp.Name, $InsMp.KeyToken, $InsMp.Version)
    $SCRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $SCmp.Name, $SCmp.KeyToken, $SCmp.Version)
    $MP.References.Add($Libalias,$LibRef)
    $MP.References.Add($InsAlias,$InsRef)
    $MP.References.Add($SCAlias,$SCRef)

    Try {
      # Save Changes
      $MP.AcceptChanges()
      Write-Verbose "MP object was verified successfully."
    } Catch {
      Throw "Failed to verify new MP content.`nError:$($_)`nExiting."
      Try {$MG.Dispose()}Catch{<#Tidy up#>}
      Exit
    }
  
    Try {
      # Import MP
      Write-Verbose "Importing new empty MP into management group: [$($MG.Name)]..."
      $MG.ImportManagementPack($mp)
    } Catch {
      Throw "Failed to import new MP.`nError:$($_)`nExiting."
      Try {$MG.Dispose()}Catch{<#Tidy up#>}
      Exit
    }  

    #TYSON $GroupID = "$($CleanMPID).Group"
    $GroupID = "$($CleanGroupID)"
    If ($GroupID -notmatch 'group$') {
      $GroupID = "$($GroupID).Group"
    }

    # Create Group Object and try to insert it into the MP.
    Write-Verbose "Create new group object: [$($GroupID)]..."
    $group = New-Object Microsoft.EnterpriseManagement.Monitoring.CustomMonitoringObjectGroup($NameSpace, $GroupID, $CleanMPID, $Formula)
    If ($GroupDisplayName) {
      $group.DisplayName = $GroupDisplayName
    }
    Try{
      Write-Verbose "Inserting new Group formula into MP..."
      $MP.InsertCustomMonitoringObjectGroup($Group)
      Write-Verbose "Success!"
    }
    Catch{
      Throw "Failed to add group formula to MP.`nError:$($_)`nExiting."
      Try {$MG.Dispose()}Catch{<#Try to tidy up#>}
      Exit
    }
  
    # Finally, get the new pack
    If ($PassThru) {
      Get-SCOMManagementPack -Name $CleanMPID 
    }

  }#end End
}#End Function

#######################################################################

<#
    .SYNOPSIS
    Will ping all entries in your HOSTS file (C:\WINDOWS\System32\drivers\etc\hosts) very quickly.
 
    .EXAMPLE
    PS C:\> Ping-AllHosts -Count 30
 
    Will ping all hosts a total of 30 times, pausing for the default 10 seconds inbetween iterations.
    .EXAMPLE
    PS C:\> Ping-AllHosts -t -DelayMS 30000
 
    Will continue to ping hosts waiting 30 seconds inbetween iterations.
    .EXAMPLE
    PS C:\> Ping-AllHosts -Count 10
 
    Will ping all hosts a total of 10 times.
    .NOTES
    Name: Ping-AllHosts
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Date: 2018.04.30
    History: v1.0
 
#>

Function Ping-AllHosts {
  Param (
    [long]$Count=1,
    [long]$DelayMS=10000,
    [switch]$t
  )

  $HOSTSfile = (Join-Path $env:SystemRoot '\System32\drivers\etc\hosts')
  If (-NOT (Test-Path $HOSTSfile -PathType Leaf) ) {
    Write-Error "Cannot find 'hosts' file at: $HOSTSFile . Exiting."
    Return
  }
  If ($t) {
    $Count = 9999999999999
    Write-Host "`$Count = $Count" -ForegroundColor Yellow

  }
  [long]$i=1
  While ($i -le $Count) {
    Write-Host "Attempt: $i. Remaining attempts: $($Count - $i)" -F Green
    # Parse the Hosts file, extract only the lines that do NOT begin with hash (#). Discard any leading tabs or spaces on all lines. Ignore blank lines
    $lines = (( (Get-Content $HOSTSfile).Replace("`t",'#').Replace(" ",'#').Replace("##",'#') | Where-Object {$_ -match '^[^#]'}))  | Where-Object {$_.Length -gt 1}

    $hash = @{}
    # Assign names and IPs to hash table
    $lines | ForEach-Object { $hash[$_.Split('#')[1]] = $_.Split('#')[0] }
    If (-Not [bool]$lines) {
      Write-Host "No valid hosts found in file. Exiting."
      Return
    }
    # Perform simultaneous address connection testing, as job.
    $job = Test-Connection -ComputerName ($hash.Keys | Out-String -stream) -Count 1 -AsJob

    Do{
      Write-Host 'Waiting for ping test to complete...' -F Yellow
      Start-Sleep 3

    }While($job.JobStateInfo.State -eq "Running")
    Receive-Job $job |
    Select-Object @{N="Source"; E={$_.PSComputerName}},
    @{N="Target"; E={$_.Address}},
    IPV4Address,
    @{N='Time(ms)';E={($_.'ResponseTime')} },
    @{N='Alive?';E={([bool]$_.'ResponseTime')} } |
    Sort-Object Target |
    Format-Table -AutoSize

    If ($i -eq $Count) {Return}
    $i++
    Write-Host "Sleeping $($DelayMS / 1000) Seconds before next ping test..."
    Start-Sleep -Milliseconds $DelayMS
  }
}#end Function
#######################################################################

<#
    .Synopsis
    Will remove obsolete aliases from unsealed management packs.
    .DESCRIPTION
    This will not alter the original file but rather will output modified versions to the designated output folder.
 
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\*.xml' -outDir 'C:\Usealed MPs\Modified MPs'
 
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\MyOverrides.xml' -outDir 'C:\Usealed MPs\Modified MPs'
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile (GetChildItem 'C:\Unsealed MPs\*.xml').FullName
 
    .EXAMPLE
    PS C:\> Get-ChildItem -Path 'C:\Unsealed MPs\*.xml' | Remove-SCOMObsoleteReferenceFromMPFile
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Version: 1.0
    Date: 20018.05.09
#>

Function Remove-SCOMObsoleteReferenceFromMPFile {
  [CmdletBinding()]

  Param (
    # Path to input file(s) (YourUnsealedPack.xml)
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
    Position=0)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("FullName")]
    [string[]]$inFile,

    # Path to output folder, to save modified .xml file(s)
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
    Position=1)]
    [string]$outDir
  )

  Begin{}
  Process{
    [System.Object[]]$paths = (Get-ChildItem -Path $inFile )
    ForEach ($FilePath in $paths){
      If (-not [bool]$outDir){
        $outDir = (Join-Path (Split-Path $FilePath -Parent) "MODIFIED")
      }
      If (-NOT (Test-Path -Path $outDir) ){
        New-Item -Path $outDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
      }
      Write-Host "Input File: " -NoNewline; Write-Host $FilePath -F Cyan
      Try{
        [xml]$xml = Get-Content -Path $FilePath
        $References = $xml.ManagementPack.Manifest.References
      }Catch{
        Continue
      }
      If (-NOT [bool]$References) {Continue}
      $References.SelectNodes("*") | ForEach-Object {
        # If neither the Alias or ID of the referenced MP is found within the body of the MP, remove the reference.
        If (-NOT (($xml.InnerXml -match ("$($_.Alias)!")) -or ($xml.InnerXml -match ("$($_.ID)!"))) ) {
          Write-Host "Obsolete Reference Removed: $($_.Alias) : $($_.ID)" -F Green
          $References.RemoveChild($_) | Out-Null
        }
      }
      $outFile = (Join-Path $outDir (Split-Path $FilePath -Leaf))
      Write-Host "Saving to file: " -NoNewline; Write-Host $outFile -F Yellow
      $Xml.Save($outFile)
    }
  }
  End{}
}# END FUNCTION
#######################################################################

<#
    .Synopsis
    Will standardize all of the aliases in one or more unsealed (.xml) management pack files.
    .DESCRIPTION
    This script will inventory all of the MP reference IDs that can be found in the single file or set of .XML files specified by the InputPath. It will create a customized set of condensed alias acronyms to be standardized across all of the unsealed management packs. It will then replace the aliases in the Manifest as well as throughout the elements where the aliases are used.
    .Parameter InputPath
    This can be a directory or full path to an unsealed management pack .xml file.
    .EXAMPLE
    PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\'
    .EXAMPLE
    PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\MyCustom.Monitoring.xml'
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Date: 2017/12/13
    History:
    2017/12/13: First version
#>

Function Set-SCOMMPAliases {
  Param (
    [Parameter(Mandatory=$true)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({Test-Path -Path $_})]
    [string]$InputPath #Either a folder or full file path
  )

  ##### TESTING ######
  #$InputPath = 'C:\Test\Test\TempTest'
  ##### TESTING ######

  # This will be used to seed the reference catalog with some hardcoded values for some common aliases
  $seed =  @'
Microsoft.SystemCenter = SC
Microsoft.SystemCenter.Apm.Infrastructure = APMInf
Microsoft.SystemCenter.Apm.Infrastructure.Monitoring = APMInfMon
Microsoft.SystemCenter.Apm.Library = APMLib
Microsoft.SystemCenter.Apm.NTServices = APMNT
Microsoft.SystemCenter.Apm.Wcf = APMWcf
Microsoft.SystemCenter.Apm.Web = APMWeb
Microsoft.SystemCenter.DataWarehouse.ApmReports.Library = APMReports
Microsoft.SystemCenter.InstanceGroup.Library = SCIGL
Microsoft.SystemCenter.Internal = SCInt
Microsoft.SystemCenter.Library =SCLib
Microsoft.SystemCenter.WebApplication.Library = WebAppLib
Microsoft.SystemCenter.WebApplicationSolutions.Library = WebAppSolLib
Microsoft.SystemCenter.WebApplicationSolutions.Library.Resources.ENU = WebAppLibRes
Microsoft.SystemCenter.WebApplicationTest.External.Library = WebAppTestExtLib
Microsoft.SystemCenter.WebApplicationTest.Library = WebAppTestLib
Microsoft.Windows.InternetInformationServices.2003 = IIS2003
Microsoft.Windows.Library = Windows
Microsoft.Windows.Server.Library = WinSrvLib
System.Health.Library = Health
System.Library = System
System.NetworkManagement.Library = NetManLib
System.NetworkManagement.Monitoring = NetManMon
System.NetworkManagement.Reports = NetManRpt
System.NetworkManagement.Templates = NetManTmp
System.Performance.Library = Performance
'@

  # $seed -split "`r`n" | Sort-Object

  ##############################################################
  # Will replace common/popular namespaces with smaller, friendly acronym
  Function AcronymCommonNames {
    Param (
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [string]$thisString
    )
    $acronyms = ConvertFrom-StringData -StringData @"
Microsoft.SQLServer = MSQL
Microsoft.Windows.Server = MWS
Microsoft.Windows.InternetInformationServices = IIS
"@


    If ( $acronyms.ContainsKey($thisString) ) {
      $arrNames = $acronyms.GetEnumerator().Name
      $arrNames | ForEach-Object {
        #Write-Host $_.Name $_.value
        $thisString = $thisString.Replace(($_),($acronyms.$_) )
      }
    }
    Return $thisString
  }


  ##############################################################
  Function Create-Alias {
    Param(
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [string]$Seed,

      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      # array of existing values, to prevent duplicates
      [system.Object[]]$Existing
    )

    $array = AcronymCommonNames -thisString $Seed
    $array = $array.Split('.')
    $newAlias = $array[0].Replace('Microsoft','').Replace('Windows','Win').Replace('HewlettPackard','HP')
    For($i = 1; $i -lt $array.Count; $i++) {
      $fragment = $array[$i]
      If ( $fragment -match '\d+' ) {
        $newAlias += $fragment
      }
      ElseIf ( $fragment -match '^ApplicationMonitoring.?' ) {
        $newAlias += 'AppMon'
      }
      ElseIf ( $fragment -match '^Dashboard.?' ) {
        $newAlias += 'Dash'
      }
      ElseIf ( $fragment -match '^Discovery.?' ) {
        $newAlias += 'Disc'
      }
      ElseIf ( $fragment -match '^Library.?' ) {
        $newAlias += 'Lib'
      }
      ElseIf ( $fragment -match '^Linux.?' ) {
        $newAlias += 'Linux'
      }
      ElseIf ( $fragment -match '^Monitor.?' ) {
        $newAlias += 'Mon'
      }
      ElseIf ( $fragment -match '^NetworkManagement.?' ) {
        $newAlias += 'NetMan'
      }
      ElseIf ( $fragment -match '^Network.?' ) {
        $newAlias += 'Net'
      }
      ElseIf ( $fragment -match '^Report.?' ) {
        $newAlias += 'Rpt'
      }
      ElseIf ( $fragment -match '^Server.?' ) {
        $newAlias += 'Ser'
      }
      ElseIf ( $fragment -match '^System.?' ) {
        $newAlias += 'Sys'
      }
      ElseIf ( $fragment -match '^Template.?' ) {
        $newAlias += 'Tmp'
      }
      ElseIf ( $fragment -match '^Unix.?' ) {
        $newAlias += 'Unix'
      }
      ElseIf ( $fragment -match '^Visual.?' ) {
        $newAlias += 'Vis'
      }
      ElseIf ( $fragment -match '^Windows.?' ) {
        $newAlias += 'Win'
      }
      Else {
        # Use capitalized letters in MP name to build the acronym
        $tempchar = ''
        [char[]]$fragment | ForEach-Object {
          If ([char]::IsUpper($_) ){$tempchar+=$_ }
        }
        # $newAlias += $fragment[0]
        $newAlias += $tempchar
      }
    }
    $i = 2
    $tempAlias = $newAlias
    While ($Existing -contains ($tempAlias)) {
      $tempAlias = $newAlias +"$i"
      $i++
    }

    Return $tempAlias
  }

  ##############################################################
  # This function will replace all instances of existing aliases with the newly customized aliases.
  Function Standardize-AllReferenceAliases {
    Param(
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [System.Object[]]$MPPaths,

      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [Hashtable]$Catalog
    )
    ForEach ($MPPath in $MPPaths){

      $mpxml = [xml](Get-Content $MPPath)
      $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference
      [int]$i = 0
      $content = (Get-Content $MPPath)
      # Replace aliases wherever used in xml
      ForEach ($ref in ($arrReferences) ) {
        <# Aliases typically appear in two scenarios...
            1) Examples: referring to some element property:
            <Property>$MPElement[Name="Windows!Microsoft.Windows.Computer"]/PrincipalName$</Property>
            <MonitoringClass>$MPElement[Name="SCLib!Microsoft.SystemCenter.ManagedComputer"]$</MonitoringClass>
            <RelationshipClass>$MPElement[Name="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities"]$
        #>

        $aliasrefbang = '="'+"$($ref.Alias)"+"!"
        $newAlias = ('="'+($Catalog.($ref.ID))+'!')
        $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) )

        <#
            2) Examples: referring to a workflow, type, datasource, etc.:
            <DataSource ID="GroupPopulationDataSource" TypeID="SCLib!Microsoft.SystemCenter.GroupPopulator">
            <DiscoveryRelationship TypeID="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities" />
        #>

        $aliasrefbang = '>'+"$($ref.Alias)"+"!"
        $newAlias = ('>'+($Catalog.($ref.ID))+'!')
        $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) )
        #$content | Set-Content $MPPath -Force -Encoding UTF8
      }

      # Reload the XML because it was just rewritten
      #$mpxml = [xml](Get-Content $MPPath)
      $mpxml = [xml]($content)
      $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference
      [int]$i = 0
      # Replace aliases in Manifest
      ForEach ($ref in ($arrReferences) ) {
        $ref.Alias = $Catalog.($ref.ID)
        $i++
      }

      Write-Host "`t$i references updated! [ " -NoNewline -b Black -f Cyan
      Write-Host $(Split-Path -Path $MPPath -Leaf) -NoNewline
      Write-Host " ]" -b Black -f Cyan
      $mpxml.Save($MPPath)
    }
  } #endFunction

  ##############################################################
  <# This function is designed to append (or trim) a unique hash value to/from the aliases
      (in the Manifest as well as elsewhere in the management pack). This is necessary in the rare cases where existing
      aliases already match my customized new condensed acronyms in the $cat (catalog). "Windows!" is a good example
      of this. I've encountered random MPs which have the alias, "Windows" already assigned to a reference. This becomes
      problematic when I attempt to change references to the 'Microsoft.Windows.Library' after which the file ends up with
      multiple identical alias references (to different sealed MPs) using the exact same alias, "Windows". This is a clever way
      to make sure that all aliases are unique (no conflicts). This function should be run twice on the catalog; Once to
      append the unique string to aliases in the catalog (after the cat has been generated, of course), then modify the files.
      Then run again to set aliases to their final, desired values, then modify the files again to their final desired values.
  #>

  Function Modify-Aliases {
    Param(
      [Hashtable]$cat,
      [Switch]$Localized
    )
    $hash = Get-StringHash $env:COMPUTERNAME
    If ($Localized){
      @($cat.Keys) | ForEach-Object {
        $Cat[$_] = $cat[$_]+$hash
      }
    }
    Else{
      @($cat.Keys) | ForEach-Object {
        $Cat[$_] = [string]$cat[$_] -Replace $hash, ""
      }
    }
    Return $cat
  }

  ##############################################################
  #http://jongurgul.com/blog/get-stringhash-get-filehash/
  Function Get-StringHash
  {
    
    param
    (
      [string]
      $String,

      [string]
      $HashName = "MD5"
    )
    $StringBuilder = New-Object System.Text.StringBuilder
    [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{
      [Void]$StringBuilder.Append($_.ToString("x2"))
    }
    $StringBuilder.ToString()
  }

  #-------------------------------------------------------------
  #################### MAIN ################################


  ##############################################################
  # Create catalog of all existing references/aliases
  # The idea here is to seed the catalog with common pack names.
  # For other MPs dynamically create aliases based on doted name structure
  ##############################################################
  $cat = ConvertFrom-StringData -StringData $seed

  If (Test-Path -Path $InputPath -PathType Container -ErrorAction SilentlyContinue) {
    $MPPaths = (Get-ChildItem $InputPath -Recurse -Include *.xml -File ).FullName | Sort-Object
    Write-Host "$($MPPaths.Count) files found..." -F Yellow -B Black
  }
  ElseIf ((Test-Path -Path $InputPath -PathType Leaf -ErrorAction SilentlyContinue) -and ($InputPath -like "*.xml") ) {
    $MPPaths = $InputPath
  }
  Else { Write-Host "Something is wrong with `$InputPath: [$($InputPath)]."; Exit}

  $refIDs = @()
  [int]$i=0
  ForEach ($MPPath in $MPPaths) {
    Write-Host $MPPath -F Green
    $mpxml = [xml](Get-Content $MPPath)
    #$mpxml.SelectNodes("//Reference") | % {
    $mpxml.ManagementPack.Manifest.References.Reference | ForEach-Object {
      $refIDs += $_.ID
      $_
      Write-Progress -Activity "Creating master catalog of known References" -Status $_.ID -PercentComplete ($i / $MPPaths.Count*100)
    }
    $i++
  }
  # Create array of all unique IDs
  $AllRefIDs = ($refIDs | Group-Object ).Name | Sort-Object
  $AllRefIDs | ForEach-Object {
    #If ID not in catalog yet, add it with customized alias
    If ( -not($cat.ContainsKey($_.ToString())) ){
      $cat.Add($_,(Create-Alias -Seed $_ -Existing $cat.Values) )
    }
  }

  # Update all aliases to unique values to prevent naming conflicts when modifying files
  $cat = Modify-Aliases -Localized:$true -cat $cat
  Write-Host "Updating references...(first pass, randomized unique values)" -b Black -f Yellow
  Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat

  # Update all aliases to final, correct values, and modify files
  $cat = Modify-Aliases -Localized:$false -cat $cat
  Write-Host "Updating references...(final pass)" -b Black -f Green
  Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat

  # Display the reference/alias Catalog
  $arrCat = @()
  ForEach ($key in $cat.Keys ){
    #Write-Host $key " = " $cat[$key]
    $arrCat += ("$key = $($cat[$key])" )
  }
  Return ($arrCat | Sort-Object)

} #End Function
#######################################################################

<#
    .Synopsis
    Will display all known modules contained in all sealed management packs as well as basic schema information.
    Based on the original script found here: http://sc.scomurr.com/scom-2012-r2-mp-authoring-getting-modules-and-their-configurations/
    .EXAMPLE
    PS C:\> Show-SCOMModules
 
    .EXAMPLE
    PS C:\> Show-SCOMModules | Out-GridView
 
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Original Date: 2017.12.13
 
    History: 2018.01.22: Refined.
    2017/12/13: First version
#>

Function Show-SCOMModules {

  #############################################
  Function Get-ModuleRunasProfileName {
    Param(
      $Module,
      $Profiles
    )
    $ModuleRunAs = $Profiles | Where-Object { $_.Id.Guid -eq $module.RunAs.Id.GUID }
    Return ($ModuleRunAs.Name+";"+$ModuleRunAs.DisplayName)
  }
  #############################################
  
  $collection=@()
  $RAAP = Get-SCOMRunAsProfile
  ForEach ($MP in (Get-SCManagementPack | Where-Object{$_.Sealed -eq $True})) {
    ForEach($Module in ($MP.GetModuleTypes() | Sort-Object -Property Name | Sort-Object -Property XMLTag)) {
      $RunAs = ''
      $Object = New-Object PSObject
      $Object | Add-Member Noteproperty -Name Module -Value $Module.Name
      $Object | Add-Member Noteproperty -Name ManagementPackName -Value $MP.Name
      $Object | Add-Member Noteproperty -Name Accessibility -Value $Module.Accessibility

      If ([bool]$Module.RunAs){
        $RunAs = Get-ModuleRunasProfileName -Module $Module -Profiles $RAAP
      }
      $Object | Add-Member Noteproperty -Name RunAs -Value $RunAs
      $Object | Add-Member Noteproperty -Name ID -Value $Module.ID

      if($null -ne $Module.Configuration.Schema) {
        #
        $set = ($Module.Configuration.schema.split("<") | Where-Object {$_ -match "^xsd:element.*"} | `
        ForEach-Object -Process {$_.substring($_.indexof("name")).split("=")[1].split()[0]})
        $Object | Add-Member Noteproperty Schema $set
      }
      $Object | Add-Member Noteproperty -Name Managed -Value $Module.Managed

      $collection += $object
    }
  }

  Return $collection
}
#######################################################################

<#
    .Synopsis
    This function is designed to launch a SCOM Property Bag PowerShell script. It will output the DataItem to an xml file for inspection.
    .DESCRIPTION
    When authoring and troubleshooting a SCOM Property Bag PowerShell script it is helpful to be able to analyze the resulting DataItem. This script will make this process easier by outputing the DataItem(s) to an XML file. The default output file will have the same path/name as the input file (+.xml). Script must propertly use 'MOM.ScriptAPI' and output the proerty bag.
    Why is this useful? It's easy to output the DataItem to the screen but then it's difficult to read/analyze. If you open the .xml file with a fancy text editor like Notepad++ you can leverage the XMLTools plugin to format the data nicely. This is especially helpful for large property bags.
    Example :
    $api = New-Object -ComObject 'MOM.ScriptAPI'
    $bag = $api.CreatePropertyBag()
    $bag.AddValue('Message',"Some message")
    $api.Return($bag) #This will output the DataItem for analysis
 
    .EXAMPLE
    Show-SCOMPropertyBag 'C:\Program Files\WindowsPowerShell\Modules\SCOMHelper\SCOMPowerShellPropertyBagTypes.ps1'
    This will execute the example script and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml. Individual bags will be returned in a formatted table.
 
    .EXAMPLE
    Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -ShowXML
    This will execute MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml and then open the .xml file with the default application.
 
    .EXAMPLE
    Get-ChildItem -Path C:\TestScripts\*.ps1 | Show-SCOMPropertyBag -ShowXML -Format Separate
    This will execute all PowerShell scripts in the path and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml and then open the .xml files with the default application.
    Each property bag will be displayed individually to the screen.
 
    .EXAMPLE
    Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -Format Combined -Verbose
    This will execute C:\test\MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml.
    All property bag data will be displayed to the screen in one formatted table.
 
    Use -Verbose switch to display output file location.
 
    .NOTES
    Name: Show-SCOMPropertyBag
    Author: Tyson Paul
    History: 2019.04.19 - Initial release.
#>

Function Show-SCOMPropertyBag {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$true, 
  PositionalBinding=$false)]

  Param (
    # The full path to the input script (PowerShell .ps1) file.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true, 
        ValueFromRemainingArguments=$false, 
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$FilePath,

    # Powershell script file object. Typically this type of object would be obtained with Get-ChildItem.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true, 
        ValueFromRemainingArguments=$false, 
        Position=0,
    ParameterSetName='Parameter Set 2')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [System.IO.FileInfo]$File,

    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$true, 
    Position=1)]
    [ValidateSet("Combined", "Separate","HashTable")]
    #Output formated objects (bags) instead of default hash table.
    # Separate: Individual bags
    # Combined: One large table containing all data from all bags
    # HashTable: Hash table containing all bag data. Useful for spying on data values that don't otherwise display in the table format due to large size.
    [string]$Format='Separate',

        
    # This will open the .XML output file type with default application.
    [switch]$ShowXML
  )
  #region BEGIN
  Begin{
    #----------------- FUNCTIONS -----------------------
    Function FormatEach {
      Param(
        $hash 
      )

      Return $hash.Value
      
    }
    
    #--------------- END FUNCTIONS ---------------------

    $hashVariantTypes = [ordered]@{
      0 = "Empty"
      1 = "Null (unconfirmed)"
      2 = "Short (unconfirmed)"
      3 = "Integer"
      4 = "Float"
      5 = "Double"
      6 = "Currency (unconfirmed)"
      7 = "Date"
      8 = "String"
      9 = "Object (unconfirmed)"
      10 = "Error (unconfirmed)"
      11 = "Boolean"
      12 = "Variant (unconfirmed)"
      13 = "DataObject (unconfirmed)"
      14 = "Decimal"
      15 = "Byte (unconfirmed)"
      16 = "Char (unconfirmed)"
      17 = "Byte"
      18 = "Char"
      20 = "Long"
    }

    # Setup temp dir for output file. Using a redirector to an output file is the only way I could find to obtain the property bag data.
    $TempDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'Show-SCOMPropertyBag') )
    New-Item -Type Directory -Path $TempDir -ErrorAction SilentlyContinue | Out-Null
    If (-not(Test-Path -Path $TempDir -PathType Container)) {
      Write-Error "Unable to create/access directory: [$($TempDir)]. " 
      Return $false
    }
    $hash = @{}
    $int=1
  }
  #endregion BEGIN
  
  
  Process {
    $batchstamp = Get-Date -Format o | ForEach-Object {$_ -replace ":", "."}
    If ([bool]$File){
      If (Test-Path -PathType Leaf -Path $File.FullName -ErrorAction SilentlyContinue){
        $OutFile = (Join-Path $TempDir "$($File.Name)_$($batchstamp)_.xml" )
        cmd /c PowerShell.exe -file $File.FullName > $OutFile
      }
    }

    Else {
      If (-NOT [bool]$FilePath) {
        #$FilePath = 'C:\Program Files\WindowsPowerShell\Modules\SCOMHelper\1.1\SCOMPowerShellPropertyBagTypes.ps1'
        $FilePath = Join-Path $psScriptRoot SCOMPowerShellPropertyBagTypes.ps1
      }
      If (Test-Path -PathType Leaf -Path $FilePath -ErrorAction SilentlyContinue){
        $OutFile = (Join-Path $TempDir "$(Split-Path $FilePath -Leaf)_$($batchstamp)_.xml")
        cmd /c PowerShell.exe -file $FilePath > $OutFile
      }
      Else {
        Throw "Problem with FilePath: [ $($FilePath) ]."
      }
    }
    # Remove generic typename that often appears as a result of standard '$bag' statement
    $content = ((Get-Content $OutFile -Raw ).Replace('System.__ComObject','').Replace('ÿ','')) 
    $content | Set-Content $OutFile
    
    $token = (Get-Date -f fffffff)
    # Add a delimiter after each DataItem, then split on it.
    $array = (($content -replace '\<\/DataItem\>',"</DataItem>$($token)" ) -split $token)

    ForEach ($DI in $array[0..($array.Count -2)]){
      $arrayItems = @()
      'type','time','sourceHealthServiceId' | ForEach-Object {
        $obj = New-Object PSCUSTOMOBJECT

        $obj | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_
        $obj | Add-Member -MemberType NoteProperty -Name 'VariantType' -Value ''
        $obj | Add-Member -MemberType NoteProperty -Name 'Value' -Value (([xml]$DI).DataItem.$_)
        $arrayItems += $obj
      }
 
      ForEach ($Property in ([xml]$DI).DataItem.Property){
        $obj = New-Object PSCUSTOMOBJECT
        $obj | Add-Member -MemberType NoteProperty -Name "Name" -Value ([string]$Property.Name)
        $obj | Add-Member -MemberType NoteProperty -Name "VariantType" -Value ("{0,2},{1}" -F $([int]$Property.VariantType), $($hashVariantTypes.[int]$Property.VariantType))
        $obj | Add-Member -MemberType NoteProperty -Name "Value" -Value ([string]$Property.'#text')
        $arrayItems += $obj
      }
      $hash.Add($int,$arrayItems)
      $int++
    }
    
    If ($ShowXML) {
      # Open with default application
      & $OutFile
    }

    Write-Verbose "Output file: $OutFile"
  }

  End {
    If ($Format -eq 'Separate'){
      ForEach ($element in @($hash.GetEnumerator() )) { 
        FormatEach -hash $element | Out-Host
      }
    }
    
    ElseIf ($Format -eq 'Combined'){
      ForEach ($element in @($hash.GetEnumerator() )) { 
        FormatEach -hash $element 
      }
    }
    
    Else{
      Return $hash
    }
  }
} #end Function
#######################################################################

<#
    .Synopsis
    Will test any number of TCP ports on any number of hosts.
    .DESCRIPTION
    Will accept piped Computers/IPs or Ports and then test those ports on the targets for TCP connectivity.
    .Parameter Computer
    Computer NetBIOS name, FQDN, or IP
    .Parameter Port
    TCP Port number to test.
    .Parameter TimeoutMS
    The amount of time to wait (in milliseconds) before abandoning a connection attempt on a given port.
    .EXAMPLE
    PS C:\> Test-Port -Computer '8.8.8.8' -Port 53
    .EXAMPLE
    PS C:\> 443,80,53,135,137,5723 | Test-Port -Computer 'MS01.contoso.com','DB01' | Sort-Object Computer,Port
    .EXAMPLE
    PS C:\> 'MS01.contoso.com','DB01' | Test-Port -Port 5723
    .NOTES
    Author: Tyson Paul
    Blog: https://monitoringguys.com/
    Version: 1.3
    Date: 2018.02.07
    History:
    2018.05.31: Improved error handling.
 
    Adapted from Boe Prox (https://gallery.technet.microsoft.com/scriptcenter/97119ed6-6fb2-446d-98d8-32d823867131)
#>

Function Test-Port {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]
  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [string[]]$Computer,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [int[]]$Port,

    [int]$TimeoutMS=5000
  )
  Begin {
    $arrResults = @()
    $Error.Clear()
  }
  Process {
    If (-not $Port) { $Port = Read-host "Enter the port number to access" }

    ForEach ($thisComputer in $Computer) {
      ForEach ($thisPort in $Port) {
        $Status = "Failure"
        $objResult = New-Object -TypeName PSCustomObject
        $tcpobject = New-Object System.Net.Sockets.TcpClient
        #Connect to remote machine's port
        $connect = $tcpobject.BeginConnect($thisComputer,$thisPort,$null,$null)
        #Configure a timeout before quitting - time in milliseconds
        $wait = $connect.AsyncWaitHandle.WaitOne($TimeoutMS,$false)
        If (-Not $Wait) {
          $Message = "Connection Failure. Address:[$($thisComputer)], Port:[$($thisPort)] connection timed out [$($TimeoutMS) milliseconds].`n"
        }
        Else {
          Try{
            $tcpobject.EndConnect($connect)  #| out-Null
            $Status = "Success"
            $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n"
          }Catch{
            #If ([bool]$Error[0]) {
            $Message = ("{0}" -f $error[0].Exception.InnerException)
            #}
            #Else {
            # $Status = "Success"
            # $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n"
            #}
          }
        }
        $objResult | Add-Member -MemberType NoteProperty -Name Computer -Value $thisComputer
        $objResult | Add-Member -MemberType NoteProperty -Name Port -Value $thisPort
        $objResult | Add-Member -MemberType NoteProperty -Name Status -Value $Status
        $objResult | Add-Member -MemberType NoteProperty -Name Result -Value $Message
        $arrResults += $objResult
        $Error.Clear()
      }#End ForEach Port
    }#End ForEach Computer
  }#End Process

  End {
    Return $arrResults
  }

} #End Function
#######################################################################

<#
    .Synopsis
    Will unseal MP and MPB files along with any additional resources contained therein and place into version-coded folders.
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir C:\MyMPs -outDir C:\MyMPs\UnsealedMPs
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir 'C:\Program Files (x86)\System Center Management Packs' -outDir 'C:\Temp\Unsealtest_OUT'
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir 'C:\en_system_center_2012_r2_operations_manager_x86_and_x64_dvd_2920299\ManagementPacks' -outDir 'C:\Temp\Unsealtest_OUT'
    .INPUTS
    Strings (directory paths)
    .OUTPUTS
    XML files (+ any resource files: .dll, .bmp, .ico etc.)
 
    .NOTES
    Author (Revised by): Tyson Paul (I modified this heavily but the original that I used as a framework was not written by me.)
    Original Author: I can't seem to find where I originally got this script from. Jon Almquist maybe?
    Blog: https://monitoringguys.com/
    History:
    2019.05.01 - Fixed issue where output path would display extra backslash
    2018.05.25 - Initial.
    .LINK
    Get-SCOMMPFileInfo
    Get-SCOMClassInfo
#>

Function Unseal-SCOMMP {
  Param(
    $inDir="C:\Temp\Sealed",
    $outDir="C:\Temp\AllUnsealed"
  )

  [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core")
  [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Packaging")

  $mpStore = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore($inDir)
  $mps = Get-ChildItem $inDir *.mp -Recurse
  If ($null -ne $mps) {
    ForEach ($file in $mps) {
      $MPFilePath = $file.FullName
      $mp = New-Object Microsoft.EnterpriseManagement.Configuration.ManagementPack($MPFilePath)
      $newOutDirPath = (Join-Path $outDir  ($file.Name + "($($mp.Version.ToString()))") )
      New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue
      Write-Host $file
      $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter( $newOutDirPath )
      $mpWriter.WriteManagementPack($mp)
    }
  }

  #now dump MPB files
  $mps = Get-ChildItem $inDir *.mpb -Recurse
  $mpbReader = [Microsoft.EnterpriseManagement.Packaging.ManagementPackBundleFactory]::CreateBundleReader()
  If ($null -ne $mps) {
    ForEach ($mp in $mps) {
      $bundle = $mpbReader.Read($mp.FullName, $mpStore)
      $newOutDirPath = (Join-Path $outDir ($mp.Name + "($($bundle.ManagementPacks.Version.ToString()))" ))
      New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue
      Write-Host $mp
      $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter($newOutDirPath)
      ForEach($bundleItem in $bundle.ManagementPacks) {
        #write the xml
        $mpWriter.WriteManagementPack($bundleItem)
        $streams = $bundle.GetStreams($bundleItem)
        ForEach($stream in $streams.Keys) {
          $mpElement = $bundleItem.FindManagementPackElementByName($stream)
          $fileName = $mpElement.FileName
          If ($null -eq $fileName) {
            $outpath = (Join-Path $newOutDirPath ('.' + $stream + '.bin'))
          }
          Else {
            If ($fileName.IndexOf('\') -gt 0) {
              #$outpath = Split-Path $fileName -Leaf
              $outpath = (Join-Path $newOutDirPath (Split-Path $fileName -Leaf))
            }
            Else {
              $outpath = (Join-Path $newOutDirPath $fileName)
            }
          }
          Write-Host "`t$outpath"
          $fs = [system.io.file]::Create($outpath)
          $streams[$stream].WriteTo($fs)
          $fs.Close()
        }
      }
    }
  }

}#End Function
#######################################################################
<#
    .DESCRIPTION
    Will overwrite an existing instance group or computer group containing Windows Computers [Microsoft.Windows.Computer] objects and optionally include related Health Service Watchers [Microsoft.SystemCenter.HealthServiceWatcher] if the original group type is Microsoft.SystemCenter.InstanceGroup.
    WARNING: This will replace existing group members with the objects for the provided computer names. This will technically not "add" to the exiting group.
 
    .EXAMPLE
    Update-SCOMComputerGroup -ComputerName 'ms01.contoso.com','ms02.contoso.com' -MgmtServerName ms01.contoso.com -GroupId '4f04c0c7-e3c3-8ef0-a710-1414dd320c5b' -PassThru -AddHealthServiceWatchers -Verbose
 
    The above example will update the designated instance group with the two Computers and associated Health Service Watchers. It will prompt for confirmation before
    updating the existing group membership. It will return the group object.
 
    .EXAMPLE
    #
    $FQDNs = (Get-SCOMAgent | Where-Object Name -like "*ProdDB*").Name
    Update-SCOMComputerGroup -ComputerName $FQDNs -MgmtServerName ms01.contoso.com -GroupName 'SQL.Production.SQLServerComputers.Group' -PassThru -Verbose -Force
 
    The above example will update the the designated group (instance group or Computer group) with all Computer objects that match the name criteria. . It will not prompt for confirmation before
    updating the existing group membership. It will return the group object.
 
    .INPUTS
    Accepts array of computer FQDN names (fully qualified domain name)
 
    .OUTPUTS
    SCOM Management Pack [Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup]
 
    .NOTES
    Script: Update-SCOMComputerGroup
    Author: Tyson Paul (https://monitoringguys.com/2019/11/12/scomhelper/)
    Version History:
    2020.11.19.1613 - Added ability to add core references if they are missing.
    2020.08.18.1703 - initial
 
    https://docs.microsoft.com/en-us/system-center/scom/manage-create-manage-groups?view=sc-om-2019
#>

Function Update-SCOMComputerGroup {
  [CmdletBinding(SupportsShouldProcess=$false, 
      PositionalBinding=$false,
      HelpUri = 'http://www.microsoft.com/',
  ConfirmImpact='Medium')]
  [Alias()]
  [OutputType([Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup])]

  Param (

    # Name of computer to be added to the group (and associated Health Service Watcher instance if used with -AddHealthServiceWatchers parameter)
    [Parameter(Mandatory=$true, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
    ValueFromRemainingArguments=$false)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string[]]$ComputerName,

    # Name of mgmt server to use for SDK connection
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
    ValueFromRemainingArguments=$false)]
    [string]$MgmtServerName = 'LOCALHOST',
    
    # Name of the group (not DisplayName)
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 1')]
    [string]$GroupName,

    # DisplayName of the group (not Name )
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 2')]
    [string]$GroupDisplayName,

    # ID/GUID of the group
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$false,
    ParameterSetName='Parameter Set 3')]
    [string]$GroupId,

    # This will include corresponding Health Service Watcher objects in the group if the group is an 'instance group'.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
    ValueFromRemainingArguments=$false)]
    [switch]$AddHealthServiceWatchers,
    
    # This will return the new group [Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup] object.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
    ValueFromRemainingArguments=$false)]
    [switch]$PassThru,
    
    # Will not prompt for confirmation
    [switch]$Force

  )


  ########################################################################################################
  ##################################### FUNCTIONS #######################################################
  Function Get-GroupType {
    Param (
      [string]$GroupName
    )
    #Get the group class
    Write-Verbose "Getting the group '$GroupName'."
    $GroupClassCriteria = New-Object Microsoft.EnterpriseManagement.Configuration.MonitoringClassCriteria("Name='$GroupName'")
    $GroupClass = $MG.GetMonitoringClasses($GroupClassCriteria)[0]
    If ($GroupClass -eq $null)
    {
      Write-Error "$GroupName is not found."
      Return $false
    }
    Else 
    {
      #Check if this monitoring class is actually an instance group or computer group
      Write-Verbose "Check if the group '$GroupName' is an instance group or a computer group."
      $GroupBaseTypes = $GroupClass.GetBaseTypes()
      Foreach ($item in $GroupBaseTypes)
      {
        If ($item.Id.Tostring() -eq '4ce499f1-0298-83fe-7740-7a0fbc8e2449')
        {
          Write-Verbose "'$GroupName' is an instance group."
          Return 'Microsoft.SystemCenter.InstanceGroup'
        }
        If ($item.Id.Tostring() -eq '0c363342-717b-5471-3aa5-9de3df073f2a')
        {
          Write-Verbose "'$GroupName' is a computer group."
          Return 'Microsoft.SystemCenter.ComputerGroup'
        }
      }

      # Should never reach this if group is valid
      Return $false
    
    }
  }#End Function Get-GroupType
  ########################################################################################################
  <#
      Will return the MP which contains the Discovery for a group.
      Only if there exists a single discovery datasource for the group.
  #>

  Function Get-GroupDiscoveryMP {
    Param (
      [string]$GroupName
    )
  
    #Get the group class
    Write-Verbose "Getting the group '$GroupName'."
    $GroupClassCriteria = New-Object Microsoft.EnterpriseManagement.Configuration.MonitoringClassCriteria("Name='$GroupName'")
    $GroupClass = $MG.GetMonitoringClasses($GroupClassCriteria)[0]
    If ($GroupClass -eq $null)
    {
      Write-Error "$GroupName is not found."
      Return $false
    }

    #Check if this monitoring class is actually an instance group or computer group
    Write-Verbose "Check if the group '$GroupName' is an instance group or a computer group."
    $GroupBaseTypes = $GroupClass.GetBaseTypes()
    $bIsGroup = $false
    Foreach ($item in $GroupBaseTypes)
    {
      If ($item.Id.Tostring() -eq '4ce499f1-0298-83fe-7740-7a0fbc8e2449')
      {
        Write-Verbose "'$GroupName' is an instance group."
        $bIsGroup = $true
      }
      If ($item.Id.Tostring() -eq '0c363342-717b-5471-3aa5-9de3df073f2a')
      {
        Write-Verbose "'$GroupName' is a computer group."
        $bIsGroup = $true
      }
    }
    If ($bIsGroup -eq $false)
    {
      Write-Error "$GroupName is not an instance group or a computer group."
      Return $false
    }

    #Get Group object
    $GroupObject = $MG.GetMonitoringObjects($GroupClass)[0]

    $GroupDiscoveries = $GroupObject.GetMonitoringDiscoveries()
    $iGroupPopDiscoveryCount = 0
    $GroupPopDiscovery = $null
    Foreach ($Discovery in $GroupDiscoveries)
    {
      $DiscoveryDS = $Discovery.DataSource
      #Microsft.SystemCenter.GroupPopulator ID is 488000ef-e20b-1ac4-d3b1-9d679435e1d7
      If ($DiscoveryDS.TypeID.Id.ToString() -eq '488000ef-e20b-1ac4-d3b1-9d679435e1d7')
      {
        #This data source module is using Microsft.SystemCenter.GroupPopulator
        $iGroupPopDiscoveryCount = $iGroupPopDiscoveryCount + 1
        $GroupPopDiscovery = $Discovery
        Write-Verbose "Group Populator discovery found: '$($GroupPopDiscovery.Name)'"
      }
    }
    If ($iGroupPopDiscoveryCount.count -eq 0)
    {
      Write-Error "No group populator discovery found for $GroupName."
      Return $false
    }

    If ($iGroupPopDiscoveryCount.count -gt 1)
    {
      Write-Error "$GroupName has multiple discoveries using Microsft.SystemCenter.GroupPopulator Module type. Unable to continue."
      Return $false
    }

    #Get the MP of where the group populator discovery is defined
    $GroupPopDiscoveryMP = $GroupPopDiscovery.GetManagementPack()
    $GroupPopDiscoveryMPName = $GroupPopDiscoveryMP.Name
    Write-Verbose "The group populator discovery '$($GroupPopDiscovery.Name)' is defined in management pack '$GroupPopDiscoveryMPName'."

    #Write Error and exit if the group discovery MP is sealed
    Write-Verbose "Checking if '$GroupPopDiscoveryMPName' MP is sealed."
    If ($GroupPopDiscoveryMP.sealed -eq $true)
    {
      Write-Error "Unable to update the group discovery because it is defined in a sealed MP: '$($GroupPopDiscoveryMP.DisplayName)'."
      Return $false
    } else {
      Write-Verbose "'$GroupPopDiscoveryMPName' MP is unsealed. OK to continue."
    }

    Return $GroupPopDiscoveryMP
  }
  ########################################################################################################
  Function Create-ExplicitMembershipConfig {
    Param (
      [Parameter(Mandatory=$true, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false,
      ParameterSetName='Parameter Set 1')]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [array]$ComputerName,
    
      [Parameter(Mandatory=$true, 
          ValueFromPipeline=$false,
          ValueFromPipelineByPropertyName=$false, 
          ValueFromRemainingArguments=$false,
      ParameterSetName='Parameter Set 1')]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [string]$GroupName,
      
      [string]$RelationshipClass,
    
      [string]$WindowsAlias='Windows',
      [string]$SCAlias='SystemCenter',
      [string]$InstanceGroupAlias='InstanceGroupLibrary'
  
    )
  
    $WinCompClass = Get-SCOMClass -Name "Microsoft.Windows.Computer"
    $HSWClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthServiceWatcher"
    
    [System.Collections.ArrayList]$arrWinComp = @()
    [System.Collections.ArrayList]$arrHSW = @()
    
    # Find appropriate Computer/HSW objects, build collections
    ForEach ($CName in $ComputerName) {
      $WinComp = $NULL
      $Criteria = "DisplayName = '$CName'"
    
      $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$WinCompClass)
      $WinComp = $MG.GetMonitoringObjects($objCriteria)
      If (-NOT $WinComp) {
        Write-Warning "Computer: [$($CName)] not found! Verify that the fully qualified domain name exists."
        Continue
      }
      $CompID = $WinComp.Id.Guid
      Write-Verbose "Found Computer:`t[$($CName)], [$($CompID)]"
      $Null = $arrWinComp.Add(" <MonitoringObjectId>$CompID</MonitoringObjectId>`n")

      If ($AddHealthServiceWatchers) {
        $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$HSWClass)
        $HSW = $MG.GetMonitoringObjects($objCriteria)  
        $WatchID = $HSW.Id.Guid
        Write-Verbose "Found HSW:`t`t[$($CName)], [$($WatchID)]"
        $Null = $arrHSW.Add(" <MonitoringObjectId>$WatchID</MonitoringObjectId>`n")
      }
    }
 
    # Add Windows Computer GUIDs to explicit membership rule
    $Rules += @"
  <MembershipRule>
  <MonitoringClass>`$MPElement[Name="$($WindowsAlias)!Microsoft.Windows.Server.Computer"]`$</MonitoringClass>
  $($RelationshipClass)
  <IncludeList>
  $arrWinComp
  </IncludeList>
  </MembershipRule>
"@
 
 
    # If appropriate, add HSW rules to membership rules
    If ($arrHSW.Count) {
      $Rules += @"
 
  <MembershipRule>
  <MonitoringClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.HealthServiceWatcher"]`$</MonitoringClass>
  $($RelationshipClass)
  <IncludeList>
  $arrHSW
  </IncludeList>
  </MembershipRule>
"@
    
    }
 
    # Assemble the new configuration
    $NewConfiguration = @"
    <RuleId>`$MPElement`$</RuleId>
    <GroupInstanceId>`$MPElement[Name="$($GroupName)"]`$</GroupInstanceId>
    <MembershipRules>
    $($Rules)
    </MembershipRules>
"@

    Return $NewConfiguration
  

  }#End Funtion Create-ExplicitMembershipConfig

  ########################################################################################################
  

  Write-Verbose "Attempt import of OperationsManager module"
  . Import-SCOMPowerShellModule

  Try {
    Write-Verbose "Verify mgmt group connection..."
    If ($MgmtServerName) {
      $MG = Connect-OMManagementGroup -SDK $MgmtServerName -Verbose:$VerbosePreference
    }
    ElseIf (-NOT $MG) {
      $MG = Connect-OMManagementGroup -SDK 'Localhost' -Verbose:$VerbosePreference
    }
  } Catch {
    Write-Error "Failed to connect to mgmt group. $_"
    Exit
  }
  
  [string]$WindowsAlias='Windows'
  [string]$SCAlias='SystemCenter'
  [string]$InstanceGroupAlias='InstanceGroupLibrary'

  $DateStamp = (Get-Date -f "yyyyMMdd.hhmmss")
  $BackupDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' "Update-SCOMComputerGroup_$($DateStamp)" ) )
  
  If ($GroupId){
    $objGroup = (Get-SCOMGroup -Id $GroupId)
  }
  If ($GroupName) {
    $Class = Get-SCOMClass -Name $GroupName
    $objGroup = (Get-SCOMGroup -DisplayName ($Class.DisplayName))
  }
  If ($GroupDisplayName) {
    $objGroup = (Get-SCOMGroup -DisplayName $GroupDisplayName)
  }
  $GroupName = $objGroup.FullName
  $GroupPopDiscoveryMP = Get-GroupDiscoveryMP -GroupName $GroupName

  If (-NOT $GroupPopDiscoveryMP) {
    Write-Error "Unable to retrieve management pack for group [$GroupName]. Exiting."
    Return
  }
  Try {
    New-Item -Path $BackupDir -ItemType Directory -Verbose -Force
    $GroupPopDiscoveryMP | Export-SCOMManagementPack -Path $BackupDir -Verbose -ErrorAction Stop
    Write-Verbose "Backup Directory: [$($BackupDir)]"
  } Catch {
    Throw "Unable to backup existing MP to path: [$($BackupDir)]. No action taken. Exiting. Error:`n$_"
    Return 
  }

  <#
      When defining the new discovery configuration we will need to use a specific alias depending on the group type.
      Aliases tend to be unique in unsealed MPs. Get unique aliases from existing group MP so they can be used in the new/updated member Configuration rules.
      This approach is a little sloppy and I may update it someday if I expand the group population automation abilities.
  #>

  #ForEach ($Key in @($GroupPopDiscoveryMP.References.Keys)) {
    # Locate "Windows" alias
    If ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.Windows.Library$') {
      $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.Windows.Library$'} ).Key
      $WindowsAlias = $thisAlias
      #Continue;
    }
    #Add the reference if it does not exist
    Else {
      Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $WindowsAlias
    }

    # Locate "SystemCenter" alias
    If ( ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.SystemCenter.Library$' )) {
      $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.SystemCenter.Library$'} ).Key
      $SCAlias = $thisAlias
      #Continue;
    }
    #Add the reference if it does not exist
    Else {
      Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $SCAlias
    }
     
    # Locate "InstanceGroup" alias
    If ( ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.SystemCenter.InstanceGroup.Library$' )) {
      $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.SystemCenter.InstanceGroup.Library$'} ).Key
      $InstanceGroupAlias = $thisAlias
      #Continue;
    }
    #Add the reference if it does not exist
    Else {
      Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $InstanceGroupAlias
    }

  #}
  
  
  # Determine what type of group this is: InstanceGroup or ComputerGroup
  $GroupClassType = Get-GroupType -GroupName $GroupName
  
  # Construct the appropriate RelationshipClass string. This must agree with the exiting discovery/group type
  Switch ($GroupClassType){
    'Microsoft.SystemCenter.InstanceGroup' {
      $RelationshipClass = @"
<RelationshipClass>`$MPElement[Name="$($InstanceGroupAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass>
"@

    }

    'Microsoft.SystemCenter.ComputerGroup' {
      If ($AddHealthServiceWatchers) {
        Write-Error "Existing group type is [$($GroupClassType)]. Cannot add Health Service Watchers to exiting group. No Action taken. (Must be 'Microsoft.SstemCenter.InstanceGroup' to add other instance types. Exiting.)"
        Return
      }
      $RelationshipClass = @"
<RelationshipClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.ComputerGroupContainsComputer"]`$</RelationshipClass>
"@

    }
    
    default {
      Write-Error "Unknown base class type [$($GroupClassType)] for group [$($GroupName)]. Exiting."
      Return
    }
  }
  
  Write-Verbose "Building new configuration..."
  $NewConfiguration = Create-ExplicitMembershipConfig -ComputerName $ComputerName -GroupName $GroupName -RelationshipClass $RelationshipClass -WindowsAlias $WindowsAlias -SCAlias $SCAlias -InstanceGroupAlias $InstanceGroupAlias
  Write-Verbose "New Configuration:`n$($NewConfiguration)"
  $Proceed = $false
  $r = ''
  If (-NOT $Force) {
    While ($r -notmatch 'y|n') {
      Write-Warning "Warning: This will overwrite existing group."
      $r = Read-Host 'Proceed? (Y/N)'
    }
    If ($r -match 'y') {
      $Proceed = $true
    }
    Else {
      Write-Warning "No action taken. Exiting."
      Return
    }
  }
  
  If ($Force -OR $Proceed) {
    $result = Update-OMGroupDiscovery2 -SDK $MgmtServerName -GroupName $GroupName -NewConfiguration $NewConfiguration -Verbose:$VerbosePreference
    Write-Verbose "Group Update succesful?: $result"
  }
  # Finally, get the new pack
  If ($PassThru) {
    Get-SCOMGroup -Id ($objGroup.Id) -Verbose:$VerbosePreference
  }


}#End Function Update-SCOMComputerGroup

#######################################################################



$TempFolder = (Join-Path (Join-Path $env:Windir "Temp") SCOMHelper )
If (-NOT (Test-Path -Path $TempFolder -PathType Container)){
  Write-Host "Attempting to create temp folder: $TempFolder" -F DarkCyan
  Try{
    New-Item -ItemType Directory -Path $TempFolder -Force -ErrorAction Stop
  }Catch{
    Write-Host "Failed to create temp directory. Please run this tool as administrator." -F Red
  }
}

. $PSScriptRoot\Start-SCOMOverrideTool.ps1
New-Alias -Name 'Export-EffectiveMonitoringConfiguration' -Value 'Export-SCOMEffectiveMonitoringConfigurationReport' -ErrorAction Ignore
New-Alias -Name 'Disable-SCOMAllEventRules' -Value 'Start-SCOMOverrideTool' -ErrorAction Ignore
Export-ModuleMember -Alias *

$Functions = @'
Clear-SCOMCache
Compare-String
Deploy-SCOMAgent
Disable-SCOMAllEventRules
Export-SCOMEffectiveMonitoringConfigurationReport
Export-SCOMEventsToCSV
Export-SCOMKnowledge
Export-SCOMOverrides
Fast-Ping
Get-SCOMAlertKnowledge
Get-SCOMClassInfo
Get-SCOMHealthCheckOpsConfig
Get-SCOMMPFileInfo
Get-SCOMRunAsAccountName
Get-SCOMRunAsProfilesAccounts
Get-SCOMRunningWorkflows
Get-StringHash
New-SCOMClassGraph
New-SCOMComputerGroup
Ping-AllHosts
Remove-SCOMObsoleteReferenceFromMPFile
Set-SCOMMPAliases
Show-SCOMModules
Show-SCOMPropertyBag
Start-SCOMOverrideTool
Test-Port
Unseal-SCOMMP
Update-SCOMComputerGroup
'@
 -split  "\n" -replace "`r","" | Export-ModuleMember