PSFramework.psm1

$script:ModuleRoot = $PSScriptRoot

if ($PSFramework_DebugMode) { [PSFramework.PSFCore.PSFCoreHost]::DebugMode = $true }
if ($PSFramework_DebugVerbose) { [PSFramework.PSFCore.PSFCoreHost]::VerboseDebug = $true }

[PSFramework.PSFCore.PSFCoreHost]::WriteDebug("Starting Import","")

if (([runspace]::DefaultRunspace.InitialSessionState.LanguageMode -eq 'NoLanguage') -or ($PSVersionTable.PSVersion.Major -lt 5))
{
    # This is considered safe, as you should not be using unsafe localization resources in a constrained endpoint
    $script:ModuleVersion = (Invoke-Expression (Get-Content -Path "$($script:ModuleRoot)\PSFramework.psd1" -Raw)).ModuleVersion
}
else
{
    $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSFramework.psd1").ModuleVersion
}

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = $false
if ($psframework_dotsourcemodule) { $script:doDotSource = $true }
if (($PSVersionTable.PSVersion.Major -lt 6) -or ($PSVersionTable.OS -like "*Windows*"))
{
    if ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "DoDotSource" -ErrorAction Ignore).DoDotSource) { $script:doDotSource = $true }
    if ((Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "DoDotSource" -ErrorAction Ignore).DoDotSource) { $script:doDotSource = $true }
}

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = $false
if ($PSFramework_importIndividualFiles) { $importIndividualFiles = $true }
if (($PSVersionTable.PSVersion.Major -lt 6) -or ($PSVersionTable.OS -like "*Windows*"))
{
    if ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "ImportIndividualFiles" -ErrorAction Ignore).ImportIndividualFiles) { $script:doDotSource = $true }
    if ((Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "ImportIndividualFiles" -ErrorAction Ignore).ImportIndividualFiles) { $script:doDotSource = $true }
}
if (Test-Path (Join-Path (Resolve-Path -Path "$($script:ModuleRoot)\..") '.git')) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

[PSFramework.PSFCore.PSFCoreHost]::WriteDebug("Finished Pre-Import Config", "DotSource: $script:doDotSource | Individual Files: $importIndividualFiles")

function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    try
    {
        if ($doDotSource) { . (Resolve-Path $Path) }
        else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path).ProviderPath))), $null, $null) }
    }
    catch { throw (New-Object System.Exception("Failed to import $(Resolve-Path $Path) : $_", $_.Exception)) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    [PSFramework.PSFCore.PSFCoreHost]::WriteDebug("PreImport", "")
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        [PSFramework.PSFCore.PSFCoreHost]::WriteDebug(" $path", "")
        . Import-ModuleFile -Path $path
    }
    
    # Import all internal functions
    [PSFramework.PSFCore.PSFCoreHost]::WriteDebug("InternalFunctions", "")
    foreach ($function in (Get-ChildItem "$($script:ModuleRoot)\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    [PSFramework.PSFCore.PSFCoreHost]::WriteDebug("Functions", "")
    foreach ($function in (Get-ChildItem "$($script:ModuleRoot)\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    [PSFramework.PSFCore.PSFCoreHost]::WriteDebug("PostImport", "")
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        [PSFramework.PSFCore.PSFCoreHost]::WriteDebug(" $path", "")
        . Import-ModuleFile -Path $path
    }
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
#region Paths
$script:path_RegistryUserDefault = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default"
$script:path_RegistryUserEnforced = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced"
$script:path_RegistryMachineDefault = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default"
$script:path_RegistryMachineEnforced = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced"
$psVersionName = "WindowsPowerShell"
if ($PSVersionTable.PSVersion.Major -ge 6) { $psVersionName = "PowerShell" }

#region User Local
if ($IsLinux -or $IsMacOs)
{
    # Defaults to $Env:XDG_CONFIG_HOME on Linux or MacOS ($HOME/.config/)
    $script:path_LocalAppData = $Env:XDG_CONFIG_HOME
    if (-not $script:path_LocalAppData) { $script:path_LocalAppData = Join-Path $HOME .config/ }
    
    $script:path_FileUserLocal = Join-Path (Join-Path $script:path_LocalAppData $psVersionName) "PSFramework/"
}
else
{
    # Defaults to $Env:LocalAppData on Windows
    $script:path_FileUserLocal = Join-Path $Env:LocalAppData "$psVersionName\PSFramework\Config"
    $script:path_LocalAppData = $Env:LocalAppData
    if (-not $script:path_FileUserLocal)
    {
        $script:path_FileUserLocal = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) "$psVersionName\PSFramework\Config"
        $script:path_LocalAppData = [Environment]::GetFolderPath("LocalApplicationData")
    }
}
#endregion User Local

#region User Shared
if ($IsLinux -or $IsMacOs)
{
    # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/)
    $script:path_AppData = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0]
    if (-not $script:path_AppData) { $script:path_AppData = Join-Path $HOME .local/share/ }
    
    $script:path_FileUserShared = Join-Path (Join-Path $script:path_AppData $psVersionName) "PSFramework/"
}
else
{
    # Defaults to $Env:AppData on Windows
    $script:path_FileUserShared = Join-Path $Env:AppData "$psVersionName\PSFramework\Config"
    $script:path_AppData = $env:APPDATA
    if (-not $Env:AppData)
    {
        $script:path_AppData = [Environment]::GetFolderPath("ApplicationData")
        $script:path_FileUserShared = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Config"
    }
}
#endregion User Shared

#region System
if ($IsLinux -or $IsMacOs)
{
    # Defaults to /etc/xdg elsewhere
    $XdgConfigDirs = $Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator) | Where-Object { $_ -and (Test-Path $_) }
    if ($XdgConfigDirs.Count -gt 1) { $script:path_ProgramData = $XdgConfigDirs[1] }
    else { $script:path_ProgramData = "/etc/xdg/" }
    $script:path_FileSystem = Join-Path $script:path_ProgramData "$psVersionName/PSFramework/"
}
else
{
    # Defaults to $Env:ProgramData on Windows
    $script:path_FileSystem = Join-Path $Env:ProgramData "$psVersionName\PSFramework\Config"
    $script:path_ProgramData = $env:ProgramData
    if (-not $script:path_FileSystem)
    {
        $script:path_ProgramData = [Environment]::GetFolderPath("CommonApplicationData")
        $script:path_FileSystem = Join-Path ([Environment]::GetFolderPath("CommonApplicationData")) "$psVersionName\PSFramework\Config"
    }
}
#endregion System

#region Special Paths
if ($IsLinux -or $IsMacOs)
{
    $script:path_Logging = Join-Path (Split-Path $script:path_FileUserShared) "Logs/"
    $script:path_typedata = Join-Path $script:path_FileUserShared "TypeData/"
}
else
{
    # Defaults to $Env:AppData on Windows
    $script:path_Logging = Join-Path $Env:AppData "$psVersionName\PSFramework\Logs"
    $script:path_typedata = Join-Path $Env:AppData "$psVersionName\PSFramework\TypeData"
    if (-not $Env:AppData)
    {
        $script:path_Logging = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Logs"
        $script:path_typedata = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\TypeData"
    }
}

#endregion Special Paths
#endregion Paths

# Determine Registry Availability
$script:NoRegistry = $false
if (($PSVersionTable.PSVersion.Major -ge 6) -and ($PSVersionTable.OS -notlike "*Windows*"))
{
    $script:NoRegistry = $true
}

if (-not ([PSFramework.Message.LogHost]::LoggingPath)) { [PSFramework.Message.LogHost]::LoggingPath = $script:path_Logging }

[PSFramework.PSFCore.PSFCoreHost]::ModuleRoot = $script:ModuleRoot
# Run the library initialization logic
# Needed before the configuration system loads
[PSFramework.PSFCore.PSFCoreHost]::Initialize()

if (($PSVersionTable.PSVersion.Major -lt 5) -and -not (Get-Module TabExpansionPlusPlus))
{
<#
Copyright (c) 2013, Jason Shirk
All rights reserved.
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 
1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#>

    #region Exported utility functions for completers
    
    #############################################################################
    #
    # Helper function to create a new completion results
    #
    function global:New-CompletionResult
    {
        param ([Parameter(Position = 0, ValueFromPipelineByPropertyName, Mandatory, ValueFromPipeline)]
            [ValidateNotNullOrEmpty()]
            [string]
            $CompletionText,
            
            [Parameter(Position = 1, ValueFromPipelineByPropertyName)]
            [string]
            $ToolTip,
            
            [Parameter(Position = 2, ValueFromPipelineByPropertyName)]
            [string]
            $ListItemText,
            
            [System.Management.Automation.CompletionResultType]
            $CompletionResultType = [System.Management.Automation.CompletionResultType]::ParameterValue,
            
            [Parameter(Mandatory = $false)]
            [switch]
            $NoQuotes = $false
        )
        
        process
        {
            $toolTipToUse = if ($ToolTip -eq '') { $CompletionText }
            else { $ToolTip }
            $listItemToUse = if ($ListItemText -eq '') { $CompletionText }
            else { $ListItemText }
            
            # If the caller explicitly requests that quotes
            # not be included, via the -NoQuotes parameter,
            # then skip adding quotes.
            
            if ($CompletionResultType -eq [System.Management.Automation.CompletionResultType]::ParameterValue -and -not $NoQuotes)
            {
                # Add single quotes for the caller in case they are needed.
                # We use the parser to robustly determine how it will treat
                # the argument. If we end up with too many tokens, or if
                # the parser found something expandable in the results, we
                # know quotes are needed.
                
                $tokens = $null
                $null = [System.Management.Automation.Language.Parser]::ParseInput("echo $CompletionText", [ref]$tokens, [ref]$null)
                if ($tokens.Length -ne 3 -or
                    ($tokens[1] -is [System.Management.Automation.Language.StringExpandableToken] -and
                        $tokens[1].Kind -eq [System.Management.Automation.Language.TokenKind]::Generic))
                {
                    $CompletionText = "'$CompletionText'"
                }
            }
            return New-Object System.Management.Automation.CompletionResult `
            ($CompletionText, $listItemToUse, $CompletionResultType, $toolTipToUse.Trim())
        }
        
    }
    
    #############################################################################
    #
    # .SYNOPSIS
    #
    # This is a simple wrapper of Get-Command gets commands with a given
    # parameter ignoring commands that use the parameter name as an alias.
    #
    function global:Get-CommandWithParameter
    {
        [CmdletBinding(DefaultParameterSetName = 'AllCommandSet')]
        param (
            [Parameter(ParameterSetName = 'AllCommandSet', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
            [ValidateNotNullOrEmpty()]
            [string[]]
            ${Name},
            
            [Parameter(ParameterSetName = 'CmdletSet', ValueFromPipelineByPropertyName)]
            [string[]]
            ${Verb},
            
            [Parameter(ParameterSetName = 'CmdletSet', ValueFromPipelineByPropertyName)]
            [string[]]
            ${Noun},
            
            [Parameter(ValueFromPipelineByPropertyName)]
            [string[]]
            ${Module},
            
            [ValidateNotNullOrEmpty()]
            [Parameter(Mandatory)]
            [string]
            ${ParameterName})
        
        begin
        {
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Command', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters | Where-Object { $_.Parameters[$ParameterName] -ne $null } }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        process
        {
            $steppablePipeline.Process($_)
        }
        end
        {
            $steppablePipeline.End()
        }
    }
    
    #############################################################################
    #
    function global:Set-CompletionPrivateData
    {
        param (
            [ValidateNotNullOrEmpty()]
            [string]
            $Key,
            
            [object]
            $Value,
            
            [ValidateNotNullOrEmpty()]
            [int]
            $ExpirationSeconds = 604800
        )
        
        $Cache = [PSCustomObject]@{
            Value           = $Value
            ExpirationTime = (Get-Date).AddSeconds($ExpirationSeconds)
        }
        $completionPrivateData[$key] = $Cache
    }
    
    #############################################################################
    #
    function global:Get-CompletionPrivateData
    {
        param (
            [ValidateNotNullOrEmpty()]
            [string]
            $Key)
        
        if (!$Key)
        { return $completionPrivateData }
        
        $cacheValue = $completionPrivateData[$key]
        if ((Get-Date) -lt $cacheValue.ExpirationTime)
        {
            return $cacheValue.Value
        }
    }
    
    #############################################################################
    #
    function global:Get-CompletionWithExtension
    {
        param ([string]
            $lastWord,
            
            [string[]]
            $extensions)
        
        [System.Management.Automation.CompletionCompleters]::CompleteFilename($lastWord) |
        Where-Object {
            # Use ListItemText because it won't be quoted, CompletionText might be
            [System.IO.Path]::GetExtension($_.ListItemText) -in $extensions
        }
    }
    
    #############################################################################
    #
    function global:New-CommandTree
    {
        [CmdletBinding(DefaultParameterSetName = 'Default')]
        param (
            [Parameter(Position = 0, Mandatory, ParameterSetName = 'Default')]
            [Parameter(Position = 0, Mandatory, ParameterSetName = 'Argument')]
            [ValidateNotNullOrEmpty()]
            [string]
            $Completion,
            
            [Parameter(Position = 1, Mandatory, ParameterSetName = 'Default')]
            [Parameter(Position = 1, Mandatory, ParameterSetName = 'Argument')]
            [string]
            $Tooltip,
            
            [Parameter(ParameterSetName = 'Argument')]
            [switch]
            $Argument,
            
            [Parameter(Position = 2, ParameterSetName = 'Default')]
            [Parameter(Position = 1, ParameterSetName = 'ScriptBlockSet')]
            [scriptblock]
            $SubCommands,
            
            [Parameter(Position = 0, Mandatory, ParameterSetName = 'ScriptBlockSet')]
            [scriptblock]
            $CompletionGenerator
        )
        
        $actualSubCommands = $null
        if ($null -ne $SubCommands)
        {
            $actualSubCommands = [NativeCommandTreeNode[]](& $SubCommands)
        }
        
        switch ($PSCmdlet.ParameterSetName)
        {
            'Default' {
                New-Object NativeCommandTreeNode $Completion, $Tooltip, $actualSubCommands
                break
            }
            'Argument' {
                New-Object NativeCommandTreeNode $Completion, $Tooltip, $true
            }
            'ScriptBlockSet' {
                New-Object NativeCommandTreeNode $CompletionGenerator, $actualSubCommands
                break
            }
        }
    }
    
    #############################################################################
    #
    function global:Get-CommandTreeCompletion
    {
        param ($wordToComplete,
            
            $commandAst,
            
            [NativeCommandTreeNode[]]
            $CommandTree)
        
        $commandElements = $commandAst.CommandElements
        
        # Skip the first command element - it's the command name
        # Iterate through the remaining elements, stopping early
        # if we find the element that matches $wordToComplete.
        for ($i = 1; $i -lt $commandElements.Count; $i++)
        {
            if (!($commandElements[$i] -is [System.Management.Automation.Language.StringConstantExpressionAst]))
            {
                # Ignore arguments that are expressions. In some rare cases this
                # could cause strange completions because the context is incorrect, e.g.:
                # $c = 'advfirewall'
                # netsh $c firewall
                # Here we would be in advfirewall firewall context, but we'd complete as
                # though we were in firewall context.
                continue
            }
            
            if ($commandElements[$i].Value -eq $wordToComplete)
            {
                $CommandTree = $CommandTree |
                Where-Object { $_.Command -like "$wordToComplete*" -or $_.CompletionGenerator -ne $null }
                break
            }
            
            foreach ($subCommand in $CommandTree)
            {
                if ($subCommand.Command -eq $commandElements[$i].Value)
                {
                    if (!$subCommand.Argument)
                    {
                        $CommandTree = $subCommand.SubCommands
                    }
                    break
                }
            }
        }
        
        if ($null -ne $CommandTree)
        {
            $CommandTree | ForEach-Object {
                if ($_.Command)
                {
                    $toolTip = if ($_.Tooltip) { $_.Tooltip }
                    else { $_.Command }
                    New-CompletionResult -CompletionText $_.Command -ToolTip $toolTip
                }
                else
                {
                    & $_.CompletionGenerator $wordToComplete $commandAst
                }
            }
        }
    }
    
    #endregion Exported utility functions for completers
    
    #region Exported functions
    
    #############################################################################
    #
    # .SYNOPSIS
    # Register a ScriptBlock to perform argument completion for a
    # given command or parameter.
    #
    # .DESCRIPTION
    # Argument completion can be extended without needing to do any
    # parsing in many cases. By registering a handler for specific
    # commands and/or parameters, PowerShell will call the handler
    # when appropriate.
    #
    # There are 2 kinds of extensions - native and PowerShell. Native
    # refers to commands external to PowerShell, e.g. net.exe. PowerShell
    # completion covers any functions, scripts, or cmdlets where PowerShell
    # can determine the correct parameter being completed.
    #
    # When registering a native handler, you must specify the CommandName
    # parameter. The CommandName is typically specified without any path
    # or extension. If specifying a path and/or an extension, completion
    # will only work when the command is specified that way when requesting
    # completion.
    #
    # When registering a PowerShell handler, you must specify the
    # ParameterName parameter. The CommandName is optional - PowerShell will
    # first try to find a handler based on the command and parameter, but
    # if none is found, then it will try just the parameter name. This way,
    # you could specify a handler for all commands that have a specific
    # parameter.
    #
    # A handler needs to return instances of
    # System.Management.Automation.CompletionResult.
    #
    # A native handler is passed 2 parameters:
    #
    # param($wordToComplete, $commandAst)
    #
    # $wordToComplete - The argument being completed, possibly an empty string
    # $commandAst - The ast of the command being completed.
    #
    # A PowerShell handler is passed 5 parameters:
    #
    # param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    #
    # $commandName - The command name
    # $parameterName - The parameter name
    # $wordToComplete - The argument being completed, possibly an empty string
    # $commandAst - The parsed representation of the command being completed.
    # $fakeBoundParameter - Like $PSBoundParameters, contains values for some of the parameters.
    # Certain values are not included, this does not mean a parameter was
    # not specified, just that getting the value could have had unintended
    # side effects, so no value was computed.
    #
    # .PARAMETER ParameterName
    # The name of the parameter that the Completion parameter supports.
    # This parameter is not supported for native completion and is
    # mandatory for script completion.
    #
    # .PARAMETER CommandName
    # The name of the command that the Completion parameter supports.
    # This parameter is mandatory for native completion and is optional
    # for script completion.
    #
    # .PARAMETER Completion
    # A ScriptBlock that returns instances of CompletionResult. For
    # native completion, the script block parameters are
    #
    # param($wordToComplete, $commandAst)
    #
    # For script completion, the parameters are:
    #
    # param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    #
    # .PARAMETER Description
    # A description of how the completion can be used.
    #
    function global:Register-ArgumentCompleter
    {
        [CmdletBinding(DefaultParameterSetName = "PowerShellSet")]
        param (
            [Parameter(ParameterSetName = "NativeSet", Mandatory)]
            [Parameter(ParameterSetName = "PowerShellSet")]
            [string[]]
            $CommandName = "",
            
            [Parameter(ParameterSetName = "PowerShellSet", Mandatory)]
            [string]
            $ParameterName = "",
            
            [Parameter(Mandatory)]
            [scriptblock]
            $ScriptBlock,
            
            [string]
            $Description,
            
            [Parameter(ParameterSetName = "NativeSet")]
            [switch]
            $Native)
        
        $fnDefn = $ScriptBlock.Ast -as [System.Management.Automation.Language.FunctionDefinitionAst]
        if (!$Description)
        {
            # See if the script block is really a function, if so, use the function name.
            $Description = if ($fnDefn -ne $null) { $fnDefn.Name }
            else { "" }
        }
        
        if ($MyInvocation.ScriptName -ne (& { $MyInvocation.ScriptName }))
        {
            # Make an unbound copy of the script block so it has access to TabExpansionPlusPlus when invoked.
            # We can skip this step if we created the script block (Register-ArgumentCompleter was
            # called internally).
            if ($fnDefn -ne $null)
            {
                $ScriptBlock = $ScriptBlock.Ast.Body.GetScriptBlock() # Don't reparse, just get a new ScriptBlock.
            }
            else
            {
                $ScriptBlock = $ScriptBlock.Ast.GetScriptBlock() # Don't reparse, just get a new ScriptBlock.
            }
        }
        
        foreach ($command in $CommandName)
        {
            if ($command -and $ParameterName)
            {
                $command += ":"
            }
            
            $key = if ($Native) { 'NativeArgumentCompleters' }
            else { 'CustomArgumentCompleters' }
            $tabExpansionOptions[$key]["${command}${ParameterName}"] = $ScriptBlock
            
            $tabExpansionDescriptions["${command}${ParameterName}$Native"] = $Description
        }
    }
    
    #############################################################################
    #
    # .SYNOPSIS
    # Tests the registered argument completer
    #
    # .DESCRIPTION
    # Invokes the registered parameteter completer for a specified command to make it easier to test
    # a completer
    #
    # .EXAMPLE
    # Test-ArgumentCompleter -CommandName Get-Verb -ParameterName Verb -WordToComplete Sta
    #
    # Test what would be completed if Get-Verb -Verb Sta<Tab> was typed at the prompt
    #
    # .EXAMPLE
    # Test-ArgumentCompleter -NativeCommand Robocopy -WordToComplete /
    #
    # Test what would be completed if Robocopy /<Tab> was typed at the prompt
    #
    function global:Test-ArgumentCompleter
    {
        [CmdletBinding(DefaultParametersetName = 'PS')]
        param
        (
            [Parameter(Mandatory, Position = 1, ParameterSetName = 'PS')]
            [string]
            $CommandName
             ,
            
            [Parameter(Mandatory, Position = 2, ParameterSetName = 'PS')]
            [string]
            $ParameterName
             ,
            
            [Parameter(ParameterSetName = 'PS')]
            [System.Management.Automation.Language.CommandAst]
            $commandAst
             ,
            
            [Parameter(ParameterSetName = 'PS')]
            [Hashtable]
            $FakeBoundParameters = @{ }
             ,
            
            [Parameter(Mandatory, Position = 1, ParameterSetName = 'NativeCommand')]
            [string]
            $NativeCommand
             ,
            
            [Parameter(Position = 2, ParameterSetName = 'NativeCommand')]
            [Parameter(Position = 3, ParameterSetName = 'PS')]
            [string]
            $WordToComplete = ''
            
        )
        
        if ($PSCmdlet.ParameterSetName -eq 'NativeCommand')
        {
            $Tokens = $null
            $Errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($NativeCommand, [ref]$Tokens, [ref]$Errors)
            $commandAst = $ast.EndBlock.Statements[0].PipelineElements[0]
            $command = $commandAst.GetCommandName()
            $completer = $tabExpansionOptions.NativeArgumentCompleters[$command]
            if (-not $Completer)
            {
                throw "No argument completer registered for command '$Command' (from $NativeCommand)"
            }
            & $completer $WordToComplete $commandAst
        }
        else
        {
            $completer = $tabExpansionOptions.CustomArgumentCompleters["${CommandName}:$ParameterName"]
            if (-not $Completer)
            {
                throw "No argument completer registered for '${CommandName}:$ParameterName'"
            }
            & $completer $CommandName $ParameterName $WordToComplete $commandAst $FakeBoundParameters
        }
    }
    
    #############################################################################
    #
    # .SYNOPSIS
    # Retrieves a list of argument completers that have been loaded into the
    # PowerShell session.
    #
    # .PARAMETER Name
    # The name of the argument complete to retrieve. This parameter supports
    # wildcards (asterisk).
    #
    # .EXAMPLE
    # Get-ArgumentCompleter -Name *Azure*;
    function global:Get-ArgumentCompleter
    {
        [CmdletBinding()]
        param ([string[]]
            $Name = '*')
        
        if (!$updatedTypeData)
        {
            # Define the default display properties for the objects returned by Get-ArgumentCompleter
            [string[]]$properties = "Command", "Parameter"
            Update-TypeData -TypeName 'TabExpansionPlusPlus.ArgumentCompleter' -DefaultDisplayPropertySet $properties -Force
            $updatedTypeData = $true
        }
        
        function WriteCompleters
        {
            function WriteCompleter($command, $parameter, $native, $scriptblock)
            {
                foreach ($n in $Name)
                {
                    if ($command -like $n)
                    {
                        $c = $command
                        if ($command -and $parameter) { $c += ':' }
                        $description = $tabExpansionDescriptions["${c}${parameter}${native}"]
                        $completer = [pscustomobject]@{
                            Command                                                                 = $command
                            Parameter                                                             = $parameter
                            Native                                                                 = $native
                            Description                                                             = $description
                            ScriptBlock                                                             = $scriptblock
                            File                                                                 = if ($scriptblock.File) { Split-Path -Leaf -Path $scriptblock.File }
                        }
                        
                        $completer.PSTypeNames.Add('TabExpansionPlusPlus.ArgumentCompleter')
                        Write-Output $completer
                        
                        break
                    }
                }
            }
            
            foreach ($pair in $tabExpansionOptions.CustomArgumentCompleters.GetEnumerator())
            {
                if ($pair.Key -match '^(.*):(.*)$')
                {
                    $command = $matches[1]
                    $parameter = $matches[2]
                }
                else
                {
                    $parameter = $pair.Key
                    $command = ""
                }
                
                WriteCompleter $command $parameter $false $pair.Value
            }
            
            foreach ($pair in $tabExpansionOptions.NativeArgumentCompleters.GetEnumerator())
            {
                WriteCompleter $pair.Key '' $true $pair.Value
            }
        }
        
        WriteCompleters | Sort-Object -Property Native, Command, Parameter
    }
    
    #############################################################################
    #
    # .SYNOPSIS
    # Register a ScriptBlock to perform argument completion for a
    # given command or parameter.
    #
    # .DESCRIPTION
    #
    # .PARAMETER Option
    #
    # The name of the option.
    #
    # .PARAMETER Value
    #
    # The value to set for Option. Typically this will be $true.
    #
    function global:Set-TabExpansionOption
    {
        param (
            [ValidateSet('ExcludeHiddenFiles',
                         'RelativePaths',
                         'LiteralPaths',
                         'IgnoreHiddenShares',
                         'AppendBackslash')]
            [string]
            $Option,
            
            [object]
            $Value = $true)
        
        $tabExpansionOptions[$option] = $value
    }
    
    #endregion Exported functions
    
    #region Internal utility functions
    
    #############################################################################
    #
    # This function checks if an attribute argument's name can be completed.
    # For example:
    # [Parameter(<TAB>
    # [Parameter(Po<TAB>
    # [CmdletBinding(DefaultPa<TAB>
    #
    function TryAttributeArgumentCompletion
    {
        param (
            [System.Management.Automation.Language.Ast]
            $ast,
            
            [int]
            $offset
        )
        
        $results = @()
        $matchIndex = -1
        
        try
        {
            # We want to find any NamedAttributeArgumentAst objects where the Ast extent includes $offset
            $offsetInExtentPredicate = {
                param ($ast)
                return $offset -gt $ast.Extent.StartOffset -and
                $offset -le $ast.Extent.EndOffset
            }
            $asts = $ast.FindAll($offsetInExtentPredicate, $true)
            
            $attributeType = $null
            $attributeArgumentName = ""
            $replacementIndex = $offset
            $replacementLength = 0
            
            $attributeArg = $asts | Where-Object { $_ -is [System.Management.Automation.Language.NamedAttributeArgumentAst] } | Select-Object -First 1
            if ($null -ne $attributeArg)
            {
                $attributeAst = [System.Management.Automation.Language.AttributeAst]$attributeArg.Parent
                $attributeType = $attributeAst.TypeName.GetReflectionAttributeType()
                $attributeArgumentName = $attributeArg.ArgumentName
                $replacementIndex = $attributeArg.Extent.StartOffset
                $replacementLength = $attributeArg.ArgumentName.Length
            }
            else
            {
                $attributeAst = $asts | Where-Object { $_ -is [System.Management.Automation.Language.AttributeAst] } | Select-Object -First 1
                if ($null -ne $attributeAst)
                {
                    $attributeType = $attributeAst.TypeName.GetReflectionAttributeType()
                }
            }
            
            if ($null -ne $attributeType)
            {
                $results = $attributeType.GetProperties('Public,Instance') |
                Where-Object {
                    # Ignore TypeId (all attributes inherit it)
                    $_.Name -like "$attributeArgumentName*" -and $_.Name -ne 'TypeId'
                } |
                Sort-Object -Property Name |
                ForEach-Object {
                    $propType = [Microsoft.PowerShell.ToStringCodeMethods]::Type($_.PropertyType)
                    $propName = $_.Name
                    New-CompletionResult $propName -ToolTip "$propType $propName" -CompletionResultType Property
                }
                
                return [PSCustomObject]@{
                    Results              = $results
                    ReplacementIndex  = $replacementIndex
                    ReplacementLength = $replacementLength
                }
            }
        }
        catch { }
    }
    
    #############################################################################
    #
    # This function completes native commands options starting with - or --
    # works around a bug in PowerShell that causes it to not complete
    # native command options starting with - or --
    #
    function TryNativeCommandOptionCompletion
    {
        param (
            [System.Management.Automation.Language.Ast]
            $ast,
            
            [int]
            $offset
        )
        
        $results = @()
        $replacementIndex = $offset
        $replacementLength = 0
        try
        {
            # We want to find any Command element objects where the Ast extent includes $offset
            $offsetInOptionExtentPredicate = {
                param ($ast)
                return $offset -gt $ast.Extent.StartOffset -and
                $offset -le $ast.Extent.EndOffset -and
                $ast.Extent.Text.StartsWith('-')
            }
            $option = $ast.Find($offsetInOptionExtentPredicate, $true)
            if ($option -ne $null)
            {
                $command = $option.Parent -as [System.Management.Automation.Language.CommandAst]
                if ($command -ne $null)
                {
                    $nativeCommand = [System.IO.Path]::GetFileNameWithoutExtension($command.CommandElements[0].Value)
                    $nativeCompleter = $tabExpansionOptions.NativeArgumentCompleters[$nativeCommand]
                    
                    if ($nativeCompleter)
                    {
                        $results = @(& $nativeCompleter $option.ToString() $command)
                        if ($results.Count -gt 0)
                        {
                            $replacementIndex = $option.Extent.StartOffset
                            $replacementLength = $option.Extent.Text.Length
                        }
                    }
                }
            }
        }
        catch { }
        
        return [PSCustomObject]@{
            Results              = $results
            ReplacementIndex  = $replacementIndex
            ReplacementLength = $replacementLength
        }
    }
    
    
    #endregion Internal utility functions
    
    #############################################################################
    #
    # This function is partly a copy of the V3 TabExpansion2, adding a few
    # capabilities such as completing attribute arguments and excluding hidden
    # files from results.
    #
    function global:TabExpansion2
    {
        [CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
        param (
            [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory, Position = 0)]
            [string]
            $inputScript,
            
            [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory, Position = 1)]
            [int]
            $cursorColumn,
            
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory, Position = 0)]
            [System.Management.Automation.Language.Ast]
            $ast,
            
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory, Position = 1)]
            [System.Management.Automation.Language.Token[]]
            $tokens,
            
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory, Position = 2)]
            [System.Management.Automation.Language.IScriptPosition]
            $positionOfCursor,
            
            [Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
            [Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
            [Hashtable]
            $options = $null
        )
        
        if ($null -ne $options)
        {
            $options += $tabExpansionOptions
        }
        else
        {
            $options = $tabExpansionOptions
        }
        
        if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
        {
            $results = [System.Management.Automation.CommandCompletion]::CompleteInput(
            <#inputScript#>                $inputScript,
            <#cursorColumn#>                $cursorColumn,
            <#options#>                $options)
        }
        else
        {
            $results = [System.Management.Automation.CommandCompletion]::CompleteInput(
            <#ast#>                $ast,
            <#tokens#>                $tokens,
            <#positionOfCursor#>                $positionOfCursor,
            <#options#>                $options)
        }
        
        if ($results.CompletionMatches.Count -eq 0)
        {
            # Built-in didn't succeed, try our own completions here.
            if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
            {
                $ast = [System.Management.Automation.Language.Parser]::ParseInput($inputScript, [ref]$tokens, [ref]$null)
            }
            else
            {
                $cursorColumn = $positionOfCursor.Offset
            }
            
            # workaround PowerShell bug that case it to not invoking native completers for - or --
            # making it hard to complete options for many commands
            $nativeCommandResults = TryNativeCommandOptionCompletion -ast $ast -offset $cursorColumn
            if ($null -ne $nativeCommandResults)
            {
                $results.ReplacementIndex = $nativeCommandResults.ReplacementIndex
                $results.ReplacementLength = $nativeCommandResults.ReplacementLength
                if ($results.CompletionMatches.IsReadOnly)
                {
                    # Workaround where PowerShell returns a readonly collection that we need to add to.
                    $collection = new-object System.Collections.ObjectModel.Collection[System.Management.Automation.CompletionResult]
                    $results.GetType().GetProperty('CompletionMatches').SetValue($results, $collection)
                }
                $nativeCommandResults.Results | ForEach-Object {
                    $results.CompletionMatches.Add($_)
                }
            }
            
            $attributeResults = TryAttributeArgumentCompletion $ast $cursorColumn
            if ($null -ne $attributeResults)
            {
                $results.ReplacementIndex = $attributeResults.ReplacementIndex
                $results.ReplacementLength = $attributeResults.ReplacementLength
                if ($results.CompletionMatches.IsReadOnly)
                {
                    # Workaround where PowerShell returns a readonly collection that we need to add to.
                    $collection = new-object System.Collections.ObjectModel.Collection[System.Management.Automation.CompletionResult]
                    $results.GetType().GetProperty('CompletionMatches').SetValue($results, $collection)
                }
                $attributeResults.Results | ForEach-Object {
                    $results.CompletionMatches.Add($_)
                }
            }
        }
        
        if ($options.ExcludeHiddenFiles)
        {
            foreach ($result in @($results.CompletionMatches))
            {
                if ($result.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderItem -or
                    $result.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer)
                {
                    try
                    {
                        $item = Get-Item -LiteralPath $result.CompletionText -ErrorAction Stop
                    }
                    catch
                    {
                        # If Get-Item w/o -Force fails, it is probably hidden, so exclude the result
                        $null = $results.CompletionMatches.Remove($result)
                    }
                }
            }
        }
        if ($options.AppendBackslash -and
            $results.CompletionMatches.ResultType -contains [System.Management.Automation.CompletionResultType]::ProviderContainer)
        {
            foreach ($result in @($results.CompletionMatches))
            {
                if ($result.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer)
                {
                    $completionText = $result.CompletionText
                    $lastChar = $completionText[-1]
                    $lastIsQuote = ($lastChar -eq '"' -or $lastChar -eq "'")
                    if ($lastIsQuote)
                    {
                        $lastChar = $completionText[-2]
                    }
                    
                    if ($lastChar -ne '\')
                    {
                        $null = $results.CompletionMatches.Remove($result)
                        
                        if ($lastIsQuote)
                        {
                            $completionText =
                            $completionText.Substring(0, $completionText.Length - 1) +
                            '\' + $completionText[-1]
                        }
                        else
                        {
                            $completionText = $completionText + '\'
                        }
                        
                        $updatedResult = New-Object System.Management.Automation.CompletionResult `
                        ($completionText, $result.ListItemText, $result.ResultType, $result.ToolTip)
                        $results.CompletionMatches.Add($updatedResult)
                    }
                }
            }
        }
        
        if ($results.CompletionMatches.Count -eq 0)
        {
            # No results, if this module has overridden another TabExpansion2 function, call it
            # but only if it's not the built-in function (which we assume if function isn't
            # defined in a file.
            if ($oldTabExpansion2 -ne $null -and $oldTabExpansion2.File -ne $null)
            {
                return (& $oldTabExpansion2 @PSBoundParameters)
            }
        }
        
        return $results
    }
    
    
    #############################################################################
    #
    # Main
    #
    
    Add-Type @"
using System;
using System.Management.Automation;
 
public class NativeCommandTreeNode
{
    private NativeCommandTreeNode(NativeCommandTreeNode[] subCommands)
    {
        SubCommands = subCommands;
    }
 
    public NativeCommandTreeNode(string command, NativeCommandTreeNode[] subCommands)
        : this(command, null, subCommands)
    {
    }
 
    public NativeCommandTreeNode(string command, string tooltip, NativeCommandTreeNode[] subCommands)
        : this(subCommands)
    {
        this.Command = command;
        this.Tooltip = tooltip;
    }
 
    public NativeCommandTreeNode(string command, string tooltip, bool argument)
        : this(null)
    {
        this.Command = command;
        this.Tooltip = tooltip;
        this.Argument = true;
    }
 
    public NativeCommandTreeNode(ScriptBlock completionGenerator, NativeCommandTreeNode[] subCommands)
        : this(subCommands)
    {
        this.CompletionGenerator = completionGenerator;
    }
 
    public string Command { get; private set; }
    public string Tooltip { get; private set; }
    public bool Argument { get; private set; }
    public ScriptBlock CompletionGenerator { get; private set; }
    public NativeCommandTreeNode[] SubCommands { get; private set; }
}
"@

    
    # Custom completions are saved in this hashtable
    $tabExpansionOptions = @{
        CustomArgumentCompleters = @{ }
        NativeArgumentCompleters = @{ }
    }
    # Descriptions for the above completions saved in this hashtable
    $tabExpansionDescriptions = @{ }
    # And private data for the above completions cached in this hashtable
    $completionPrivateData = @{ }
}

function Convert-PsfConfigValue
{
<#
    .SYNOPSIS
        Converts a persisted configuration's value back to its data type.
     
    .DESCRIPTION
        Converts a persisted configuration's value back to its data type.
        Can be used for either registry-based or json-file-based items.
     
    .PARAMETER Value
        The full value item to decode (must include the original type identifier).
        Example:
          "bool:true"
     
    .EXAMPLE
        PS C:\> Convert-PsfConfigValue -Value "bool:true"
     
        Will return a boolean $true
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding()]
    Param (
        [string]
        $Value
    )
    
    begin
    {
        
    }
    process
    {
        $index = $Value.IndexOf(":")
        if ($index -lt 1) { throw "No type identifier found!" }
        $type = $Value.Substring(0, $index)
        $content = $Value.Substring($index + 1)
        
        switch ($type)
        {
            "bool"
            {
                if ($content -eq "true") { return $true }
                if ($content -eq "1") { return $true }
                if ($content -eq "false") { return $false }
                if ($content -eq "0") { return $false }
                throw "Failed to interpret as bool: $content"
            }
            "int" { return ([int]$content) }
            "double" { return [double]$content }
            "long" { return [long]$content }
            "string" { return $content }
            "timespan" { return (New-Object System.TimeSpan($content)) }
            "datetime" { return (New-Object System.DateTime($content)) }
            "consolecolor" { return ([System.ConsoleColor]$content) }
            "array"
            {
                if ($content -eq "") { return, @() }
                $tempArray = @()
                foreach ($item in ($content -split "þþþ"))
                {
                    $tempArray += Convert-PsfConfigValue -Value $item
                }
                return, $tempArray
            }
            
            default { throw "Unknown type identifier" }
        }
    }
    end
    {
    
    }
}

function Read-PsfConfigEnvironment {
<#
    .SYNOPSIS
        Reads configuration settings from environment variables.
     
    .DESCRIPTION
        Reads configuration settings from environment variables.
        Returns objects with two properties: Name & Value
     
    .PARAMETER Prefix
        The prefix by which to filter environment variables.
        Only variables that start with the prefix, followeb by an underscore are processed.
     
    .PARAMETER Simple
        Whether to perform simple data processing.
        By default, the full configuration data format is expected.
     
    .EXAMPLE
        PS C:\> Read-PsfConfigEnvironment -Prefix PSFramework
     
        Loads all configuration settings provided by environment starting with PSFramework_.
        Will apply full configuration object parsing.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Prefix,
        
        [switch]
        $Simple
    )
    
    begin {
        function ConvertFrom-EnvironmentSetting {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipelineByPropertyName = $true)]
                [string]
                $Name,
                
                [Parameter(ValueFromPipelineByPropertyName = $true)]
                [string]
                $Value,
                
                [bool]
                $Simple,
                
                [string]
                $Prefix
            )
            
            process {
                #region Common Mode
                if (-not $Simple) {
                    try {
                        [pscustomobject]@{
                            FullName = $Name.SubString(($Prefix.Length + 1))
                            Value = [PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($Value)
                        }
                    }
                    catch {
                        Write-PSFMessage -Level Warning -String 'Read-PsfConfigEnvironment.BadData' -StringValues $Name, $Value
                    }
                }
                #endregion Common Mode
                #region Simple Mode
                else {
                    $fullName = $Name.SubString(($Prefix.Length + 1))
                    if ($Value -eq '') { return [PSCustomObject]@{ FullName = $fullName; Value = $null } }
                    if ($Value -eq 'true') { return [PSCustomObject]@{ FullName = $fullName; Value = $true } }
                    if ($Value -eq 'false') { return [PSCustomObject]@{ FullName = $fullName; Value = $false } }
                    $tempVal = $null
                    if ([int32]::TryParse($Value, [ref]$tempVal)) {
                        return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal }
                    }
                    $tempVal = $null
                    if ([int64]::TryParse($Value, [ref]$tempVal)) {
                        return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal }
                    }
                    $tempVal = $null
                    if ([double]::TryParse($Value, 'Any', [System.Globalization.NumberFormatInfo]::InvariantInfo, [ref]$tempVal)) {
                        return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal }
                    }
                    $tempVal = $null
                    if ([datetime]::TryParse($Value, [System.Globalization.DateTimeFormatInfo]::InvariantInfo, 'AssumeUniversal', [ref]$tempVal)) {
                        return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal }
                    }
                    if ($Value -match "^.|*") {
                        return [PSCustomObject]@{ FullName = $fullName; Value = $Value.SubString(2).Split($Value.Substring(0, 1)) }
                    }
                    return [PSCustomObject]@{ FullName = $fullName; Value = $Value }
                }
                #endregion Simple Mode
            }
        }
    }
    process {
        Get-ChildItem "env:$($Prefix)_*" | ConvertFrom-EnvironmentSetting -Simple $Simple -Prefix $Prefix
    }
}

function Read-PsfConfigFile
{
<#
    .SYNOPSIS
        Reads a configuration file and parses it.
     
    .DESCRIPTION
        Reads a configuration file and parses it.
     
    .PARAMETER Path
        The path to the file to parse.
     
    .PARAMETER WebLink
        The link to a website to download straight as raw json.
     
    .PARAMETER RawJson
        Raw json data to interpret.
     
    .EXAMPLE
        PS C:\> Read-PsfConfigFile -Path config.json
     
        Reads the config.json file and returns interpreted configuration objects.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Weblink')]
        [string]
        $Weblink,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'RawJson')]
        [string]
        $RawJson
    )
    
    #region Utility Function
    function New-ConfigItem
    {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
        [CmdletBinding()]
        param (
            $FullName,
            
            $Value,
            
            $Type,
            
            [switch]
            $KeepPersisted,
            
            [switch]
            $Enforced,
            
            [switch]
            $Policy
        )
        
        [pscustomobject]@{
            FullName        = $FullName
            Value            = $Value
            Type            = $Type
            KeepPersisted   = $KeepPersisted
            Enforced        = $Enforced
            Policy            = $Policy
        }
    }
    
    function Get-WebContent
    {
        [CmdletBinding()]
        param (
            [string]
            $WebLink
        )
        
        $webClient = New-Object System.Net.WebClient
        $webClient.Encoding = [System.Text.Encoding]::UTF8
        $webClient.DownloadString($WebLink)
    }
    #endregion Utility Function
    
    if ($Path)
    {
        if (-not (Test-Path $Path)) { return }
        $data = Get-Content -Path $Path -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
    }
    if ($Weblink)
    {
        $data = Get-WebContent -WebLink $Weblink | ConvertFrom-Json -ErrorAction Stop
    }
    if ($RawJson)
    {
        $data = $RawJson | ConvertFrom-Json -ErrorAction Stop
    }
    
    foreach ($item in $data)
    {
        #region No Version
        if (-not $item.Version)
        {
            New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type))
        }
        #endregion No Version
        
        #region Version One
        if ($item.Version -eq 1)
        {
            if ((-not $item.Style) -or ($item.Style -eq "Simple")) { New-ConfigItem -FullName $item.FullName -Value $item.Data }
            else
            {
                if (($item.Type -eq "Object") -or ($item.Type -eq 12))
                {
                    New-ConfigItem -FullName $item.FullName -Value $item.Value -Type "Object" -KeepPersisted
                }
                else
                {
                    New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type))
                }
            }
        }
        #endregion Version One
    }
}

function Read-PsfConfigPersisted {
<#
    .SYNOPSIS
        Reads configurations from persisted file / registry.
     
    .DESCRIPTION
        Reads configurations from persisted file / registry.
     
    .PARAMETER Scope
        Where to read from.
     
    .PARAMETER Module
        Load module specific data.
        Use this to load on-demand configuration only when the module is imported.
        Useful when using the config system as cache.
     
    .PARAMETER ModuleVersion
        The configuration version of the module-settings to load.
     
    .PARAMETER Hashtable
        Rather than returning results, insert them into this hashtable.
     
    .PARAMETER Default
        When inserting into a hashtable, existing values are overwritten by default.
        Enabling this setting will cause it to only insert values if the key does not exist yet.
     
    .EXAMPLE
        Read-PsfConfigPersisted -Scope 127
     
        Read all persisted default configuration items in the default mandated order.
#>

    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]
    param (
        [PSFramework.Configuration.ConfigScope]
        $Scope,
        
        [string]
        $Module,
        
        [int]
        $ModuleVersion = 1,
        
        [System.Collections.Hashtable]
        $Hashtable,
        
        [switch]
        $Default
    )
    
    begin {
        #region Helper Functions
        function New-ConfigItem {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $FullName,
                
                $Value,
                
                $Type,
                
                [switch]
                $KeepPersisted,
                
                [switch]
                $Enforced,
                
                [switch]
                $Policy
            )
            
            [pscustomobject]@{
                FullName      = $FullName
                Value          = $Value
                Type          = $Type
                KeepPersisted = $KeepPersisted
                Enforced      = $Enforced
                Policy          = $Policy
            }
        }
        
        function Read-Registry {
            [CmdletBinding()]
            param (
                $Path,
                
                [switch]
                $Enforced
            )
            
            if (-not (Test-Path $Path)) { return }
            
            $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider'
            
            foreach ($item in ((Get-ItemProperty -Path $Path -ErrorAction Ignore).PSObject.Properties | Where-Object Name -NotIn $common)) {
                if ($item.Value -like "Object:*") {
                    $data = $item.Value.Split(":", 2)
                    New-ConfigItem -FullName $item.Name -Type $data[0] -Value $data[1] -KeepPersisted -Enforced:$Enforced -Policy
                }
                else {
                    try { New-ConfigItem -FullName $item.Name -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value)) -Policy }
                    catch {
                        Write-PSFMessage -Level Warning -Message "Failed to load configuration from Registry: $($item.Name)" -ErrorRecord $_ -Target "$Path : $($item.Name)"
                    }
                }
            }
        }
        #endregion Helper Functions
        
        if (-not $Hashtable) { $results = @{ } }
        else { $results = $Hashtable }
        
        if ($Module) { $filename = "$($Module.ToLower())-$($ModuleVersion).json" }
        else { $filename = "psf_config.json" }
    }
    process {
        #region Environment - Simple
        if ($Scope -band 256) {
            foreach ($item in Read-PsfConfigEnvironment -Prefix PSF -Simple) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Environment - Simple
        
        #region Environment - Full
        if ($Scope -band 128) {
            foreach ($item in Read-PsfConfigEnvironment -Prefix PSFramework) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Environment - Full
        
        #region File - Computer Wide
        if ($Scope -band 64) {
            if (-not $Module) {
                foreach ($file in Get-ChildItem -Path $script:path_FileSystem -Filter "psf_config_*.json" -ErrorAction Ignore) {
                    foreach ($item in Read-PsfConfigFile -Path $file.FullName) {
                        if (-not $Default) { $results[$item.FullName] = $item }
                        elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
                    }
                }
            }
            foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileSystem $filename))) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion File - Computer Wide
        
        #region Registry - Computer Wide
        if (($Scope -band 4) -and (-not $script:NoRegistry)) {
            foreach ($item in (Read-Registry -Path $script:path_RegistryMachineDefault)) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Registry - Computer Wide
        
        #region File - User Shared
        if ($Scope -band 32) {
            if (-not $Module) {
                foreach ($file in Get-ChildItem -Path $script:path_FileUserShared -Filter "psf_config_*.json" -ErrorAction Ignore) {
                    foreach ($item in Read-PsfConfigFile -Path $file.FullName) {
                        if (-not $Default) { $results[$item.FullName] = $item }
                        elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
                    }
                }
            }
            foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileUserShared $filename))) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion File - User Shared
        
        #region Registry - User Shared
        if (($Scope -band 1) -and (-not $script:NoRegistry)) {
            foreach ($item in (Read-Registry -Path $script:path_RegistryUserDefault)) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Registry - User Shared
        
        #region File - User Local
        if ($Scope -band 16) {
            if (-not $Module) {
                foreach ($file in Get-ChildItem -Path $script:path_FileUserLocal -Filter "psf_config_*.json" -ErrorAction Ignore) {
                    foreach ($item in Read-PsfConfigFile -Path $file.FullName) {
                        if (-not $Default) { $results[$item.FullName] = $item }
                        elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
                    }
                }
            }
            foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileUserLocal $filename))) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion File - User Local
        
        #region Registry - User Enforced
        if (($Scope -band 2) -and (-not $script:NoRegistry)) {
            foreach ($item in (Read-Registry -Path $script:path_RegistryUserEnforced -Enforced)) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Registry - User Enforced
        
        #region Registry - System Enforced
        if (($Scope -band 8) -and (-not $script:NoRegistry)) {
            foreach ($item in (Read-Registry -Path $script:path_RegistryMachineEnforced -Enforced)) {
                if (-not $Default) { $results[$item.FullName] = $item }
                elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item }
            }
        }
        #endregion Registry - System Enforced
    }
    end {
        $results
    }
}

function Write-PsfConfigFile
{
<#
    .SYNOPSIS
        Handles config export to file.
     
    .DESCRIPTION
        Handles config export to file.
     
    .PARAMETER Config
        The configuration items to export.
     
    .PARAMETER Path
        The path to export to.
        Needs to point to the specific file to export to.
        Will create the folder structure if needed.
     
    .PARAMETER Replace
        Completely replaces previous file contents.
        By default, it will integrate settings into one coherent configuration file.
     
    .EXAMPLE
        PS C:\> Write-PsfConfigFile -Config $items -Path .\file.json
     
        Exports all settings stored in $items to .\file.json.
        If the file already exists, the new settings will be merged into the existing file.
#>

    [CmdletBinding()]
    Param (
        [PSFramework.Configuration.Config[]]
        $Config,
        
        [string]
        $Path,
        
        [switch]
        $Replace
    )
    
    begin
    {
        $parent = Split-Path -Path $Path
        if (-not (Test-Path $parent))
        {
            $null = New-Item $parent -ItemType Directory -Force
        }
        
        $data = @{ }
        if ((Test-Path $Path) -and (-not $Replace))
        {
            foreach ($item in (Get-Content -Path $Path -Encoding UTF8 | ConvertFrom-Json))
            {
                $data[$item.FullName] = $item
            }
        }
    }
    process
    {
        foreach ($item in $Config)
        {
            $datum = @{
                Version  = 1
                FullName = $item.FullName
            }
            if ($item.SimpleExport)
            {
                $datum["Data"] = $item.Value
            }
            else
            {
                $persisted = [PSFramework.Configuration.ConfigurationHost]::ConvertToPersistedValue($item.Value)
                $datum["Value"] = $persisted.PersistedValue
                $datum["Type"] = $persisted.PersistedType
                $datum["Style"] = "default"
            }
            
            $data[$item.FullName] = [pscustomobject]$datum
        }
    }
    end
    {
        $data.Values | ConvertTo-Json | Set-Content -Path $Path -Encoding UTF8 -ErrorAction Stop
    }
}

function Resolve-PsfFilterConditionSet
{
<#
    .SYNOPSIS
        Internal helper to consistently resolve the relevant Condition Set.
     
    .DESCRIPTION
        Internal helper to consistently resolve the relevant Condition Set.
        Either by object or module & name combination.
     
    .PARAMETER ConditionSet
        A condition set object to use.
     
    .PARAMETER SetModule
        The name of the module from which to select the condition set to use.
     
    .PARAMETER SetName
        The name of the condition set to use.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, so that the terminating exception happens in the context of the caller.
     
    .EXAMPLE
        PS C:\> Resolve-PsfFilterConditionSet -ConditionSet $ConditionSet -SetModule $SetModule -SetName $SetName -Cmdlet $Cmdlet
     
        Resolves the Condition Set to use or fails in blood, pain and suffering.
#>

    [OutputType([PSFramework.Filter.ConditionSet])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [PSFramework.Filter.ConditionSet]
        $ConditionSet,
        
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $SetModule,
        
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $SetName,
        
        [Parameter(Mandatory = $true)]
        $Cmdlet
    )
    
    process
    {
        $conditionSetObject = $null
        if ($SetModule) {
            $conditionSetObject = $script:filterContainer.GetConditionSet($SetModule, $SetName)
            if (-not $conditionSetObject) {
                Invoke-PsfTerminatingException -Message "Unable to find condition set $SetName in module $SetModule" -ErrorId 'ConditionSetNotFound' -Category InvalidArgument -TargetObject $SetName -Cmdlet $Cmdlet
            }
        }
        if ($ConditionSet) { $conditionSetObject = $ConditionSet }
        if (-not $conditionSetObject) {
            Invoke-PsfTerminatingException -Message "Unable to find condition set" -ErrorId 'ConditionSetNotFound' -Category InvalidArgument -Cmdlet $Cmdlet -TargetObject $null
        }
        $conditionSetObject
    }
}

function Invoke-PsfTerminatingException
{
<#
    .SYNOPSIS
        Executes a terminating error.
     
    .DESCRIPTION
        Executes a terminating error in the context of the caller.
     
    .PARAMETER Message
        Message to include in the terminating error.
     
    .PARAMETER ErrorId
        Error ID to make part of the error record.
     
    .PARAMETER Category
        The error category.
     
    .PARAMETER TargetObject
        The target of the error.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller
     
    .EXAMPLE
        PS C:\> Invoke-PsfTerminatingException -Message 'Failed to do XYZ' -ErrorId 'FatalFail' -Category InvalidOperation -TargetObject $TargetObject -Cmdlet $PSCmdlet
     
        Executes a terminating error in the context of the caller.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Message,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ErrorId,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ErrorCategory]
        $Category,
        
        [Parameter(Mandatory = $true)]
        $TargetObject,
        
        [Parameter(Mandatory = $true)]
        $Cmdlet
    )
    
    process{
        $exception = switch ("$Category") {
            "InvalidArgument" { New-Object System.ArgumentException($Message) }
            default { New-Object System.Exception($Message) }
        }
        $errorRecord = New-Object System.Management.Automation.ErrorRecord($exception, $ErrorId, $Category, $TargetObject)
        $Cmdlet.ThrowTerminatingError($errorRecord)
    }
}

function Convert-PsfMessageException
{
    <#
        .SYNOPSIS
            Transforms the Exception input to the message system.
         
        .DESCRIPTION
            Transforms the Exception input to the message system.
         
            If there is an exception running a transformation scriptblock, it will log the error in the transform error queue and return the original object instead.
         
        .PARAMETER Exception
            The input Exception object, that might have to be transformed (may not either)
         
        .PARAMETER FunctionName
            The function writing the message
         
        .PARAMETER ModuleName
            The module, that the function writing the message is part of
         
        .EXAMPLE
            PS C:\> Convert-PsfMessageException -Exception $Exception -FunctionName 'Get-Test' -ModuleName 'MyModule'
         
            Checks internal storage for definitions that require a Exception transform, and either returns the original object or the transformed object.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        $Exception,
        
        [Parameter(Mandatory = $true)]
        [string]
        $FunctionName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName
    )
    
    if ($null -eq $Exception) { return }
    
    $typeName = $Exception.GetType().FullName
    
    if ([PSFramework.Message.MessageHost]::ExceptionTransforms.ContainsKey($typeName))
    {
        $scriptBlock = [PSFramework.Message.MessageHost]::ExceptionTransforms[$typeName]
        try
        {
            $tempException = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($scriptBlock.ToString())), $null, $Exception)
            return $tempException
        }
        catch
        {
            [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Exception, "Exception", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))
            return $Exception
        }
    }
    
    if ($transform = [PSFramework.Message.MessageHost]::ExceptionTransformList.Get($typeName, $ModuleName, $FunctionName))
    {
        try
        {
            $tempException = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($transform.ScriptBlock.ToString())), $null, $Exception)
            return $tempException
        }
        catch
        {
            [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Exception, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))
            return $Exception
        }
    }
    
    return $Exception
}

function Convert-PsfMessageLevel
{
    <#
        .SYNOPSIS
            Processes the effective message level of a message
         
        .DESCRIPTION
            Processes the effective message level of a message
            - Applies level decrements
            - Applies message level modifiers
         
        .PARAMETER OriginalLevel
            The level the message was originally written to
         
        .PARAMETER FromStopFunction
            Whether the message was passed through Stop-PSFFunction first.
            This is used to increment the automatic message level decrement counter by 1 (so it ignores the fact, that it was passed through Stop-PSFFunction).
            The automatic message level decrement functionality allows users to make nested commands' messages be less verbose.
         
        .PARAMETER Tags
            The tags that were added to the message
         
        .PARAMETER FunctionName
            The function that wrote the message.
         
        .PARAMETER ModuleName
            The module the function writing the message comes from.
     
        .EXAMPLE
            Convert-PsfMessageLevel -OriginalLevel $Level -FromStopFunction $fromStopFunction -Tags $Tag -FunctionName $FunctionName -ModuleName $ModuleName
     
            This will convert the original level of $Level based on the transformation rules for levels.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [PSFramework.Message.MessageLevel]
        $OriginalLevel,
        
        [Parameter(Mandatory = $true)]
        [bool]
        $FromStopFunction,
        
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [string[]]
        $Tags,
        
        [Parameter(Mandatory = $true)]
        [string]
        $FunctionName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName
    )
    
    $number = $OriginalLevel.value__
    
    if ([PSFramework.Message.MessageHost]::NestedLevelDecrement -gt 0)
    {
        $depth = (Get-PSCallStack).Count - 3
        if ($FromStopFunction) { $depth = $depth - 1 }
        $number = $number + $depth * ([PSFramework.Message.MessageHost]::NestedLevelDecrement)
    }
    
    foreach ($modifier in [PSFramework.Message.MessageHost]::MessageLevelModifiers.Values)
    {
        if ($modifier.AppliesTo($FunctionName, $ModuleName, $Tags))
        {
            $number = $number + $modifier.Modifier
        }
    }
    
    # Finalize number and return
    if ($number -lt 1) { $number = 1 }
    if ($number -gt 9) { $number = 9 }
    return ([PSFramework.Message.MessageLevel]$number)
}

function Convert-PsfMessageTarget
{
    <#
        .SYNOPSIS
            Transforms the target input to the message system.
         
        .DESCRIPTION
            Transforms the target input to the message system.
         
            If there is an exception running a transformation scriptblock, it will log the error in the transform error queue and return the original object instead.
         
        .PARAMETER Target
            The input target object, that might have to be transformed (may not either)
         
        .PARAMETER FunctionName
            The function writing the message
         
        .PARAMETER ModuleName
            The module, that the function writing the message is part of
         
        .EXAMPLE
            PS C:\> Convert-PsfMessageTarget -Target $Target -FunctionName 'Get-Test' -ModuleName 'MyModule'
         
            Checks internal storage for definitions that require a target transform, and either returns the original object or the transformed object.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        $Target,
        
        [Parameter(Mandatory = $true)]
        [string]
        $FunctionName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName
    )
    
    if ($null -eq $Target) { return }
    
    $typeName = $Target.GetType().FullName
    
    if ([PSFramework.Message.MessageHost]::TargetTransforms.ContainsKey($typeName))
    {
        $scriptBlock = [PSFramework.Message.MessageHost]::TargetTransforms[$typeName]
        try
        {
            $tempTarget = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($scriptBlock.ToString())), $null, $Target)
            return $tempTarget
        }
        catch
        {
            [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Target, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))
            return $Target
        }
    }
    
    if ($transform = [PSFramework.Message.MessageHost]::TargetTransformlist.Get($typeName, $ModuleName, $FunctionName))
    {
        try
        {
            $tempTarget = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($transform.ScriptBlock.ToString())), $null, $Target)
            return $tempTarget
        }
        catch
        {
            [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Target, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))
            return $Target
        }
    }
    
    return $Target
}

function Invoke-PSFCommand
{
<#
    .SYNOPSIS
        An Invoke-Command wrapper with integrated session management.
     
    .DESCRIPTION
        This wrapper command around Invoke-Command allows conveniently calling remote calls.
     
        - It uses the PSFComputer parameter class, and is thus a lot more flexible in accepted input
        - It automatically reuses sessions specified for input
        - It automatically establishes new sessions, tracks usage and retires sessions that have timed out.
     
        Using this command, it is no longer necessary to first establish a connection and then manually handle the session object.
        Just point the command at the computer and it will remember.
        It also reuses sessions across multiple commands that call it.
     
        Note:
        Special connection conditions (like a custom application name, alternative authentication schemes, etc.) are not supported and require using New-PSSession to establish the connection.
        Once that session has been established, the session object can be used with this command and will be used for command invocation.
     
    .PARAMETER ComputerName
        The computer(s) to invoke the command on.
        Accepts all kinds of things that legally point at a computer, including DNS names, ADComputer objects, IP Addresses, SQL Server connection strings, CimSessions or PowerShell Sessions.
        It will reuse PSSession objects if specified (and not include them in its session management).
     
    .PARAMETER ScriptBlock
        The code to execute.
     
    .PARAMETER ArgumentList
        The arguments to pass into the scriptblock.
     
    .PARAMETER Credential
        Credentials to use when establishing connections.
        Note: These will be ignored if there already exists an established connection.
     
    .PARAMETER HideComputerName
        Indicates that this cmdlet omits the computer name of each object from the output display. By default, the name of the computer that generated the object appears in the display.
     
    .PARAMETER ThrottleLimit
        Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 32, is used.
     
    .EXAMPLE
        PS C:\> Invoke-PSFCommand -ScriptBlock $ScriptBlock
     
        Runs the $scriptblock against the local computer.
     
    .EXAMPLE
        PS C:\> Invoke-PSFCommand -ScriptBlock $ScriptBlock (Get-ADComputer -Filter "name -like 'srv-db*'")
     
        Runs the $scriptblock against all computers in AD with a name that starts with "srv-db".
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Invoke-PSFCommand')]
    param (
        [PSFComputer[]]
        [Alias('Session')]
        $ComputerName = $env:COMPUTERNAME,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,
        
        [object[]]
        $ArgumentList,
        
        [System.Management.Automation.CredentialAttribute()]
        [System.Management.Automation.PSCredential]
        $Credential,
        
        [switch]
        $HideComputerName,
        
        [int]
        $ThrottleLimit = 32
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
        
        #region Clean up broken sessions
        [array]$broken = $psframework_pssessions.GetBroken()
        foreach ($sessionInfo in $broken)
        {
            Write-PSFMessage -Level Debug -Message "Removing broken session to $($sessionInfo.ComputerName)"
            Remove-PSSession -Session $sessionInfo.Session -ErrorAction Ignore
            $null = $psframework_pssessions.Remove($sessionInfo.ComputerName)
        }
        #endregion Clean up broken sessions
        
        #region Invoke Command Splats
        $paramInvokeCommand = @{
            ScriptBlock           = $ScriptBlock
            ArgumentList       = $ArgumentList
            HideComputerName   = $HideComputerName
            ThrottleLimit       = $ThrottleLimit
        }
        
        $paramInvokeCommandLocal = @{
            ScriptBlock            = $ScriptBlock
            ArgumentList        = $ArgumentList
        }
        #endregion Invoke Command Splats
    }
    process
    {
        #region Collect list of sessions to process
        $sessionsToInvoke = @()
        $managedSessions = @()
        
        foreach ($computer in $ComputerName)
        {
            if ($computer.Type -eq "PSSession") { $sessionsToInvoke += $computer.InputObject }
            elseif ($sessionObject = $computer.InputObject -as [System.Management.Automation.Runspaces.PSSession]) { $sessionsToInvoke += $sessionObject }
            else
            {
                #region Handle localhost
                if ($computer.IsLocalHost)
                {
                    Write-PSFMessage -Level Verbose -Message "Executing command against localhost" -Target $computer
                    Invoke-Command @paramInvokeCommandLocal
                    continue
                }
                #endregion Handle localhost
                
                #region Already have a cached session
                if ($session = $psframework_pssessions[$computer.ComputerName])
                {
                    $sessionsToInvoke += $session.Session
                    $managedSessions += $session
                    $session.ResetTimestamp()
                }
                #endregion Already have a cached session
                
                #region Establish new session and add to management
                else
                {
                    Write-PSFMessage -Level Verbose -Message "Establishing connection to $computer" -Target $computer
                    try
                    {
                        if ($Credential) { $pSSession = New-PSSession -ComputerName $computer -Credential $Credential -ErrorAction Stop }
                        else { $pSSession = New-PSSession -ComputerName $computer -ErrorAction Stop }
                    }
                    catch
                    {
                        Write-PSFMessage -Level Warning -Message "Failed to connect to $computer" -ErrorRecord $_ -Target $computer 3>$null
                        Write-Error -ErrorRecord $_
                        continue
                    }
                    
                    $session = New-Object PSFramework.ComputerManagement.PSSessioninfo($pSSession)
                    $psframework_pssessions[$session.ComputerName] = $session
                    $sessionsToInvoke += $session.Session
                    $managedSessions += $session
                }
                #endregion Establish new session and add to management
            }
        }
        #endregion Collect list of sessions to process
        
        if ($sessionsToInvoke)
        {
            Write-PSFMessage -Level VeryVerbose -Message "Invoking command against $($sessionsToInvoke.ComputerName -join ', ' )"
            Invoke-Command -Session $sessionsToInvoke @paramInvokeCommand
        }
        
        #region Refresh timestamp
        foreach ($session in $managedSessions)
        {
            $session.ResetTimestamp()
        }
        #endregion Refresh timestamp
    }
    end
    {
        #region Cleanup expired sessions
        [array]$expired = $psframework_pssessions.GetExpired()
        foreach ($sessionInfo in $expired)
        {
            Write-PSFMessage -Level Debug -Message "Removing expired session to $($sessionInfo.ComputerName)"
            Remove-PSSession -Session $sessionInfo.Session -ErrorAction Ignore
            $null = $psframework_pssessions.Remove($sessionInfo.ComputerName)
        }
        #endregion Cleanup expired sessions
    }
}

function New-PSFSessionContainer
{
<#
    .SYNOPSIS
        Creates an object containing multiple session objects to the same computer.
     
    .DESCRIPTION
        Creates an object containing multiple session objects to the same computer.
        Using this, a single object can be used to point at a computer while containing session objects for multiple protocols inside.
     
        Only session types registered via Reigster-PSSessionObjectType are supported.
     
    .PARAMETER ComputerName
        The name of the computer to connect to
     
    .PARAMETER Session
        The session objects that are a live connection to the host.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> New-PSFSessionContainer -ComputerName "server1" -Session $pssession, $cimsession, $smosession
     
        Create a session container containing three different kinds of session objects
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSFComputer]
        $ComputerName,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object[]]
        $Session,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $container = New-Object PSFramework.ComputerManagement.SessionContainer
        $container.ComputerName = $ComputerName
    }
    process
    {
        foreach ($sessionItem in $Session)
        {
            if ($null -eq $sessionItem) { continue }
            
            if (-not ($sessionName = [PSFramework.ComputerManagement.ComputerManagementHost]::KnownSessionTypes[$sessionItem.GetType()]))
            {
                Stop-PSFFunction -String 'New-PSFSessionContainer.UnknownSessionType' -StringValues $sessionItem.GetType().Name, $sessionItem -Continue -EnableException $EnableException
            }
            
            $container.Connections[$sessionName] = $sessionItem
        }
    }
    end
    {
        $container
    }
}

function Register-PSFSessionObjectType
{
<#
    .SYNOPSIS
        Registers a new type as a live session object.
     
    .DESCRIPTION
        Registers a new type as a live session object.
        This is used in the session container object, used to pass through multiple types of connection objects to a single PSFComputer parameterclassed parameter.
     
    .PARAMETER DisplayName
        The display name for the type.
        Pick anything that intuitively points at what the object is.
     
    .PARAMETER TypeName
        The full name of the type.
     
    .EXAMPLE
        PS C:\> Register-PSFSessionObjectType -DisplayName 'PSSession' -TypeName 'System.Management.Automation.Runspaces.PSSession'
     
        Registers the type 'System.Management.Automation.Runspaces.PSSession' under the name of 'PSSession'.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $DisplayName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $TypeName
    )
    
    process
    {
        [PSFramework.ComputerManagement.ComputerManagementHost]::KnownSessionTypes[$TypeName] = $DisplayName
    }
}

function Export-PSFConfig
{
<#
    .SYNOPSIS
        Exports configuration items to a Json file.
     
    .DESCRIPTION
        Exports configuration items to a Json file.
     
    .PARAMETER FullName
        Select the configuration objects to export by filtering by their full name.
     
    .PARAMETER Module
        Select the configuration objects to export by filtering by their module name.
     
    .PARAMETER Name
        Select the configuration objects to export by filtering by their name.
     
    .PARAMETER Config
        The configuration object(s) to export.
        Returned by Get-PSFConfig.
     
    .PARAMETER ModuleName
        Exports all configuration pertinent to a module to a predefined path.
        Exported configuration items include all settings marked as 'ModuleExport' that have been changed from the default value.
     
    .PARAMETER ModuleVersion
        The configuration version of the module-settings to write.
     
    .PARAMETER Scope
        Which predefined path to write module specific settings to.
        Only file scopes are considered.
        By default it writes to the suer profile.
     
    .PARAMETER OutPath
        The path (filename included) to export to.
        Will fail if the folder does not exist, will overwrite the file if it exists.
     
    .PARAMETER SkipUnchanged
        If set, configuration objects whose value was not changed from its original value will not be exported.
        (Note: Settings that were updated with the same value as the original default will still be considered changed)
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-PSFConfig | Export-PSFConfig -OutPath '~/export.json'
         
        Exports all current settings to json.
     
    .EXAMPLE
        Export-PSFConfig -Module MyModule -OutPath '~/export.json' -SkipUnchanged
         
        Exports all settings of the module 'MyModule' that are no longer the original default values to json.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'FullName', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Export-PSFConfig')]
    param (
        [Parameter(ParameterSetName = "FullName", Position = 0, Mandatory = $true)]
        [string]
        $FullName,
        
        [Parameter(ParameterSetName = "Module", Position = 0, Mandatory = $true)]
        [string]
        $Module,
        
        [Parameter(ParameterSetName = "Module", Position = 1)]
        [string]
        $Name = "*",
        
        [Parameter(ParameterSetName = "Config", Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [PSFramework.Configuration.Config[]]
        $Config,
        
        [Parameter(ParameterSetName = "ModuleName", Mandatory = $true)]
        [string]
        $ModuleName,
        
        [Parameter(ParameterSetName = "ModuleName")]
        [int]
        $ModuleVersion = 1,
        
        [Parameter(ParameterSetName = "ModuleName")]
        [PSFramework.Configuration.ConfigScope]
        $Scope = "FileUserShared",
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = 'Config')]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = 'FullName')]
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = 'Module')]
        [string]
        $OutPath,
        
        [switch]
        $SkipUnchanged,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $items = @()
        
        # Values 1, 2, 4 and 8 represent the four registry locations
        if (($Scope -band 15) -and $ModuleName)
        {
            Stop-PSFFunction -String 'Export-PSFConfig.ToRegistry' -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'scope', 'registry'
            return
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        if (-not $ModuleName)
        {
            foreach ($item in $Config) { $items += $item }
            if ($FullName) { $items = Get-PSFConfig -FullName $FullName }
            if ($Module) { $items = Get-PSFConfig -Module $Module -Name $Name }
        }
    }
    end
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        if (-not $ModuleName)
        {
            try { Write-PsfConfigFile -Config ($items | Where-Object { -not $SkipUnchanged -or -not $_.Unchanged }) -Path $OutPath -Replace }
            catch
            {
                Stop-PSFFunction -String 'Export-PSFConfig.Write.Error' -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'export'
                return
            }
        }
        else
        {
            if ($Scope -band 16) # File: User Local
            {
                Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileUserLocal "$($ModuleName.ToLower())-$($ModuleVersion).json")
            }
            if ($Scope -band 32) # File: User Shared
            {
                Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileUserShared "$($ModuleName.ToLower())-$($ModuleVersion).json")
            }
            if ($Scope -band 64) # File: System-Wide
            {
                Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileSystem "$($ModuleName.ToLower())-$($ModuleVersion).json")
            }
        }
    }
}

function Get-PSFConfig
{
    <#
        .SYNOPSIS
            Retrieves configuration elements by name.
         
        .DESCRIPTION
            Retrieves configuration elements by name.
            Can be used to search the existing configuration list.
     
        .PARAMETER FullName
            Default: "*"
            Search for configurations using the full name
         
        .PARAMETER Name
            Default: "*"
            The name of the configuration element(s) to retrieve.
            May be any string, supports wildcards.
         
        .PARAMETER Module
            Default: "*"
            Search configuration by module.
         
        .PARAMETER Force
            Overrides the default behavior and also displays hidden configuration values.
         
        .EXAMPLE
            PS C:\> Get-PSFConfig 'Mail.To'
             
            Retrieves the configuration element for the key "Mail.To"
     
        .EXAMPLE
            PS C:\> Get-PSFConfig -Force
     
            Retrieve all configuration elements from all modules, even hidden ones.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [OutputType([PSFramework.Configuration.Config])]
    [CmdletBinding(DefaultParameterSetName = "FullName", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFConfig')]
    Param (
        [Parameter(ParameterSetName = "FullName", Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $FullName = "*",
        
        [Parameter(ParameterSetName = "Module", Position = 1)]
        [string]
        $Name = "*",
        
        [Parameter(ParameterSetName = "Module", Position = 0)]
        [string]
        $Module = "*",
        
        [switch]
        $Force
    )
    
    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            "Module"
            {
                [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object {
                    ($_.Name -like $Name) -and
                    ($_.Module -like $Module) -and
                    ((-not $_.Hidden) -or ($Force))
                } | Sort-Object Module, Name
            }
            
            "FullName"
            {
                [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object {
                    ("$($_.Module).$($_.Name)" -like $FullName) -and
                    ((-not $_.Hidden) -or ($Force))
                } | Sort-Object Module, Name
            }
        }
    }
}


function Get-PSFConfigValue
{
    <#
        .SYNOPSIS
            Returns the configuration value stored under the specified name.
         
        .DESCRIPTION
            Returns the configuration value stored under the specified name.
            It requires the full name (<Module>.<Name>) and is usually only called by functions.
         
        .PARAMETER FullName
            The full name (<Module>.<Name>) of the configured value to return.
     
        .PARAMETER Fallback
            A fallback value to use, if no value was registered to a specific configuration element.
            This basically is a default value that only applies on a "per call" basis, rather than a system-wide default.
         
        .PARAMETER NotNull
            By default, this function returns null if one tries to retrieve the value from either a Configuration that does not exist or a Configuration whose value was set to null.
            However, sometimes it may be important that some value was returned.
            By specifying this parameter, the function will throw an error if no value was found at all.
         
        .EXAMPLE
            PS C:\> Get-PSFConfigValue -FullName 'System.MailServer'
     
            Returns the configured value that was assigned to the key 'System.MailServer'
     
        .EXAMPLE
            PS C:\> Get-PSFConfigValue -FullName 'Default.CoffeeMilk' -Fallback 0
     
            Returns the configured value for 'Default.CoffeeMilk'. If no such value is configured, it returns '0' instead.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFConfigValue')]
    param (
        [Alias('Name')]
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $FullName,
        
        [object]
        $Fallback,
        
        [switch]
        $NotNull
    )
    
    process
    {
        $temp = $null
        $temp = [PSFramework.Configuration.ConfigurationHost]::Configurations[$FullName].Value
        if ($null -eq $temp) { $temp = $Fallback }
        
        if ($NotNull -and ($null -eq $temp))
        {
            Stop-PSFFunction -String 'Get-PSFConfigValue.NoValue' -StringValues $FullName -EnableException $true -Category InvalidData -Target $FullName
        }
        return $temp
    }
}

function Import-PSFConfig {
<#
    .SYNOPSIS
        Imports a configuration file into the configuration system.
     
    .DESCRIPTION
        Imports a configuration file into the configuration system.
        There are two modes of import:
        - By ModuleName for the module cache scenario:
        https://psframework.org/documentation/documents/psframework/configuration/scenario-cache.html
        This consumes the json files generated by Export-PSFConfig used in the same scenario.
        - By explicit Path.
        When importing by path, you use a configuration schema to parse the input file.
        The default schema expects the json file format produced by Export-PSFConfig,
        however you can freely extend this using the Register-PSFConfigSchema to understand other formats,
        such as csv, XML, yaml, or whatever else you may care to parse as configuration.
     
    .PARAMETER Path
        The path to the file to import.
        Ensure the file is properly formatted for the configuration schema specified.
     
    .PARAMETER ModuleName
        Import configuration items specific to a module from the default configuration paths.
     
    .PARAMETER ModuleVersion
        The configuration version of the module-settings to load.
     
    .PARAMETER Scope
        Where to import the module specific configuration items form.
        Only file-based scopes are supported for this.
        By default, all locations are queried, with user settings beating system settings.
     
    .PARAMETER Schema
        The configuration schema to use for import.
        Use Register-PSFConfigSchema to extend the way input content can be laid out.
     
    .PARAMETER IncludeFilter
        If specified, only elements with names that are similar (-like) to names in this list will be imported.
     
    .PARAMETER ExcludeFilter
        Elements that are similar (-like) to names in this list will not be imported.
     
    .PARAMETER Peek
        Rather than applying the setting, return the configuration items that would have been applied.
     
    .PARAMETER AllowDelete
        Configurations that have been imported will be flagged as deletable.
        This allows to purge them at a later time using Remove-PSFConfig.
     
    .PARAMETER EnvironmentPrefix
        Import values from environment variables.
        Entries will be expected to start with the prefix, then an Underscore, then the full name of the configuration setting.
        Example: PSF_PSFramework.Utility.Size.Digits
        By default, the same value formatting needs to be adhered to as is in registry settings.
        For example, to store the number 3, the value would be "Int:3". Use:
          (Get-PSFConfig -FullName '<name of setting>').RegistryData
        To see how an existing setting would look in that format.
        You can switch to simple mode using the '-Simple' parameter.
        Which cannot handle complex objects, but has less overhead for simple data types.
     
    .PARAMETER Simple
        Switches the import from environment variables into a simple data mode.
        In this mode it will only understand a few simple data types, but provide for very simple value formatting:
        - An empty string will be $null
        - "true" will be $true
        - "false" will be $false
        - A number (e.g. "12") will be parsed as integer first, long second, double third
        - A DateTime compliant string will be parsed as such, ignoring local culture.
        - A value starting with any character followed by a "|" will be considered a string array.
          the first character will be the delimiter.
          ";|abc;def;ghi" would thus become @("abc","def","ghi")
        - Anything else will be considered a string.
     
    .PARAMETER PassThru
        Return configuration settings that have been imported.
        By default, this command will not produce any output.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Import-PSFConfig -Path '.\config.json'
         
        Imports the configuration stored in '.\config.json'
     
    .EXAMPLE
        PS C:\> Import-PSFConfig -ModuleName mymodule
         
        Imports all the module specific settings that have been persisted in any of the default file system paths.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")]
    [CmdletBinding(DefaultParameterSetName = "Path", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFConfig')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Path")]
        [string[]]
        $Path,
        
        [Parameter(ParameterSetName = "ModuleName", Mandatory = $true)]
        [string]
        $ModuleName,
        
        [Parameter(ParameterSetName = "ModuleName")]
        [int]
        $ModuleVersion = 1,
        
        [Parameter(ParameterSetName = "ModuleName")]
        [PSFramework.Configuration.ConfigScope]
        $Scope = "FileUserLocal, FileUserShared, FileSystem",
        
        [Parameter(ParameterSetName = "Path")]
        [PsfValidateSet(TabCompletion = 'PSFramework-Config-Schema')]
        [string]
        $Schema = "Default",
        
        [Parameter(ParameterSetName = "Path")]
        [string[]]
        $IncludeFilter,
        
        [Parameter(ParameterSetName = "Path")]
        [string[]]
        $ExcludeFilter,
        
        [Parameter(ParameterSetName = "Path")]
        [switch]
        $Peek,
        
        [Parameter(ParameterSetName = 'Path')]
        [switch]
        $AllowDelete,
        
        [Parameter(ParameterSetName = 'Environment')]
        [string]
        $EnvironmentPrefix,
        
        [Parameter(ParameterSetName = 'Environment')]
        [switch]
        $Simple,
        
        [switch]
        $PassThru,
        
        [switch]
        $EnableException
    )
    
    begin {
        $settings = @{
            IncludeFilter = $IncludeFilter
            ExcludeFilter = $ExcludeFilter
            Peek          = $Peek.ToBool()
            AllowDelete   = $AllowDelete.ToBool()
            EnableException = $EnableException.ToBool()
            Cmdlet          = $PSCmdlet
            Path          = (Get-Location).Path
            PassThru      = $PassThru.ToBool()
        }
        
        $schemaScript = [PSFramework.Configuration.ConfigurationHost]::Schemata[$Schema]
    }
    process {
        #region Explicit Path
        foreach ($item in $Path) {
            try { $resolvedItem = Resolve-PSFPath -Path $item -Provider FileSystem }
            catch { $resolvedItem = $item } # More than just filesystem paths are permissible
            
            foreach ($rItem in $resolvedItem) {
                $schemaScript.ToGlobal().Invoke($rItem, $settings)
            }
        }
        #endregion Explicit Path
        
        #region ModuleName
        if ($ModuleName) {
            $data = Read-PsfConfigPersisted -Module $ModuleName -Scope $Scope -ModuleVersion $ModuleVersion
            
            foreach ($value in $data.Values) {
                if (-not $value.KeepPersisted) { Set-PSFConfig -FullName $value.FullName -Value $value.Value -EnableException:$EnableException -PassThru:$PassThru }
                else { Set-PSFConfig -FullName $value.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($value.Value, $value.Type)) -EnableException:$EnableException -PassThru:$PassThru }
            }
        }
        #endregion ModuleName
        
        #region Environment
        if ($EnvironmentPrefix) {
            foreach ($entry in Read-PsfConfigEnvironment -Prefix $EnvironmentPrefix -Simple:$Simple) {
                Set-PSFConfig -FullName $entry.FullName -Value $entry.Value -PassThru:$PassThru -EnableException:$EnableException
            }
        }
        #endregion Environment
    }
}

function Register-PSFConfig
{
<#
    .SYNOPSIS
        Registers an existing configuration object in registry.
     
    .DESCRIPTION
        Registers an existing configuration object in registry.
        This allows simple persisting of settings across powershell consoles.
        It also can be used to generate a registry template, which can then be used to create policies.
     
    .PARAMETER Config
        The configuration object to write to registry.
        Can be retrieved using Get-PSFConfig.
     
    .PARAMETER FullName
        The full name of the setting to be written to registry.
     
    .PARAMETER Module
        The name of the module, whose settings should be written to registry.
     
    .PARAMETER Name
        Default: "*"
        Used in conjunction with the -Module parameter to restrict the number of configuration items written to registry.
     
    .PARAMETER Scope
        Default: UserDefault
        Who will be affected by this export how? Current user or all? Default setting or enforced?
        Legal values: UserDefault, UserMandatory, SystemDefault, SystemMandatory
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-PSFConfig psframework.message.* | Register-PSFConfig
     
        Retrieves all configuration items that that start with psframework.message. and registers them in registry for the current user.
     
    .EXAMPLE
        PS C:\> Register-PSFConfig -FullName "psframework.developer.mode.enable" -Scope SystemDefault
     
        Retrieves the configuration item "psframework.developer.mode.enable" and registers it in registry as the default setting for all users on this machine.
     
    .EXAMPLE
        PS C:\> Register-PSFConfig -Module MyModule -Scope SystemMandatory
     
        Retrieves all configuration items of the module MyModule, then registers them in registry to enforce them for all users on the current system.
#>

    [CmdletBinding(DefaultParameterSetName = "Default", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFConfig')]
    param (
        [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)]
        [PSFramework.Configuration.Config[]]
        $Config,
        
        [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)]
        [string[]]
        $FullName,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)]
        [string]
        $Module,
        
        [Parameter(ParameterSetName = "Name", Position = 1)]
        [string]
        $Name = "*",
        
        [PSFramework.Configuration.ConfigScope]
        $Scope = "UserDefault",
        
        [switch]
        $EnableException
    )
    
    begin
    {
        if ($script:NoRegistry -and ($Scope -band 10))
        {
            Stop-PSFFunction -String 'Register-PSFConfig.NoRegistry' -Tag 'NotSupported' -Category ResourceUnavailable
            return
        }
        
        # Linux and MAC default to local user store file
        if ($script:NoRegistry -and ($Scope -eq "UserDefault"))
        {
            $Scope = [PSFramework.Configuration.ConfigScope]::FileUserLocal
        }
        # Linux and MAC get redirection for SystemDefault to FileSystem
        if ($script:NoRegistry -and ($Scope -eq "SystemDefault"))
        {
            $Scope = [PSFramework.Configuration.ConfigScope]::FileSystem
        }
        
        function Write-Config
        {
            [CmdletBinding()]
            param (
                [PSFramework.Configuration.Config]
                $Config,
                
                [PSFramework.Configuration.ConfigScope]
                $Scope,
                
                [bool]
                $EnableException,
                
                [string]
                $FunctionName = (Get-PSCallStack)[0].Command
            )
            
            if (-not $Config -or ($Config.RegistryData -eq "<type not supported>"))
            {
                Stop-PSFFunction -String 'Register-PSFConfig.Type.NotSupported' -StringValues $Config.FullName -EnableException $EnableException -Category InvalidArgument -Tag "config", "fail" -Target $Config -FunctionName $FunctionName -ModuleName "PSFramework"
                return
            }
            
            try
            {
                Write-PSFMessage -Level Verbose -String 'Register-PSFConfig.Registering' -StringValues $Config.FullName, $Scope -Tag "Config" -Target $Config -FunctionName $FunctionName -ModuleName "PSFramework"
                #region User Default
                if (1 -band $Scope)
                {
                    Ensure-RegistryPath -Path $script:path_RegistryUserDefault -ErrorAction Stop
                    Set-ItemProperty -Path $script:path_RegistryUserDefault -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop
                }
                #endregion User Default
                
                #region User Mandatory
                if (2 -band $Scope)
                {
                    Ensure-RegistryPath -Path $script:path_RegistryUserEnforced -ErrorAction Stop
                    Set-ItemProperty -Path $script:path_RegistryUserEnforced -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop
                }
                #endregion User Mandatory
                
                #region System Default
                if (4 -band $Scope)
                {
                    Ensure-RegistryPath -Path $script:path_RegistryMachineDefault -ErrorAction Stop
                    Set-ItemProperty -Path $script:path_RegistryMachineDefault -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop
                }
                #endregion System Default
                
                #region System Mandatory
                if (8 -band $Scope)
                {
                    Ensure-RegistryPath -Path $script:path_RegistryMachineEnforced -ErrorAction Stop
                    Set-ItemProperty -Path $script:path_RegistryMachineEnforced -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop
                }
                #endregion System Mandatory
            }
            catch
            {
                Stop-PSFFunction -String 'Register-PSFConfig.Registering.Failed' -StringValues $Config.FullName, $Scope -EnableException $EnableException -Tag "config", "fail" -Target $Config -ErrorRecord $_ -FunctionName $FunctionName -ModuleName "PSFramework"
                return
            }
        }
        
        function Ensure-RegistryPath
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
            [CmdletBinding()]
            param (
                [string]
                $Path
            )
            
            if (-not (Test-Path $Path))
            {
                $null = New-Item $Path -Force
            }
        }
        
        # For file based persistence
        $fileConfigurationItems = @()
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Registry Based
        if ($Scope -band 15)
        {
            switch ($PSCmdlet.ParameterSetName)
            {
                "Default"
                {
                    foreach ($item in $Config)
                    {
                        Write-Config -Config $item -Scope $Scope -EnableException $EnableException
                    }
                    
                    foreach ($item in $FullName)
                    {
                        if ([PSFramework.Configuration.ConfigurationHost]::Configurations.ContainsKey($item))
                        {
                            Write-Config -Config ([PSFramework.Configuration.ConfigurationHost]::Configurations[$item]) -Scope $Scope -EnableException $EnableException
                        }
                    }
                }
                "Name"
                {
                    foreach ($item in ([PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name))
                    {
                        Write-Config -Config $item -Scope $Scope -EnableException $EnableException
                    }
                }
            }
        }
        #endregion Registry Based
        
        #region File Based
        else
        {
            switch ($PSCmdlet.ParameterSetName)
            {
                "Default"
                {
                    foreach ($item in $Config)
                    {
                        if ($fileConfigurationItems.FullName -notcontains $item.FullName) { $fileConfigurationItems += $item }
                    }
                    
                    foreach ($item in $FullName)
                    {
                        if (($fileConfigurationItems.FullName -notcontains $item) -and ([PSFramework.Configuration.ConfigurationHost]::Configurations.ContainsKey($item)))
                        {
                            $fileConfigurationItems += [PSFramework.Configuration.ConfigurationHost]::Configurations[$item]
                        }
                    }
                }
                "Name"
                {
                    foreach ($item in ([PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name))
                    {
                        if ($fileConfigurationItems.FullName -notcontains $item.FullName) { $fileConfigurationItems += $item }
                    }
                }
            }
        }
        #endregion File Based
    }
    end
    {
        #region Finish File Based Persistence
        if ($Scope -band 16)
        {
            Write-PsfConfigFile -Config $fileConfigurationItems -Path (Join-Path $script:path_FileUserLocal "psf_config.json")
        }
        if ($Scope -band 32)
        {
            Write-PsfConfigFile -Config $fileConfigurationItems -Path (Join-Path $script:path_FileUserShared "psf_config.json")
        }
        if ($Scope -band 64)
        {
            Write-PsfConfigFile -Config $fileConfigurationItems -Path (Join-Path $script:path_FileSystem "psf_config.json")
        }
        #endregion Finish File Based Persistence
    }
}

function Register-PSFConfigSchema
{
<#
    .SYNOPSIS
        Register new schemas for ingersting configuration data.
     
    .DESCRIPTION
        Register new schemas for ingersting configuration data.
        This can be used to dynamically extend the configuration system and add new file types as supported input.
     
    .PARAMETER Name
        The name of the Schema to register.
     
    .PARAMETER Schema
        The Schema Code to register.
     
    .EXAMPLE
        PS C:\> Register-PSFConfigSchema -Name Default -Schema $scriptblock
     
        Registers the scriptblock stored in $scriptblock under 'Default'
#>

    [CmdletBinding()]
    Param (
        [string]
        $Name,
        
        [ScriptBlock]
        $Schema
    )
    
    process
    {
        [PSFramework.Configuration.ConfigurationHost]::Schemata[$Name] = $Schema
    }
}

function Register-PSFConfigValidation
{
    <#
        .SYNOPSIS
            Registers a validation scriptblock for use with the configuration system.
         
        .DESCRIPTION
            Registers a validation scriptblock for use with the configuration system.
     
            The scriptblock must be designed according to a few guidelines:
            - It must not throw exceptions
            - It must accept a single parameter (the value to be tested)
            - It must return an object with two properties: 'Message', 'Value' and 'Success'.
            The Success property should be boolean and indicate whether the value is valid.
            The Value property contains the validated input. The scriptblock may legally convert the input (For example from string to int in case of integer validation)
            The message contains a string that will be passed along to an exception in case the input is NOT valid.
         
        .PARAMETER Name
            The name under which to register the validation scriptblock
         
        .PARAMETER ScriptBlock
            The scriptblock to register
         
        .EXAMPLE
            PS C:\> Register-PSFConfigValidation -Name IntPositive -ScriptBlock $scriptblock
     
            Registers the scriptblock stored in $scriptblock as validation with the name IntPositive
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFConfigValidation')]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [ScriptBlock]
        $ScriptBlock
    )
    process
    {
        [PSFramework.Configuration.ConfigurationHost]::Validation[$Name] = $ScriptBlock
    }
}

function Remove-PSFConfig
{
<#
    .SYNOPSIS
        Removes configuration items from memory.
     
    .DESCRIPTION
        This command removes configuration items from memory.
        However, not all settings can just be deleted!
        A configuration item must be flagged as deletable.
        This can be done using Set-PSFConfig -AllowDelete or Import-PSFConfig -AllowDelete.
        Certain schema versions of configuration json may also support defining this in the file.
     
        Limitations to flagging configuration as deletable:
        > Once a configuration item has been initialized, its deletable status is frozen.
          The last time it is possible to change the deletable status is during initialization.
        > A setting that has been set as mandated by policy cannot be removed.
     
        Reason for this limit:
        The configuration system is designed for multiple scenarios.
        Deleting settings makes sense in some, while in others it is actually detrimental.
        Initialization is especially designed for the module scenario, where the module's configuration is its options menu.
        In this scenario, having a user deleting settings could lead to broken execution and unintended code paths, that might be at odds with policies defined.
     
    .PARAMETER Config
        The configuration object to remove from memory.
        Can be retrieved using Get-PSFConfig.
     
    .PARAMETER FullName
        The full name of the setting to be removed from memory.
     
    .PARAMETER Module
        The name of the module, whose settings should be removed from memory.
     
    .PARAMETER Name
        Default: "*"
        Used in conjunction with the -Module parameter to restrict the number of configuration items deleted from memory.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Remove-PSFConfig -FullName 'Phase1.Step1.Server' -Confirm:$false
     
        Deletes the setting 'Phase1.Step1.Server' from memory, assuming it exists and supports deletion.
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param (
        [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)]
        [PSFramework.Configuration.Config[]]
        $Config,
        
        [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)]
        [string[]]
        $FullName,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)]
        [string]
        $Module,
        
        [Parameter(ParameterSetName = "Name", Position = 1)]
        [string]
        $Name = "*"
    )
    
    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            "Default"
            {
                #region Try removing all items specified
                foreach ($item in $Config)
                {
                    if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $item.FullName)) { continue }
                    try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($item.FullName) }
                    catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $item.FullName -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ }
                    
                    if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName }
                    else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced }
                }
                # Since configuration items will also bind to string, if any were included, break the switch
                if (Test-PSFParameterBinding -ParameterName Config) { break }
                #endregion Try removing all items specified
                
                #region Try removing all full names specified
                foreach ($nameItem in $FullName)
                {
                    if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $nameItem)) { continue }
                    $item = Get-PSFConfig -FullName $nameItem
                    
                    try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($nameItem) }
                    catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $nameItem -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ }
                    
                    
                    if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName }
                    else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced }
                }
                #endregion Try removing all full names specified
            }
            "Name"
            {
                #region Try removing by filter
                foreach ($item in (Get-PSFConfig -Module $Module -Name $Name))
                {
                    if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $item.FullName)) { continue }
                    
                    try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($item.FullName) }
                    catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $item.FullName -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ }
                    
                    if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName }
                    else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced }
                }
                #endregion Try removing by filter
            }
        }
    }
}

function Reset-PSFConfig
{
<#
    .SYNOPSIS
        Reverts a configuration item to its default value.
     
    .DESCRIPTION
        This command can be used to revert a configuration item to the value it was initialized with.
        Generally, this amounts to reverting it to its default value.
         
        In order for a reset to be possible, two conditions must be met:
        - The setting must have been initialized.
        - The setting cannot have been enforced by policy.
     
    .PARAMETER ConfigurationItem
        A configuration object as returned by Get-PSFConfig.
     
    .PARAMETER FullName
        The full name of the setting to reset, offering the maximum of precision.
     
    .PARAMETER Module
        The name of the module, from which configurations should be reset.
        Used in conjunction with the -Name parameter to filter a specific set of items.
     
    .PARAMETER Name
        Used in conjunction with the -Module parameter to select which settings to reset using wildcard comparison.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Reset-PSFConfig -Module MyModule
     
        Resets all configuration items of the MyModule to default.
     
    .EXAMPLE
        PS C:\> Get-PSFConfig | Reset-PSFConfig
     
        Resets ALL configuration items to default.
     
    .EXAMPLE
        PS C:\> Reset-PSFConfig -FullName MyModule.Group.Setting1
     
        Resets the configuration item named 'MyModule.Group.Setting1'.
#>

    [CmdletBinding(DefaultParameterSetName = 'Pipeline', SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Reset-PSFConfig')]
    param (
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
        [PSFramework.Configuration.Config[]]
        $ConfigurationItem,
        
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
        [string[]]
        $FullName,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Module')]
        [string]
        $Module,
        
        [Parameter(ParameterSetName = 'Module')]
        [string]
        $Name = "*",
        
        [switch]
        $EnableException
    )
    
    process
    {
        #region By configuration Item
        foreach ($item in $ConfigurationItem)
        {
            if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -ActionString 'Reset-PSFConfig.Resetting')
            {
                try { $item.ResetValue() }
                catch { Stop-PSFFunction -String 'Reset-PSFConfig.Resetting.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue -EnableException $EnableException }
            }
        }
        #endregion By configuration Item
        
        #region By FullName
        foreach ($nameItem in $FullName)
        {
            # The configuration items themselves can be cast to string, so they need to be filtered out,
            # otherwise on bind they would execute for this code-path as well.
            if ($nameItem -ceq "PSFramework.Configuration.Config") { continue }
            
            foreach ($item in (Get-PSFConfig -FullName $nameItem))
            {
                if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -ActionString 'Reset-PSFConfig.Resetting')
                {
                    try { $item.ResetValue() }
                    catch { Stop-PSFFunction -String 'Reset-PSFConfig.Resetting.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue -EnableException $EnableException}
                }
            }
        }
        #endregion By FullName
        if ($Module)
        {
            foreach ($item in (Get-PSFConfig -Module $Module -Name $Name))
            {
                if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -ActionString 'Reset-PSFConfig.Resetting')
                {
                    try { $item.ResetValue() }
                    catch { Stop-PSFFunction -String 'Reset-PSFConfig.Resetting.Failed' -ErrorRecord $_ -Cmdlet $PSCmdlet -EnableException $EnableException -Continue }
                }
            }
        }
    }
}

function Select-PSFConfig
{
<#
    .SYNOPSIS
        Select a subset of configuration entries and return them as objects.
     
    .DESCRIPTION
        Select a subset of configuration entries and return them as objects.
         
        This can be used to retrieve related configuration entries as a single PowerShell object.
         
        For example, assuming there are the following configuration entries:
         
        LoggingProvider.LogFile.AutoInstall
        LoggingProvider.LogFile.Enabled
        LoggingProvider.LogFile.ExcludeModules
        LoggingProvider.LogFile.ExcludeTags
        LoggingProvider.LogFile.IncludeModules
        LoggingProvider.LogFile.IncludeTags
        LoggingProvider.LogFile.InstallOptional
         
        Then this line:
        Select-PSFConfig 'LoggingProvider.LogFile.*'
         
        Will return a PSCustomObject that looks similar to this:
         
        _Name : LogFile
        _FullName : LoggingProvider.LogFile
        _Depth : 1
        _Children : {}
        AutoInstall : False
        Enabled : False
        ExcludeModules : {}
        ExcludeTags : {}
        IncludeModules : {}
        IncludeTags : {}
        InstallOptional : True
         
        This selection is recursive:
        It will group on each part of the FullName of the selected configuration entries.
        - Entries that only have children and no straight values (In the example above, that would be the first, the "LoggingProvider" node) will not be included and only return children.
        - Entries with values AND children, will have child entries included in the _Children property.
        - Even child entries of Entries with values will be returned
     
    .PARAMETER FullName
        String filter to select, which configuration entries to select on.
        Use the same value on Get-PSFConfig to see what configuration entries will be processed.
     
    .PARAMETER Depth
        Only entries at the specified depth level will be returned.
        Depth starts at "0"
        In the name 'LoggingProvider.LogFile.AutoInstall' ...
     
        - "LoggingProvider" would be depth 0
        - "LogFile" would be depth 1
        - ...
     
    .EXAMPLE
        PS C:\> Select-PSFConfig 'LoggingProvider.LogFile.*'
     
        Selects all configuration settings under 'LoggingProvider.LogFile.*', grouping the value ends as PSObject.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [Alias('Name')]
        [string]
        $FullName,
        
        [int[]]
        $Depth
    )
    
    begin
    {
        function Group-Config
        {
            [CmdletBinding()]
            param (
                $Config,
                
                [int]
                $Depth
            )
            
            $grouped = $Config | Group-Object { $_.FullName.Split('.')[$Depth] }
            foreach ($group in $grouped)
            {
                if (-not $group.Name) { continue }
                $data = [ordered]@{
                    _Name = $group.Name
                    _FullName = $group.Group[0].FullName.Split('.')[0..($Depth)] -join "."
                    _Depth = $Depth
                    _Children = @()
                }
                if ($subGroups = $group.Group | Where-Object { $_.FullName.Split(".").Count -gt ($Depth + 2) })
                {
                    $data._Children = Group-Config -Config $subGroups -Depth ($Depth + 1)
                    $data._Children
                }
                
                foreach ($cfgItem in ($group.Group | Where-Object { $_.FullName.Split(".").Count -eq ($Depth + 2) }))
                {
                    $cfgName = $cfgItem.FullName -replace "^([^\.]+\.){0,$($Depth + 1)}"
                    $data[$cfgName] = $cfgItem.Value
                }
                if ($data.Keys.Count -gt 4) { [PSCustomObject]$data }
            }
        }
    }
    process
    {
        $configItems = Get-PSFConfig -FullName $FullName
        Group-Config -Config $configItems -Depth 0 | ForEach-Object {
            if (-not $Depth) { return $_ }
            if ($_._Depth -in $Depth) { $_ }
        }
    }
}

function Unregister-PSFConfig
{
<#
    .SYNOPSIS
        Removes registered configuration settings.
     
    .DESCRIPTION
        Removes registered configuration settings.
        This function can be used to remove settings that have been persisted for either user or computer.
     
        Note: This command has no effect on configuration setings currently in memory.
     
    .PARAMETER ConfigurationItem
        A configuration object as returned by Get-PSFConfig.
     
    .PARAMETER FullName
        The full name of the configuration setting to purge.
     
    .PARAMETER Module
        The module, amongst which settings should be unregistered.
     
    .PARAMETER Name
        The name of the setting to unregister.
        For use together with the module parameter, to limit the amount of settings that are unregistered.
     
    .PARAMETER Scope
        Settings can be set to either default or enforced, for user or the entire computer.
        By default, only DefaultSettings for the user are unregistered.
        Use this parameter to choose the actual scope for the command to process.
     
    .EXAMPLE
        PS C:\> Get-PSFConfig | Unregister-PSFConfig
     
        Completely removes all registered configurations currently loaded in memory.
        In most cases, this will mean removing all registered configurations.
     
    .EXAMPLE
        PS C:\> Unregister-PSFConfig -Scope SystemDefault -FullName 'MyModule.Path.DefaultExport'
     
        Unregisters the setting 'MyModule.Path.DefaultExport' from the list of computer-wide defaults.
        Note: Changing system wide settings requires running the console with elevation.
     
    .EXAMPLE
        PS C:\> Unregister-PSFConfig -Module MyModule
     
        Unregisters all configuration settings for the module MyModule.
#>

    [CmdletBinding(DefaultParameterSetName = 'Pipeline', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Unregister-PSFConfig')]
    param (
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
        [PSFramework.Configuration.Config[]]
        $ConfigurationItem,
        
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
        [string[]]
        $FullName,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Module')]
        [string]
        $Module,
        
        [Parameter(ParameterSetName = 'Module')]
        [string]
        $Name = "*",
        
        [PSFramework.Configuration.ConfigScope]
        $Scope = "UserDefault"
    )
    
    begin
    {
        if ($script:NoRegistry -and ($Scope -band 10))
        {
            Stop-PSFFunction -String 'Unregister-PSFConfig.NoRegistry' -Tag 'NotSupported' -Category ResourceUnavailable
            return
        }
        
        # Linux and MAC default to local user store file
        if ($script:NoRegistry -and ($Scope -eq "UserDefault"))
        {
            $Scope = [PSFramework.Configuration.ConfigScope]::FileUserLocal
        }
        # Linux and MAC get redirection for SystemDefault to FileSystem
        if ($script:NoRegistry -and ($Scope -eq "SystemDefault"))
        {
            $Scope = [PSFramework.Configuration.ConfigScope]::FileSystem
        }
        
        #region Initialize Collection
        $registryProperties = @()
        if ($Scope -band 1)
        {
            if (Test-Path $script:path_RegistryUserDefault) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryUserDefault }
        }
        if ($Scope -band 2)
        {
            if (Test-Path $script:path_RegistryUserEnforced) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryUserEnforced }
        }
        if ($Scope -band 4)
        {
            if (Test-Path $script:path_RegistryMachineDefault) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryMachineDefault }
        }
        if ($Scope -band 8)
        {
            if (Test-Path $script:path_RegistryMachineEnforced) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryMachineEnforced }
        }
        $pathProperties = @()
        if ($Scope -band 16)
        {
            $fileUserLocalSettings = @()
            if (Test-Path (Join-Path $script:path_FileUserLocal "psf_config.json")) { $fileUserLocalSettings = Get-Content (Join-Path $script:path_FileUserLocal "psf_config.json") -Encoding UTF8 | ConvertFrom-Json }
            if ($fileUserLocalSettings)
            {
                $pathProperties += [pscustomobject]@{
                    Path       = (Join-Path $script:path_FileUserLocal "psf_config.json")
                    Properties = $fileUserLocalSettings
                    Changed    = $false
                }
            }
        }
        if ($Scope -band 32)
        {
            $fileUserSharedSettings = @()
            if (Test-Path (Join-Path $script:path_FileUserShared "psf_config.json")) { $fileUserSharedSettings = Get-Content (Join-Path $script:path_FileUserShared "psf_config.json") -Encoding UTF8 | ConvertFrom-Json }
            if ($fileUserSharedSettings)
            {
                $pathProperties += [pscustomobject]@{
                    Path       = (Join-Path $script:path_FileUserShared "psf_config.json")
                    Properties = $fileUserSharedSettings
                    Changed    = $false
                }
            }
        }
        if ($Scope -band 64)
        {
            $fileSystemSettings = @()
            if (Test-Path (Join-Path $script:path_FileSystem "psf_config.json")) { $fileSystemSettings = Get-Content (Join-Path $script:path_FileSystem "psf_config.json") -Encoding UTF8 | ConvertFrom-Json }
            if ($fileSystemSettings)
            {
                $pathProperties += [pscustomobject]@{
                    Path       = (Join-Path $script:path_FileSystem "psf_config.json")
                    Properties = $fileSystemSettings
                    Changed    = $false
                }
            }
        }
        #endregion Initialize Collection
        
        $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider'
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        # Silently skip since no action necessary
        if (-not ($pathProperties -or $registryProperties)) { return }
        
        foreach ($item in $ConfigurationItem)
        {
            # Registry
            foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -eq $item.FullName }))
            {
                Remove-ItemProperty -Path $hive.PSPath -Name $item.FullName
            }
            # Prepare file
            foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -contains $item.FullName }))
            {
                $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NE $item.FullName
                $fileConfig.Changed = $true
            }
        }
        
        foreach ($item in $FullName)
        {
            # Ignore string-casted configurations
            if ($item -ceq "PSFramework.Configuration.Config") { continue }
            
            # Registry
            foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -eq $item }))
            {
                Remove-ItemProperty -Path $hive.PSPath -Name $item
            }
            # Prepare file
            foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -contains $item }))
            {
                $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NE $item
                $fileConfig.Changed = $true
            }
        }
        
        if ($Module)
        {
            $compoundName = "{0}.{1}" -f $Module, $Name
            
            # Registry
            foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -like $compoundName }))
            {
                foreach ($propName in $hive.PSObject.Properties.Name)
                {
                    if ($propName -in $common) { continue }
                    
                    if ($propName -like $compoundName)
                    {
                        Remove-ItemProperty -Path $hive.PSPath -Name $propName
                    }
                }
            }
            # Prepare file
            foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -like $compoundName }))
            {
                $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NotLike $compoundName
                $fileConfig.Changed = $true
            }
        }
    }
    end
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        foreach ($fileConfig in $pathProperties)
        {
            if (-not $fileConfig.Changed) { continue }
            
            if ($fileConfig.Properties)
            {
                $fileConfig.Properties | ConvertTo-Json | Set-Content -Path $fileConfig.Path -Encoding UTF8
            }
            else
            {
                Remove-Item $fileConfig.Path
            }
        }
    }
}


function Get-PSFFeature
{
<#
    .SYNOPSIS
        Returns a list of all registered features.
     
    .DESCRIPTION
        Returns a list of all registered features.
     
    .PARAMETER Name
        The name to filter by.
     
    .EXAMPLE
        PS C:\> Get-PSFFeature
     
        Returns all features registered.
#>

    [CmdletBinding()]
    param (
        [string]
        $Name = "*"
    )
    
    process
    {
        [PSFramework.Feature.FeatureHost]::Features.Values | Where-Object Name -Like $Name
    }
}

function Register-PSFFeature
{
<#
    .SYNOPSIS
        Registers a feature for use in the PSFramework Feature Flag System.
     
    .DESCRIPTION
        Registers a feature for use in the PSFramework Feature Flag System.
        This allows offering a common interface for enabling and disabling features on-demand.
        Typical use-cases:
        - Experimental Features
        - Reverting breaking behavior on a per-module basis.
     
    .PARAMETER Name
        The name of the feature to register.
        Feature names are scoped globally, so please prefix by your own module's name.
     
    .PARAMETER Description
        A description of the feature, so users can discover what it is about.
     
    .PARAMETER NotGlobal
        Disables global flags for this feature.
        By default, features can be enabled or disabled on a global scope.
     
    .PARAMETER NotModuleSpecific
        Disables module specific feature flags.
        By default, individual modules can override the global settings either way.
        This may not really be applicable for all features however.
     
    .PARAMETER Owner
        The name of the module owning the feature.
        Autodiscovery is attempted, but it is recommended to explicitly specify the owning module's name.
     
    .EXAMPLE
        PS C:\> Register-PSFFeature -Name 'MyModule.DividebyZeroExp' -Description 'Attempt to divide by zero' -Owner MyModule
     
        Registers the feature under its owning module and adds a nice description.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [string]
        $Description,
        
        [switch]
        $NotGlobal,
        
        [switch]
        $NotModuleSpecific,
        
        [string]
        $Owner = (Get-PSCallStack)[1].InvocationInfo.MyCommand.ModuleName
    )
    
    begin
    {
        $featureObject = New-Object PSFramework.Feature.FeatureItem -Property @{
            Name = $Name
            Owner = $Owner
            Global = (-not $NotGlobal)
            ModuleSpecific = (-not $NotModuleSpecific)
            Description = $Description
        }
    }
    process
    {
        [PSFramework.Feature.FeatureHost]::Features[$Name] = $featureObject
    }
}

function Set-PSFFeature
{
<#
    .SYNOPSIS
        Toggles a feature on or off.
     
    .DESCRIPTION
        Toggles a feature on or off.
        This controls the flags for optional features a module might offer.
     
        Features can be controlled globally or specific to a module that tries to consume it.
        Module specific settings can override global settings, if a feature supports both global and module flags.
     
    .PARAMETER Name
        The name of the feature to set.
     
    .PARAMETER Value
        The value to set it to.
     
    .PARAMETER ModuleName
        The module it should apply to.
        Specifying this parameter sets the flag only for the module specified.
     
    .EXAMPLE
        PS C:\> Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName SPReplicator
     
        This sets the flag for the Enable Exception Inheritance Name to $true, but only applies to the module SPReplicator.
     
    .EXAMPLE
        PS C:\> Set-PSFFeature -Name 'MyModule.Feierabend' -Value $true
     
        This enables the global flag for the MyModule.Feierabend feature.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        #[PsfValidateSet(TabCompletion = 'PSFramework.Feature.Name')]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [bool]
        $Value,
        
        [string]
        $ModuleName
    )
    process
    {
        foreach ($featureItem in $Name)
        {
            if ($ModuleName)
            {
                [PSFramework.Feature.FeatureHost]::WriteModuleFlag($ModuleName, $Name, $Value)
            }
            else
            {
                [PSFramework.Feature.FeatureHost]::WriteGlobalFlag($Name, $Value)
            }
        }
    }
}

function Test-PSFFeature
{
<#
    .SYNOPSIS
        Tests whether a given feature has been enabled.
     
    .DESCRIPTION
        Tests whether a given feature has been enabled.
        Use this within the feature-owning module to determine, whether a feature should be enabled or not.
     
    .PARAMETER Name
        The feature to test for.
     
    .PARAMETER ModuleName
        The name of the module that seeks to use the feature.
        Must be specified in order to determine module-specific flags.
     
    .EXAMPLE
        PS C:\> Test-PSFFeature -Name PSFramework.InheritEnableException -ModuleName SPReplicator
     
        Tests whether the module SPReplicator has enabled the Enable Exception Inheritance feature.
#>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        #[PsfValidateSet(TabCompletion = 'PSFramework.Feature.Name')]
        [parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [string]
        $ModuleName
    )
    
    begin
    {
        $featureItem = Get-PSFFeature -Name $Name
    }
    process
    {
        if (-not $featureItem.Global) { [PSFramework.Feature.FeatureHost]::ReadModuleFlag($Name, $ModuleName) }
        else { [PSFramework.Feature.FeatureHost]::ReadFlag($Name, $ModuleName) }
    }
}

function Add-PSFFilterCondition
{
<#
    .SYNOPSIS
        Add a filter Condition to a Condition Set.
     
    .DESCRIPTION
        Add a filter Condition to a Condition Set.
     
    .PARAMETER ConditionSet
        The Condition Set object to add to.
     
    .PARAMETER SetModule
        Module of the Condition Set to which to add to.
     
    .PARAMETER SetName
        Name of the Condition Set to which to add to.
     
    .PARAMETER Condition
        The condition object to add to the Condition Set.
     
    .PARAMETER Name
        Name of the Condition to add to the Condition Set.
     
    .PARAMETER Module
        Name of the Module the Condition being added comes from.
        Defaults to the Module of the Condition Set if not specified
     
    .EXAMPLE
        PS C:\> Add-PSFFilterCondition -ConditionSet $barFilter -Name 'FromCellar'
         
        Adds the Condition "FromCellar" to the Condition Set stored in $barFilter.
        The Condition is chosen from the same module as the the Condition Set.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ParameterSetName = 'ObjectObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ObjectName')]
        [PSFramework.Filter.ConditionSet]
        $ConditionSet,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NameObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'NameName')]
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $SetModule,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NameObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'NameName')]
        [PsfArgumentCompleter('PSFramework.Filter.SetName')]
        [string]
        $SetName,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ObjectObject')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'NameObject')]
        [PSFramework.Filter.Condition[]]
        $Condition,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'ObjectName')]
        [Parameter(Mandatory = $true, ParameterSetName = 'NameName')]
        [PsfArgumentCompleter('PSFramework.Filter.Name')]
        [string[]]
        $Name,
        
        [Parameter(ParameterSetName = 'ObjectName')]
        [Parameter(ParameterSetName = 'NameName')]
        [PsfArgumentCompleter('PSFramework.Filter.Module')]
        [string]
        $Module
    )
    
    begin {
        $conditionSetObject = Resolve-PsfFilterConditionSet -ConditionSet $ConditionSet -SetModule $SetModule -SetName $SetName -Cmdlet $PSCmdlet
    }
    process    {
        $moduleName = $Module
        if (-not $moduleName) { $moduleName = $conditionSetObject.Module }
        
        foreach ($conditionName in $Name) {
            foreach ($conditionObject in Get-PSFFilterCondition -Module $moduleName -Name $conditionName) {
                $conditionSetObject.Add($conditionObject)
            }
        }
    }
}

function Get-PSFFilterCondition {
<#
    .SYNOPSIS
        Retrieve defined filter conditions.
     
    .DESCRIPTION
        Retrieve defined filter conditions.
        Filter conditions can be used as part of a condition set, used to evaluate filter expressions defined with New-PSFFilter.
     
    .PARAMETER Module
        The module to filter by.
        Defaults to '*'
     
    .PARAMETER Name
        The name of the condition to retrieve.
        Defaults to '*'
     
    .PARAMETER SetName
        The name of the condition set the condition is assigned to.
        Allows searching by assignment.
     
    .PARAMETER Version
        Retrieve a specific version of the filter condition.
        By default, the latest version only is returned.
     
    .PARAMETER AllVersions
        Retrieve all versions of a given filter condition.
     
    .EXAMPLE
        PS C:\> Get-PSFFilterCondition
     
        List all defined filter conditions.
     
    .EXAMPLE
        PS C:\> Get-PSFFilterCondition -Module PSFramework -Name OSWindows
     
        Returns the filter condition "OSWindows" from the module PSFramework.
#>

    [OutputType([PSFramework.Filter.Condition])]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [PsfArgumentCompleter('PSFramework.Filter.Module')]
        [string]
        $Module = '*',
        
        [PsfArgumentCompleter('PSFramework.Filter.Name')]
        [string]
        $Name = '*',
        
        [PsfArgumentCompleter('PSFramework.Filter.SetName')]
        [string]
        $SetName,
        
        [Parameter(ParameterSetName = 'Version')]
        [System.Version]
        $Version,
        
        [Parameter(ParameterSetName = 'AllVersion')]
        [switch]
        $AllVersions
    )
    
    process {
        if ($SetName) {
            Get-PSFFilterConditionSet -Module $Module -Name $SetName | ForEach-Object {
                $_.ConditionTable.Values | Where-Object Name -Like $Name
            }
        }
        else {
            $script:filterContainer.FindCondition($Module, $Name, $Version, $AllVersions)
        }
    }
}

function Get-PSFFilterConditionSet {
<#
    .SYNOPSIS
        Retrieve defined filter condition sets.
     
    .DESCRIPTION
        Retrieve defined filter condition sets.
        Filter condition sets are a grouped set of conditions used in filter expressions.
        Create a filter expression by using New-PSFFilter.
     
    .PARAMETER Module
        The module to filter by.
        Defaults to '*'
     
    .PARAMETER Name
        The name of the condition set to retrieve.
        Defaults to '*'
     
    .PARAMETER Version
        Retrieve a specific version of the filter condition set.
        By default, the latest version only is returned.
     
    .PARAMETER AllVersions
        Retrieve all versions of a given filter condition set.
     
    .EXAMPLE
        PS C:\> Get-PSFFilterConditionSet
     
        List all defined filter condition sets.
     
    .EXAMPLE
        PS C:\> Get-PSFFilterConditionSet -Module PSFramework -Name Environment
     
        Returns the filter condition set "Environment" from the module PSFramework.
#>

    [OutputType([PSFramework.Filter.ConditionSet])]
    [CmdletBinding()]
    param (
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $Module = '*',
        
        [PsfArgumentCompleter('PSFramework.Filter.SetName')]
        [string]
        $Name = '*',
        
        [System.Version]
        $Version,
        
        [switch]
        $AllVersions
    )
    
    process {
        $script:filterContainer.FindConditionSet($Module, $Name, $Version, $AllVersions)
    }
}

function Invoke-PSFFilter {
<#
    .SYNOPSIS
        Evaluate a filter.
     
    .DESCRIPTION
        Evaluate a filter.
        Can either run all by itself - for example to evaluate the runtime environment - or be given an argument to evaluate it against the expression.
     
        Use ...
        - New-PSFFilter to customize a filter
        - New-PSFFilterCondition to architect your own conditions for use in expressions.
        - New-PSFFilterConditionSet to tie conditions together into a set
        Each filter must have a condition set assigned to be able to run (as otherwise it will not know which conditions are available).
     
        This function exists mostly for discoverability.
        Filter objects returned by New-PSFFilter can be invoked directly using their .Evaluate() method, providing far greater performance at scale.
     
    .PARAMETER Expression
        The filter expression to execute.
        Can only consist of:
        - Names of conditions: Words (may contain letters, numbers and underscore, but no dash)
        - Parenthesis
        - Logical operators (-or, -and, -not, -xor)
     
    .PARAMETER ArgumentList
        Any argument to specify as input to the filter expression.
        All input is passed as one item, to evaluate against multiple items separately, loop the entire command.
        Note: To avoid incurring the overhead for large datasets, filter objects returned by New-PSFFilter can be invoked directly using the .Evaluate() method which also accepts arguments.
     
    .PARAMETER Filter
        The filter object to invoke.
        Objects of this type can be created by New-PSFFilter.
     
    .PARAMETER ConditionSet
        A full Condition Set object as returned by Get-PSFConditionSet.
     
    .PARAMETER SetModule
        Name of the module in which to search for a Condition Set.
     
    .PARAMETER SetName
        Name of the Condition Set to use.
     
    .EXAMPLE
        PS C:\> Invoke-PSFFilter -Expression 'OSWindows -or EnvAzurePipelines' -SetModule PSFramework -SetName Environment
     
        Executes the specified filter expression using the Environment ConditionSet from the PSFramework module.
        Will return true if executed either on Windows or from within Azure DevOps Pipelines
#>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'ExpressionObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ExpressionName')]
        [string]
        $Expression,
        
        $ArgumentList,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Filter')]
        [PSFramework.Filter.Expression]
        $Filter,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'ExpressionObject')]
        [Parameter(ParameterSetName = 'Filter')]
        [PSFramework.Filter.ConditionSet]
        $ConditionSet,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'ExpressionName')]
        [Parameter(ParameterSetName = 'Filter')]
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $SetModule,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'ExpressionName')]
        [Parameter(ParameterSetName = 'Filter')]
        [PsfArgumentCompleter('PSFramework.Filter.SetName')]
        [string]
        $SetName
    )
    
    begin {
        if ($SetModule -and -not $SetName) {
            Invoke-PsfTerminatingException -Message 'Cannot specify -SetModule without -SetName' -ErrorId 'InvalidArgument' -Category InvalidArgument -Cmdlet $PSCmdlet
        }
        if (-not $SetModule -and $SetName) {
            Invoke-PsfTerminatingException -Message 'Cannot specify -SetName without -SetModule' -ErrorId 'InvalidArgument' -Category InvalidArgument -Cmdlet $PSCmdlet
        }
    }
    process {
        $filterObject = $null
        if ($Filter) {
            $filterObject = $Filter.Clone()
            if ($ConditionSet) { $filterObject.ConditionSet = $ConditionSet }
            if ($SetModule -and $SetName) {
                $filterObject.ConditionSet = $script:filterContainer.GetConditionSet($SetModule, $SetName)
            }
        }
        if ($Expression) {
            $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Expression, ConditionSet, SetModule, SetName
            try { $filterObject = New-PSFFilter @parameters }
            catch { throw }
        }
        if (-not $filterObject.ConditionSet) {
            Invoke-PsfTerminatingException -Message 'Filters must have a Condition Set in order to evaluate them!' -ErrorId 'NoConditionSet' -Category InvalidArgument -TargetObject $filterObject -Cmdlet $PSCmdlet
        }
        
        # Can only fail if the condition scriptblocks are written badly
        try { $filterObject.Evaluate($ArgumentList) }
        catch { throw }
    }
}

function New-PSFFilter
{
<#
    .SYNOPSIS
        Creates a new filter object.
     
    .DESCRIPTION
        Creates a new filter object based off the specified expression.
        Optionally, an associated condition set can be specified, either as object or by name and module.
        Conditions Set contain the implementation of the condition logic, that will be used when evaluating the filter expression.
     
        These filter objects can then be used to execute validation either statically or against input objects.
        Combined with filter conditions and condition sets, filters allow defining a custom filter syntax that is easy to write humanly readable filter expressions for without exposing direct code execution.
        This makes filter expressions suitable for data regions and makes conditions easily reusable.
     
    .PARAMETER Expression
        The expression to build a filter object for.
        Can only consist of:
        - Names of conditions: Words (may contain letters, numbers and underscore, but no dash)
        - Parenthesis
        - Logical operators (-or, -and, -not, -xor)
     
    .PARAMETER ConditionSet
        A condition set object to attach to the filter.
     
    .PARAMETER SetModule
        The name of the module from which to select the condition set to attach to the filter.
     
    .PARAMETER SetName
        The name of the condition set to attach to the filter.
     
    .EXAMPLE
        PS C:\> New-PSFFilter -Expression 'OSWindows -or EnvAzurePipelines'
     
        Creates a filter object around the "OSWindows -or EnvAzurePipelines" expression.
     
    .EXAMPLE
        PS C:\> New-PSFFilter -Expression 'OSWindows -or EnvAzurePipelines' -SetModule PSFramework -SetName Environment
     
        Creates a filter object around the "OSWindows -or EnvAzurePipelines" expression.
        It then attaches the latest version of the Environment ConditionSet from within the PSFramework module.
#>

    [OutputType([PSFramework.Filter.Expression])]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Expression,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Object')]
        [PSFramework.Filter.ConditionSet]
        $ConditionSet,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $SetModule,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [PsfArgumentCompleter('PSFramework.Filter.SetName')]
        [string]
        $SetName
    )
    
    process {
        $conditionSetObject = $null
        if ($ConditionSet -or $SetName) {
            $conditionSetObject = Resolve-PsfFilterConditionSet -ConditionSet $ConditionSet -SetModule $SetModule -SetName $SetName -Cmdlet $PSCmdlet
        }
        
        try { New-Object PSFramework.Filter.Expression($Expression, $conditionSetObject) }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }
}

function New-PSFFilterCondition {
<#
    .SYNOPSIS
        Create a new filter condition used in filter expressions.
     
    .DESCRIPTION
        Create a new filter condition used in filter expressions.
        A filter condition is a named scriptblock that is designed evaluates either the environment or an input object.
        It should never throw an exception and instead return $true or $false.
         
        Using a filter expression then allows the user to dynamically build a truth statement by combining any number of conditions through boolean operators.
     
    .PARAMETER Module
        The module that owns/defines the condition.
        Use your own module's name to avoid conflicting with foreign modules.
     
    .PARAMETER Name
        The name of the condition.
        Can only contain letters, numbers and underscore.
        A name is unique within a module.
     
    .PARAMETER ScriptBlock
        The scriptblock that is used to execute the condition.
        This scriptblock is bound to the context that defined it and thus runspace specific.
        This means, if you define it within your module, it will have access to module interna, such as private functions.
     
    .PARAMETER Version
        The version number of the condition.
        There can exist multiple versions of a condition at any given time, but only version can exist in a single Condition Set.
        Defaults to 1.0.0
     
    .PARAMETER Type
        What kind of condition is this:
        Static:
        A condition that will not change during the runtime of the process.
        For example the operating system. Static conditions are executed only once on their first run.
     
        Dynamic:
        Dynamic conditions will be executed every single time an expression that uses it is evaluated.
        Note: An expression may use the same condition multiple times, but it will still only be run once.
     
    .EXAMPLE
        PS C:\> New-PSFFilterCondition -Module 'MyModule' -Name 'HasTemp' -ScriptBlock { Test-Path -Path C:\temp }
     
        Creates a condition named "HasTemp" that is part of the MyModule module.
        When executed, it will validate, whether the temp folder exists.
#>

    [OutputType([PSFramework.Filter.Condition])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('PSFramework.Filter.Module')]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('PSFramework.Validate.Filter.ConditionName', ErrorString = 'PSFramework.Validate.Filter.ConditionName')]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [PSFramework.Utility.PsfScriptBlock]
        $ScriptBlock,
        
        [System.Version]
        $Version = '1.0.0',
        
        [PSFramework.Filter.ConditionType]
        $Type = [PSFramework.Filter.ConditionType]::Dynamic
    )
    
    process
    {
        try { $script:filterContainer.AddCondition($Module, $Name, $ScriptBlock, $Version, $Type) }
        catch { throw }
    }
}

function New-PSFFilterConditionSet {
<#
    .SYNOPSIS
        Create a new filter condition set.
     
    .DESCRIPTION
        Create a new filter condition set.
        A filter condition set is a grouping of filter conditions.
        These sets are referenced when creating or invoking a filter expression and are the logic implementation used to evaluate the expression.
     
        Individual filter conditions can be added, and ...
        - Not all Conditions in a set need be used in any given Filter expression using that set.
        - Not all Conditions must be from the same module as the Condition Set.
     
    .PARAMETER Module
        The module that owns the filter condition set.
     
    .PARAMETER Name
        The name of the filter condition set.
        This name is unique within any given module.
     
    .PARAMETER Version
        The version of the filter condition set.
        You can provide multiple versions of a set for backwards compatiblity, however selecting the correct version for your expressions is your own responsibility.
        Defaults to 1.0.0
     
    .PARAMETER Conditions
        The conditions that are part of the Condition Set.
        These are the individual technical implementations used to evaluate a fitler expression.
        Use New-PSFFilterCondition to define these objects or Get-PSFFilterCondition to retrieve already defined objects.
     
    .PARAMETER ScriptBlock
        A Scriptblock that will be executed and all Filter Condition objects returned will become part of the set.
        This allows combining the definition of a Condition Set and its component Conditions into a single call.
     
    .EXAMPLE
        PS C:\> New-PSFFilterConditionSet -Module 'Bartender' -Name 'Alcohols'
     
        Defines a new Condition Set named Alcohols in the module Bartender.
        This set is created empty and must later be filled with Conditions before using it.
     
    .EXAMPLE
        PS C:\> Get-PSFFilterCondition -Module Bartender | New-PSFFilterConditionSet -Module 'Bartender' -Name 'Alcohols'
     
        Defines a new Condition Set named Alcohols in the module Bartender.
        Adds all Conditions defined in the same module to it.
     
    .EXAMPLE
        PS C:\> New-PSFFilterConditionSet -Module 'Bartender' -Name 'Alcohols' -ScriptBlock {
            New-PSFFilterCondition -Module Bartender -Name Beer -ScriptBlock { $_.Type -eq 'Beer' }
            New-PSFFilterCondition -Module Bartender -Name Vodka -ScriptBlock { $_.Type -eq 'Vodka' }
            New-PSFFilterCondition -Module Bartender -Name Whiskey -ScriptBlock { $_.Type -eq 'Whiskey' }
            New-PSFFilterCondition -Module Bartender -Name Rum -ScriptBlock { $_.Type -eq 'Rum' }
        }
     
        Defines a new Condition Set named Alcohols in the module Bartender.
        Adds the four newly created Coditions straight to the Set.
#>

    [OutputType([PSFramework.Filter.ConditionSet])]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [System.Version]
        $Version = '1.0.0',
        
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Objects')]
        [PSFramework.Filter.Condition[]]
        $Conditions,
        
        [Parameter(ParameterSetName = 'Scriptblock')]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )
    
    begin {
        $conditionObjects = [System.Collections.ArrayList]@()
    }
    process {
        if ($Conditions) {
            $conditionObjects.AddRange($Conditions)
        }
        if ($ScriptBlock) {
            try { $results = & $ScriptBlock }
            catch { throw }
            foreach ($result in $results) {
                if ($result -isnot [PSFramework.Filter.Condition]) { continue }
                $null = $conditionObjects.Add($result)
            }
        }
    }
    end {
        $script:filterContainer.AddConditionSet($Module, $Name, $Version, $conditionObjects.ToArray())
    }
}

function Test-PSFFilter {
<#
    .SYNOPSIS
        Tests a filter expression on whether it is valid.
     
    .DESCRIPTION
        Tests a filter expression on whether it is valid.
        Can also validate, that it will work with the specified condition set.
     
    .PARAMETER Expression
        The filter expression to validate.
     
    .PARAMETER ConditionSet
        The condition set object (as returned by Get-PSFFilterConditionSet) to validate against the expression.
     
    .PARAMETER SetModule
        The name of the module from which to pick up the condition set to validate against the expression.
     
    .PARAMETER SetName
        The name of the condition set to validate against the expression.
     
    .EXAMPLE
        PS C:\> Test-PSFFilter -Expression 'OSWindows -or EnvAzurePipelines'
     
        Validates the syntax of the "OSWindows -or EnvAzurePipelines" expression (which is correct).
     
    .EXAMPLE
        PS C:\> Test-PSFFilter -Expression 'OSWindows -or EnvAzurePipelines' -SetModule PSFramework -SetName Environment
     
        Validates the syntax of the "OSWindows -or EnvAzurePipelines" expression (which is correct).
        It then compares it to the latest version of the Environment ConditionSet from within the PSFramework and validates, that it contains the conditions used in the expression ("OSWindows" and "EnvAzurePipelines").
#>

    [OutputType([bool])]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Expression,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Object')]
        [PSFramework.Filter.ConditionSet]
        $ConditionSet,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [PsfArgumentCompleter('PSFramework.Filter.SetModule')]
        [string]
        $SetModule,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $SetName
    )
    
    process {
        $conditionSetObject = $null
        if ($SetModule -or $ConditionSet) {
            $conditionSetObject = Resolve-PsfFilterConditionSet -ConditionSet $ConditionSet -SetModule $SetModule -SetName $SetName -Cmdlet $PSCmdlet
        }
        
        try { $filter = New-Object PSFramework.Filter.Expression($Expression, $conditionSetObject) }
        catch { return $false }
        
        if (-not $conditionSetObject) { return $true }
        
        $success = $true
        foreach ($condition in $filter.Conditions) {
            if ($conditionSetObject.Conditions.Name -contains $condition) { continue }
            Write-PSFMessage -Level Verbose -String 'Test-PSFFilter.Condition.NotInSet' -StringValues $condition -Target $filter
            $success = $false
        }
        return $success
    }
}

function Get-PSFCallback
{
<#
    .SYNOPSIS
        Returns a list of callback scripts.
     
    .DESCRIPTION
        Returns a list of callback scripts.
        Use Register-PSFCallback to register new callback scripts.
        Use Unregister-PSFCallback to remove callback scripts.
        Use Invoke-PSFCallback within a function of your module to execute all registered callback scripts that apply.
     
    .PARAMETER Name
        The name to filter by.
     
    .PARAMETER All
        Return all callback scripts, even those specific to other runspaces.
     
    .EXAMPLE
        PS C:\> Get-PSFCallback
     
        Returns all callback scripts relevant to the current runspace.
     
    .EXAMPLE
        PS C:\> Get-PSFCallback -All
     
        Returns all callback scripts in the entire process.
     
    .EXAMPLE
        PS C:\> Get-PSFCallback -Name MyModule.Configuration
     
        Returns the callback script named 'MyModule.Configuration'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [OutputType([PSFramework.FlowControl.Callback])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name = '*',
        
        [switch]
        $All
    )
    
    process
    {
        foreach ($nameString in $Name)
        {
            [PSFramework.FlowControl.CallbackHost]::Get($nameString, $All.ToBool())
        }
    }
}

function Get-PSFUserChoice
{
<#
    .SYNOPSIS
        Prompts the user to choose between a set of options.
     
    .DESCRIPTION
        Prompts the user to choose between a set of options.
        Returns the index of the choice picked as a number.
     
    .PARAMETER Options
        The options the user may pick from.
        The user selects a choice by specifying the letter associated with a choice.
        The letter assigned to a choice is picked from the character after the first '&' in any specified string.
        If there is no '&', the system will automatically pick the first letter as choice letter:
        "This &is an example" will have the character "i" bound for the choice.
        "This is &an example" will have the character "a" bound for the choice.
        "This is an example" will have the character "T" bound for the choice.
     
        This parameter takes both strings and hashtables (in any combination).
        A hashtable is expected to have two properties, 'Label' and 'Help'.
        Label is the text shown in the initial prompt, help what the user sees when requesting help.
     
    .PARAMETER Caption
        The title of the question, so the user knows what it is all about.
     
    .PARAMETER Message
        A message to offer to the user. Be more specific about the implications of this choice.
     
    .PARAMETER DefaultChoice
        The index of the choice made by default.
        By default, the first option is selected as default choice.
     
    .EXAMPLE
        PS C:\> Get-PSFUserChoice -Options "1) Create a new user", "2) Disable a user", "3) Unlock an account", "4) Get a cup of coffee", "5) Exit" -Caption "User administration menu" -Message "What operation do you want to perform?"
     
        Prompts the user for what operation to perform from the set of options provided
#>

    [OutputType([System.Int32])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object[]]
        $Options,
        
        [string]
        $Caption,
        
        [string]
        $Message,
        
        [int]
        $DefaultChoice = 0
    )
    
    begin
    {
        $choices = @()
        foreach ($option in $Options)
        {
            if ($option -is [hashtable])
            {
                $label = $option.Keys -match '^l' | Select-Object -First 1
                [string]$labelValue = $option[$label]
                $help = $option.Keys -match '^h' | Select-Object -First 1
                [string]$helpValue = $option[$help]
                
            }
            else
            {
                $labelValue = "$option"
                $helpValue = "$option"
            }
            if ($labelValue -match "&") { $choices += New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList $labelValue, $helpValue }
            else { $choices += New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($labelValue.Trim())", $helpValue }
        }
    }
    process
    {
        # Will error on one option so we just return the value 0 (which is the result of the only option the user would have)
        # This is for cases where the developer dynamically assembles options so that they don't need to ensure a minimum of two options.
        if ($Options.Count -eq 1) { return 0 }
        
        $Host.UI.PromptForChoice($Caption, $Message, $choices, $DefaultChoice)
    }
}

function Register-PSFCallback
{
<#
    .SYNOPSIS
        Registers a scriptblock to execute when a command calls Invoke-PSFCallback.
     
    .DESCRIPTION
        Registers a scriptblock to execute when a command calls Invoke-PSFCallback.
        The basic concept of this feature is for a module to offer a registration point,
        where foreign modules - even those unknown to the implementing module - can register
        scriptblocks as delegates. These will then be executed in the implementing module's commands,
        where those call Invoke-PSFCallback.
     
        When designing a callback, keep in mind, that it will be executed on all applicable commmands.
        A major aspect to consider here is the execution time, as this will get added on top of each applicable execution.
     
    .PARAMETER Name
        Name of the callback.
        Must be unique.
     
    .PARAMETER ModuleName
        The name of the module from which Invoke-PSFCallback is being called.
     
    .PARAMETER CommandName
        Name of the command calling Invoke-PSFCallback.
        Allows wildcard matching.
     
    .PARAMETER ScriptBlock
        The scriptblock to execute as callback action.
        This scriptblock will receive a single argument: A hashtable.
        That hashtable will contain the following keys:
        - Command: Name of the command calling Invoke-PSFCallback
        - ModuleName: Name of the module the command calling Invoke-PSFCallback is part of.
        - CallerFunction: Name of the command calling the command calling Invoke-PSFCallback
        - CallerModule: Name of the module of the command calling the command calling Invoke-PSFCallback
        - Data: Additional data specified by the command calling Invoke-PSFCallback
     
    .PARAMETER Scope
        Whether the callback script is valid in this runspace only (default) or process-wide.
     
    .PARAMETER BreakAffinity
        By default, the callback scriptblock is being executed in the runspace that defined it.
        Setting this parameter, the callback scriptblock is instead being executed in whatever
        runspace it is being triggered from.
     
    .EXAMPLE
        PS C:\> Register-PSFCallback -Name 'MyModule.Configuration' -ModuleName 'DomainManagement' -CommandName '*' -ScriptBlock $ScriptBlock
     
        Defines a callback named 'MyModule.Configuration'.
        This callback scriptblock will be triggered from all commands of the DomainManagement module,
        however only from the current runspace.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $ModuleName,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $CommandName,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [scriptblock]
        $ScriptBlock,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('CurrentRunspace', 'Process')]
        [string]
        $Scope = 'CurrentRunspace',
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch]
        $BreakAffinity
    )
    
    process
    {
        $callback = New-Object PSFramework.Flowcontrol.Callback -Property @{
            Name          = $Name
            ModuleName    = $ModuleName
            CommandName   = $CommandName
            BreakAffinity = $BreakAffinity
            ScriptBlock   = $ScriptBlock
        }
        if ($Scope -eq 'CurrentRunspace') { $callback.Runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId }
        [PSFramework.FlowControl.CallbackHost]::Add($callback)
    }
}

function Stop-PSFFunction
{
<#
    .SYNOPSIS
        Function that interrupts a function.
     
    .DESCRIPTION
        Function that interrupts a function.
         
        This function is a utility function used by other functions to reduce error catching overhead.
        It is designed to allow gracefully terminating a function with a warning by default and also allow opt-in into terminating errors.
        It also allows simple integration into loops.
         
        Note:
        When calling this function with the intent to terminate the calling function in non-ExceptionEnabled mode too, you need to add a return below the call.
         
        For a more detailed explanation - including commented full-scale implementation examples - see the associated help article:
        Get-Help about_psf_flowcontrol
     
    .PARAMETER Message
        A message to pass along, explaining just what the error was.
     
    .PARAMETER String
        A stored string to use to write the log.
        Used in combination with the localization component.
        For more details see the help on Import-PSFLocalizedString and Get-PSFLocalizedString.
     
    .PARAMETER StringValues
        Values to format into the localized string referred to in the -String parameter.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .PARAMETER Category
        What category does this termination belong to?
        Is automatically set when passing an error record. Helps with differentiating exceptions without having to resort to text parsing.
     
    .PARAMETER ErrorRecord
        An option to include an inner exception in the error record (and in the exception thrown, if one is thrown).
        Use this, whenever you call Stop-PSFFunction in a catch block.
         
        Note:
        Pass the full error record, not just the exception.
     
    .PARAMETER Tag
        Tags to add to the message written.
        This allows filtering and grouping by category of message, targeting specific messages.
     
    .PARAMETER FunctionName
        The name of the function to crash.
        This parameter is very optional, since it automatically selects the name of the calling function.
        The function name is used as part of the errorid.
        That in turn allows easily figuring out, which exception belonged to which function when checking out the $error variable.
     
    .PARAMETER ModuleName
        The name of the module, the function to be crashed is part of.
        This parameter is very optional, since it automatically selects the name of the calling function.
     
    .PARAMETER File
        The file in which Stop-PSFFunction was called.
        Will be automatically set, but can be overridden when necessary.
     
    .PARAMETER Line
        The line on which Stop-PSFFunction was called.
        Will be automatically set, but can be overridden when necessary.
     
    .PARAMETER Exception
        Allows specifying an inner exception as input object. This will be passed on to the logging and used for messages.
        When specifying both ErrorRecord AND Exception, Exception wins, but ErrorRecord is still used for record metadata.
     
    .PARAMETER OverrideExceptionMessage
        Disables automatic appending of exception messages.
        Use in cases where you already have a speaking message interpretation and do not need the original message.
     
    .PARAMETER Target
        The object that was processed when the error was thrown.
        For example, if you were trying to process a Database Server object when the processing failed, add the object here.
        This object will be in the error record (which will be written, even in non-silent mode, just won't show it).
        If you specify such an object, it becomes simple to actually figure out, just where things failed at.
     
    .PARAMETER AlwaysWarning
        Ensures the command always writes a warning, no matter what.
        by default, when -EnableException is set to $true it will hide the warning instead.
        You can enable this to always be on for your module by setting the feature flag: PSFramework.Stop-PSFFunction.ShowWarning
        For more information on feature flags, see "Get-Help Set-PSFFeature -Detailed"
     
    .PARAMETER Continue
        This will cause the function to call continue while not running with exceptions enabled (-EnableException).
        Useful when mass-processing items where an error shouldn't break the loop.
     
    .PARAMETER SilentlyContinue
        This will cause the function to call continue while running with exceptions enabled (-EnableException).
        Useful when mass-processing items where an error shouldn't break the loop.
     
    .PARAMETER ContinueLabel
        When specifying a label in combination with "-Continue" or "-SilentlyContinue", this function will call continue with this specified label.
        Helpful when trying to continue on an upper level named loop.
     
    .PARAMETER Cmdlet
        The $PSCmdlet object of the calling command.
        Used to write exceptions in a more hidden manner, avoiding exposing internal script text in the default message display.
     
    .PARAMETER StepsUpward
        When not throwing an exception and not calling continue, Stop-PSFFunction signals the calling command to stop.
        In some cases you may want to signal a step or more further up the chain (notably from helper functions within a function).
        This parameter allows you to add additional steps up the callstack that it will notify.
     
    .EXAMPLE
        Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -ErrorRecord $_
        return
         
        Depending on whether $EnableException is true or false it will:
        - Throw a bloody terminating error. Game over.
        - Write a nice warning about how Foo failed bar, then terminate the function. The return on the next line will then end the calling function.
     
    .EXAMPLE
        Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -Category InvalidOperation -Target $foo -Continue
         
        Depending on whether $EnableException is true or false it will:
        - Throw a bloody terminating error. Game over.
        - Write a nice warning about how Foo failed bar, then call continue to process the next item in the loop.
        In both cases, the error record added to $error will have the content of $foo added, the better to figure out what went wrong.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Message', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFFunction')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Message')]
        [string]
        $Message,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'String')]
        [string]
        $String,
        
        [Parameter(ParameterSetName = 'String')]
        [object[]]
        $StringValues,
        
        [bool]
        $EnableException,
        
        [System.Management.Automation.ErrorCategory]
        $Category = ([System.Management.Automation.ErrorCategory]::NotSpecified),
        
        [Alias('InnerErrorRecord')]
        [System.Management.Automation.ErrorRecord[]]
        $ErrorRecord,
        
        [string[]]
        $Tag,
        
        [string]
        $FunctionName,
        
        [string]
        $ModuleName,
        
        [string]
        $File,
        
        [int]
        $Line,
        
        [System.Exception]
        $Exception,
        
        [switch]
        $OverrideExceptionMessage,
        
        [object]
        $Target,
        
        [switch]
        $AlwaysWarning,
        
        [switch]
        $Continue,
        
        [switch]
        $SilentlyContinue,
        
        [string]
        $ContinueLabel,
        
        [System.Management.Automation.PSCmdlet]
        $Cmdlet,
        
        [int]
        $StepsUpward = 0
    )
    
    if ($Cmdlet) { $myCmdlet = $Cmdlet }
    else { $myCmdlet = $PSCmdlet }
    
    #region Initialize information on the calling command
    $callStack = (Get-PSCallStack)[1]
    if (-not $FunctionName) { $FunctionName = $callStack.Command }
    if (-not $FunctionName) { $FunctionName = "<Unknown>" }
    if (-not $ModuleName) { $ModuleName = $callstack.InvocationInfo.MyCommand.ModuleName }
    if (-not $ModuleName) { $ModuleName = "<Unknown>" }
    if (-not $File) { $File = $callStack.Position.File }
    if (-not $Line) { $Line = $callStack.Position.StartLineNumber }
    if ((Test-PSFParameterBinding -ParameterName EnableException -Not) -and (Test-PSFFeature -Name "PSFramework.InheritEnableException" -ModuleName $ModuleName))
    {
        $EnableException = [bool]$PSCmdlet.GetVariableValue('EnableException')
    }
    #endregion Initialize information on the calling command
    
    #region Apply Transforms
    #region Target Transform
    if ($null -ne $Target)
    {
        $Target = Convert-PsfMessageTarget -Target $Target -FunctionName $FunctionName -ModuleName $ModuleName
    }
    #endregion Target Transform
    
    #region Exception Transforms
    if ($Exception)
    {
        $Exception = Convert-PsfMessageException -Exception $Exception -FunctionName $FunctionName -ModuleName $ModuleName
    }
    elseif ($ErrorRecord)
    {
        $int = 0
        while ($int -lt $ErrorRecord.Length)
        {
            $tempException = Convert-PsfMessageException -Exception $ErrorRecord[$int].Exception -FunctionName $FunctionName -ModuleName $ModuleName
            if ($tempException -ne $ErrorRecord[$int].Exception)
            {
                $ErrorRecord[$int] = New-Object System.Management.Automation.ErrorRecord($tempException, $ErrorRecord[$int].FullyQualifiedErrorId, $ErrorRecord[$int].CategoryInfo.Category, $ErrorRecord[$int].TargetObject)
            }
            
            $int++
        }
    }
    #endregion Exception Transforms
    #endregion Apply Transforms
    
    #region Message Handling
    $records = @()
    $showWarning = $AlwaysWarning
    if (-not $showWarning) { $showWarning = Test-PSFFeature -Name 'PSFramework.Stop-PSFFunction.ShowWarning' -ModuleName $ModuleName }
    
    $paramWritePSFMessage = @{
        Level                     = 'Warning'
        EnableException             = $EnableException
        FunctionName             = $FunctionName
        Target                     = $Target
        Tag                         = $Tag
        ModuleName                 = $ModuleName
        File                     = $File
        Line                     = $Line
    }
    if ($OverrideExceptionMessage) { $paramWritePSFMessage['OverrideExceptionMessage'] = $true }
    if ($Message) { $paramWritePSFMessage["Message"] = $Message }
    else
    {
        $paramWritePSFMessage["String"] = $String
        $paramWritePSFMessage["StringValues"] = $StringValues
    }
    
    if ($ErrorRecord -or $Exception)
    {
        if ($ErrorRecord)
        {
            foreach ($record in $ErrorRecord)
            {
                if (-not $Exception) { $newException = New-Object System.Exception($record.Exception.Message, $record.Exception) }
                else { $newException = $Exception }
                if ($record.CategoryInfo.Category) { $Category = $record.CategoryInfo.Category }
                $records += New-Object System.Management.Automation.ErrorRecord($newException, "$($ModuleName)_$FunctionName", $Category, $Target)
            }
        }
        else
        {
            $records += New-Object System.Management.Automation.ErrorRecord($Exception, "$($ModuleName)_$FunctionName", $Category, $Target)
        }
        
        # Manage Debugging
        if ($EnableException -and -not $showWarning) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null }
        else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage }
    }
    else
    {
        if ($String) {
            if ($StringValues) { $exception = New-Object System.Exception(([PSFramework.Localization.LocalizationHost]::Read("$ModuleName.$String", $StringValues))) }
            else { $exception = New-Object System.Exception(([PSFramework.Localization.LocalizationHost]::Read("$ModuleName.$String"))) }
        }
        else { $exception = New-Object System.Exception($Message) }

        $records += New-Object System.Management.Automation.ErrorRecord($Exception, "$($ModuleName)_$FunctionName", $Category, $Target)
        
        # Manage Debugging
        if ($EnableException -and -not $showWarning) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null }
        else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage }
    }
    #endregion Message Handling
    
    #region Silent Mode
    if ($EnableException)
    {
        if ($SilentlyContinue)
        {
            foreach ($record in $records) { $myCmdlet.WriteError($record) }
            if ($ContinueLabel) { continue $ContinueLabel }
            else { continue }
        }
        
        # Extra insurance that it'll stop
        $psframework_killqueue.Enqueue($callStack.InvocationInfo.GetHashCode())
        
        # Need to use "throw" as otherwise calling function will not be interrupted without passing the cmdlet parameter
        if (-not $Cmdlet) { throw $records[0] }
        else { $Cmdlet.ThrowTerminatingError($records[0]) }
    }
    #endregion Silent Mode
    
    #region Non-Silent Mode
    else
    {
        # This ensures that the error is stored in the $error variable AND has its Stacktrace (simply adding the record would lack the stacktrace)
        foreach ($record in $records)
        {
            $null = Write-Error -Message $record -Category $Category -TargetObject $Target -Exception $record.Exception -ErrorId "$($ModuleName)_$FunctionName" -ErrorAction Continue 2>&1
        }
        
        if ($Continue)
        {
            if ($ContinueLabel) { continue $ContinueLabel }
            else { continue }
        }
        else
        {
            # Make sure the function knows it should be stopping
            if ($StepsUpward -eq 0) { $psframework_killqueue.Enqueue($callStack.InvocationInfo.GetHashCode()) }
            elseif ($StepsUpward -gt 0) { $psframework_killqueue.Enqueue((Get-PSCallStack)[($StepsUpward + 1)].InvocationInfo.GetHashCode()) }
            return
        }
    }
    #endregion Non-Silent Mode
}

function Test-PSFFunctionInterrupt
{
    <#
        .SYNOPSIS
            Tests whether the calling function should be interrupted.
         
        .DESCRIPTION
            This helper function is designed to work in tandem with Stop-PSFFunction.
            When gracefully terminating a function, there is a major issue:
            "Return" will only stop the current one of the three blocks (Begin, Process, End).
            All other statements have side effects or produce lots of red text.
     
            So, Stop-PSFFunction writes a variable into the parent scope, that signals the function should cease.
            This function then checks for that very variable and returns true if it is set.
     
            This avoids having to handle odd variables in the parent function and causes the least impact on contributors.
     
            For a more detailed explanation - including commented full-scale implementation examples - see the associated help article:
            Get-Help about_psf_flowcontrol
         
        .EXAMPLE
            if (Test-PSFFunctionInterrupt) { return }
     
            The calling function will stop if this function returns true.
    #>

    [OutputType([System.Boolean])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFFunctionInterrupt')]
    Param (
        
    )
    
    $psframework_killqueue -contains (Get-PSCallStack)[1].InvocationInfo.GetHashCode()
}

function Test-PSFLanguageMode
{
<#
    .SYNOPSIS
        Tests, in what language mode a specified scriptblock is in.
     
    .DESCRIPTION
        Tests, in what language mode a specified scriptblock is in.
        Use this to determine the trustworthyness of a scriptblock, or for insights, into what its capabilities are.
     
    .PARAMETER ScriptBlock
        The scriptblock to test.
     
    .PARAMETER Mode
        The Languagemode(s) to compare it to.
        The scriptblock must be in one of the specified modes.
        Defaults to 'FullLanguage'
     
    .PARAMETER Not
        Reverses the test results - now the scriptblock may NOT be in one of the specified language modes.
     
    .EXAMPLE
        PS C:\> Test-PSFLanguageMode -ScriptBlock $ScriptBlock
     
        Returns, whether the $Scriptblock is in FullLanguage mode.
     
    .EXAMPLE
        PS C:\> Test-PSFLanguageMode -ScriptBlock $code -Mode ConstrainedLanguage -Not
     
        Returns $true if the specified scriptblock is NOT inconstrained language mode.
#>

    [OutputType([boolean])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,
        
        [System.Management.Automation.PSLanguageMode[]]
        $Mode = 'FullLanguage',
        
        [switch]
        $Not
    )
    
    process
    {
        $languageMode = [PSFramework.Utility.UtilityHost]::GetPrivateProperty("LanguageMode", $ScriptBlock)
        if ($Not) { $languageMode -notin $Mode }
        else { $languageMode -in $Mode }
    }
}

function Test-PSFParameterBinding
{
    <#
        .SYNOPSIS
            Helper function that tests, whether a parameter was bound.
         
        .DESCRIPTION
            Helper function that tests, whether a parameter was bound.
         
        .PARAMETER ParameterName
            The name(s) of the parameter that is tested for being bound.
            By default, the check is true when AT LEAST one was bound.
     
        .PARAMETER Not
            Reverses the result. Returns true if NOT bound and false if bound.
     
        .PARAMETER And
            All specified parameters must be present, rather than at least one of them.
     
        .PARAMETER Mode
            Parameters can be explicitly bound or as scriptblocks to be invoked for each item piped to the command.
            The mode determines, which will be tested for.
            Supported Modes: Any, Explicit, PipeScript.
            By default, any will do.
            Whether a parameter was bound as PipeScript is only detectable during the begin block.
         
        .PARAMETER BoundParameters
            The hashtable of bound parameters. Is automatically inherited from the calling function via default value. Needs not be bound explicitly.
         
        .EXAMPLE
            if (Test-PSFParameterBinding "Day")
            {
                 
            }
     
            Snippet as part of a function. Will check whether the parameter "Day" was bound. If yes, whatever logic is in the conditional will be executed.
     
        .EXAMPLE
            Test-PSFParameterBinding -Not 'Login', 'Spid', 'ExcludeSpid', 'Host', 'Program', 'Database'
     
            Returns whether none of the parameters above were specified.
     
        .EXAMPLE
            Test-PSFParameterBinding -And 'Login', 'Spid', 'ExcludeSpid', 'Host', 'Program', 'Database'
     
            Returns whether any of the specified parameters was not bound
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFParameterBinding')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string[]]
        $ParameterName,
        
        [Alias('Reverse')]
        [switch]
        $Not,
        
        [switch]
        $And,
        
        [ValidateSet('Any', 'Explicit', 'PipeScript')]
        [string]
        $Mode = 'Any',
        
        [object]
        $BoundParameters = (Get-PSCallStack)[0].InvocationInfo.BoundParameters
    )
    
    process
    {
        if ($And)
        {
            $test = $true
        }
        else
        {
            $test = $false
        }
        $pipeScriptForbidden = $Mode -eq "Explicit"
        $explicitForbidden = $Mode -eq "PipeScript"
        
        foreach ($name in $ParameterName)
        {
            $isPipeScript = ($BoundParameters.$name.PSObject.TypeNames -eq 'System.Management.Automation.CmdletParameterBinderController+DelayedScriptBlockArgument') -as [bool]
            if ($And)
            {
                if (-not $BoundParameters.ContainsKey($name))
                {
                    $test = $false
                    continue
                }
                if ($isPipeScript -and $pipeScriptForbidden) { $test = $false }
                if (-not $isPipeScript -and $explicitForbidden) { $test = $false }
                
            }
            else
            {
                if ($BoundParameters.ContainsKey($name))
                {
                    if ($isPipeScript -and -not $pipeScriptForbidden) { $test = $true }
                    if (-not $isPipeScript -and -not $explicitForbidden) { $test = $true }
                }
            }
        }
        
        return ((-not $Not) -eq $test)
    }
}
if (-not (Test-Path Alias:Was-Bound)) { Set-Alias -Value Test-PSFParameterBinding -Name Was-Bound -Scope Global }

function Test-PSFPowerShell
{
<#
    .SYNOPSIS
        Tests for conditions in the PowerShell environment.
     
    .DESCRIPTION
        This helper command can evaluate various runtime conditions, such as:
        - PowerShell Version
        - PowerShell Edition
        - Operating System
        - Elevation
        This makes it easier to do conditional code.
        It also makes it easier to simulate code-paths during pester tests, by mocking this command.
     
    .PARAMETER PSMinVersion
        PowerShell must be running under at least this version.
     
    .PARAMETER PSMaxVersion
        PowerShell most not be runnign on a version higher than this.
     
    .PARAMETER Edition
        PowerShell must be running in the specifioed edition (Core or Desktop)
     
    .PARAMETER OperatingSystem
        PowerShell must be running on the specified OS.
     
    .PARAMETER Elevated
        PowerShell must be running with elevation.
         
        Note:
        This test is only supported on Windows.
        On other OS it will automatically succede and assume root privileges.
     
    .PARAMETER ComputerName
        The computer on which to test local PowerShell conditions.
        If this parameter is not specified, it tests the current PowerShell process and hosting OS.
        Accepts established PowerShell sessions.
     
    .PARAMETER Credential
        The credentials to use when connecting to a remote computer.
     
    .EXAMPLE
        PS C:\> Test-PSFPowerShell -PSMinVersion 5.0
     
        Will return $false, unless the executing powershell version is at least 5.0
     
    .EXAMPLE
        PS C:\> Test-PSFPowerShell -Edition Core
     
        Will return $true, if the current powershell session is a PowerShell core session.
     
    .EXAMPLE
        PS C:\> Test-PSFPowerShell -Elevated
     
        Will return $false if on windows and not running as admin.
        Will return $true otherwise.
     
    .EXAMPLE
        PS C:\> Test-PSFPowerShell -PSMinVersion 6.1 -OperatingSystem Windows
     
        Will return $false unless executed on a PowerShell 6.1 console running on windows.
#>

    [OutputType([System.Boolean])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFPowerShell')]
    param (
        [Version]
        $PSMinVersion,
        
        [Version]
        $PSMaxVersion,
        
        [PSFramework.FlowControl.PSEdition]
        $Edition,
        
        [PSFramework.FlowControl.OperatingSystem]
        [Alias('OS')]
        $OperatingSystem,
        
        [switch]
        $Elevated,
        
        [PSFComputer]
        $ComputerName,
        
        [PSCredential]
        $Credential
    )
    
    begin
    {
        $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include ComputerName, Credential
    }
    process
    {
        #region Local execution for performance reasons separate
        if (-not $PSBoundParameters.ContainsKey('ComputerName'))
        {
            #region PS Version Test
            if ($PSMinVersion -and ($PSMinVersion -ge $PSVersionTable.PSVersion))
            {
                return $false
            }
            if ($PSMaxVersion -and ($PSMaxVersion -le $PSVersionTable.PSVersion))
            {
                return $false
            }
            #endregion PS Version Test
            
            #region PS Edition Test
            if ($Edition -like "Desktop")
            {
                if ($PSVersionTable.PSEdition -eq "Core")
                {
                    return $false
                }
            }
            if ($Edition -like "Core")
            {
                if ($PSVersionTable.PSEdition -ne "Core")
                {
                    return $false
                }
            }
            #endregion PS Edition Test
            
            #region OS Test
            if ($OperatingSystem)
            {
                switch ($OperatingSystem)
                {
                    "MacOS"
                    {
                        if ($PSVersionTable.PSVersion.Major -lt 6) { return $false }
                        if (-not $IsMacOS) { return $false }
                    }
                    "Linux"
                    {
                        if ($PSVersionTable.PSVersion.Major -lt 6) { return $false }
                        if (-not $IsLinux) { return $false }
                    }
                    "Windows"
                    {
                        if (($PSVersionTable.PSVersion.Major -ge 6) -and (-not $IsWindows))
                        {
                            return $false
                        }
                    }
                }
            }
            #endregion OS Test
            
            #region Elevation
            if ($Elevated)
            {
                if (($PSVersionTable.PSVersion.Major -lt 6) -or ($IsWindows))
                {
                    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
                    $principal = New-Object Security.Principal.WindowsPrincipal $identity
                    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))
                    {
                        return $false
                    }
                }
            }
            #endregion Elevation
            
            return $true
        }
        #endregion Local execution for performance reasons separate
        
        Invoke-PSFCommand @parameter -ScriptBlock {
            #region PS Version Test
            if ($PSMinVersion -and ($PSMinVersion -ge $PSVersionTable.PSVersion))
            {
                return $false
            }
            if ($PSMaxVersion -and ($PSMaxVersion -le $PSVersionTable.PSVersion))
            {
                return $false
            }
            #endregion PS Version Test
            
            #region PS Edition Test
            if ($Edition -like "Desktop")
            {
                if ($PSVersionTable.PSEdition -eq "Core")
                {
                    return $false
                }
            }
            if ($Edition -like "Core")
            {
                if ($PSVersionTable.PSEdition -ne "Core")
                {
                    return $false
                }
            }
            #endregion PS Edition Test
            
            #region OS Test
            if ($OperatingSystem)
            {
                switch ($OperatingSystem)
                {
                    "MacOS"
                    {
                        if ($PSVersionTable.PSVersion.Major -lt 6) { return $false }
                        if (-not $IsMacOS) { return $false }
                    }
                    "Linux"
                    {
                        if ($PSVersionTable.PSVersion.Major -lt 6) { return $false }
                        if (-not $IsLinux) { return $false }
                    }
                    "Windows"
                    {
                        if (($PSVersionTable.PSVersion.Major -ge 6) -and (-not $IsWindows))
                        {
                            return $false
                        }
                    }
                }
            }
            #endregion OS Test
            
            #region Elevation
            if ($Elevated)
            {
                if (($PSVersionTable.PSVersion.Major -lt 6) -or ($IsWindows))
                {
                    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
                    $principal = New-Object Security.Principal.WindowsPrincipal $identity
                    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))
                    {
                        return $false
                    }
                }
            }
            #endregion Elevation
            
            return $true
        }
    }
}

function Unregister-PSFCallback
{
<#
    .SYNOPSIS
        Removes a callback from the list of registered callbacks.
     
    .DESCRIPTION
        Removes a callback from the list of registered callbacks.
     
    .PARAMETER Name
        The name of the callback to remove.
        Does NOT support wildcards.
     
    .PARAMETER Callback
        A full callback object to remove.
        Use Get-PSFCallback to get the list of relevant callback objects.
     
    .EXAMPLE
        PS C:\> Unregister-PSFCallback -Name 'MyModule.Configuration'
     
        Unregisters the 'MyModule.Configuration' callback script.
     
    .EXAMPLE
        PS C:\> Get-PSFCallback | Unregister-PSFCallback
     
        Removes all callback scripts applicable to the current runspace.
#>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object', Mandatory = $true)]
        [PSFramework.FlowControl.Callback[]]
        $Callback
    )
    
    process
    {
        foreach ($callbackItem in $Callback)
        {
            [PSFramework.FlowControl.CallbackHost]::Remove($callbackItem)
        }
        foreach ($nameString in $Name)
        {
            foreach ($callbackItem in ([PSFramework.FlowControl.CallbackHost]::Get($nameString, $false)))
            {
                if ($callbackItem.Name -ne $nameString) { continue }
                [PSFramework.FlowControl.CallbackHost]::Remove($callbackItem)
            }
        }
    }
}

function Export-PSFModuleClass
{
<#
    .SYNOPSIS
        Exports a module-defined PowerShell class irrespective of how the module is being imported.
     
    .DESCRIPTION
        Exports a module-defined PowerShell class irrespective of how the module is being imported.
        This avoids having to worry about how the module is being imported.
     
        Please beware the risk of class-name-collisions however.
     
    .PARAMETER ClassType
        The types to publish.
     
    .EXAMPLE
        PS C:\> Export-PSFModuleClass -ClassType ([MyModule_MyClass])
     
        Publishes the class MyModule_MyClass, making it available outside of your module.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Type[]]
        $ClassType
    )
    
    begin
    {
        $internalExecutionContext = [PSFramework.Utility.UtilityHost]::GetExecutionContextFromTLS()
        $topLevelSessionState = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('TopLevelSessionState', $internalExecutionContext)
        $globalScope = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('GlobalScope', $topLevelSessionState)
        $addMethod = $globalScope.GetType().GetMethod('AddType', [System.Reflection.BindingFlags]'Instance, NonPublic')
    }
    process
    {
        foreach ($typeObject in $ClassType)
        {
            $arguments = @($typeObject.Name, $typeObject)
            $addMethod.Invoke($globalScope, $arguments)
        }
    }
}

function Import-PSFCmdlet
{
<#
    .SYNOPSIS
        Loads a cmdlet into the current context.
     
    .DESCRIPTION
        Loads a cmdlet into the current context.
        This can be used to register a cmdlet during module import, making it easy to have hybrid modules publishing both cmdlets and functions.
        Can also be used to register cmdlets written in PowerShell classes.
     
    .PARAMETER Name
        The name of the cmdlet to register.
     
    .PARAMETER Type
        The type of the class implementing the cmdlet.
     
    .PARAMETER HelpFile
        Path to the help XML containing the help for the cmdlet.
     
    .PARAMETER Module
        Module to inject the cmdlet into.
     
    .EXAMPLE
        PS C:\> Import-PSFCmdlet -Name Get-Something -Type ([GetSomethingCommand])
     
        Imports the Get-Something cmdlet into the current context.
     
    .EXAMPLE
        PS C:\> Import-PSFCmdlet -Name Get-Something -Type ([GetSomethingCommand]) -Module (Get-Module PSReadline)
     
        Imports the Get-Something cmdlet into the PSReadline module.
     
    .NOTES
        Original Author: Chris Dent
        Link: https://www.indented.co.uk/cmdlets-without-a-dll/
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFCmdlet')]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [Type]
        $Type,
        
        [string]
        $HelpFile,
        
        [System.Management.Automation.PSModuleInfo]
        $Module
    )
    
    begin
    {
        $scriptBlock = {
            param (
                [String]
                $Name,
                
                [Type]
                $Type,
                
                [string]
                $HelpFile
            )
            
            $sessionStateCmdletEntry = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry(
                $Name,
                $Type,
                $HelpFile
            )
            
            $context = [PSFramework.Utility.UtilityHost]::GetExecutionContextFromTLS()
            
            # Get the SessionStateInternal type
            $internalType = [PowerShell].Assembly.GetType('System.Management.Automation.SessionStateInternal')
            
            # Get a valid constructor which accepts a param of type ExecutionContext
            $constructor = $internalType.GetConstructor(
                [System.Reflection.BindingFlags]'Instance,NonPublic',
                $null,
                $context.GetType(),
                $null
            )
            
            # Get the SessionStateInternal for this execution context
            $sessionStateInternal = $constructor.Invoke($context)
            
            # Get the method which allows Cmdlets to be added to the session
            $method = $internalType.GetMethod(
                'AddSessionStateEntry',
                [System.Reflection.BindingFlags]'Instance,NonPublic',
                $null,
                $sessionStateCmdletEntry.GetType(),
                $null
            )
            # Invoke the method.
            $method.Invoke($sessionStateInternal, $sessionStateCmdletEntry)
        }
    }
    
    process
    {
        if (-not $Module) { $scriptBlock.Invoke($Name, $Type, $HelpFile) }
        else { $Module.Invoke($scriptBlock, @($Name, $Type, $HelpFile)) }
    }
}

function Register-PSFParameterClassMapping
{
<#
    .SYNOPSIS
        Registers types to a parameter classes input interpretation.
     
    .DESCRIPTION
        The parameter classes shipped in PSFramework can be extended to support input of an unknown object type.
        In order to accomplish that, it is necessary to register the name of that type (and the properties to use) using this command.
     
        On input interpretation, it will check the TypeNames property on the PSObject for evaluation.
        This means you can also specify custom PSObjects by giving them a dedicated name.
     
    .PARAMETER ParameterClass
        The parameter class to extend.
     
    .PARAMETER TypeName
        The name of the type to register.
     
    .PARAMETER Properties
        The properties to check.
        When processing an object of this type, it will try to access the properties in this order, trying to make something fit the intended result.
        The first property that is a fit for the parameter class is chosen, other ones are ignored.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Register-PSFParameterClassMapping -ParameterClass 'Computer' -TypeName 'microsoft.activedirectory.management.adcomputer' -Properties 'DNSHostName', 'Name'
     
        This extends the computer parameter class by ...
        - having it accept the type 'microsoft.activedirectory.management.adcomputer'
        - having it use the 'DNSHostName' property if available, falling back to 'Name' if necessary
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFParameterClassMapping')]
    param (
        [Parameter(Mandatory = $true)]
        [PSFramework.Parameter.ParameterClasses]
        $ParameterClass,
        
        [Parameter(Mandatory = $true)]
        [string]
        $TypeName,
        
        [Parameter(Mandatory = $true)]
        [string[]]
        $Properties,
        
        [switch]
        $EnableException
    )
    
    process
    {
        try
        {
            switch ($ParameterClass)
            {
                "Computer"
                {
                    [PSFramework.Parameter.ComputerParameter]::SetTypePropertyMapping($TypeName, $Properties)
                }
                "DateTime"
                {
                    [PSFramework.Parameter.DateTimeParameter]::SetTypePropertyMapping($TypeName, $Properties)
                }
                "TimeSpan"
                {
                    [PSFramework.Parameter.TimeSpanParameter]::SetTypePropertyMapping($TypeName, $Properties)
                }
                "Encoding"
                {
                    [PSFramework.Parameter.EncodingParameter]::SetTypePropertyMapping($TypeName, $Properties)
                }
                default
                {
                    Stop-PSFFunction -String 'Register-PSFParameterClassMapping.NotImplemented' -StringValues $ParameterClass -EnableException $EnableException -Tag 'fail', 'argument' -Category NotImplemented
                    return
                }
            }
        }
        catch
        {
            Stop-PSFFunction -String 'Register-PSFParameterClassMapping.Registration.Error' -StringValues $ParameterClass, $Typename -EnableException $EnableException -Tag 'fail', '.NET' -ErrorRecord $_
            return
        }
    }
}

function Set-PSFTypeAlias
{
<#
    .SYNOPSIS
        Registers or updates an alias for a .NET type.
     
    .DESCRIPTION
        Registers or updates an alias for a .NET type.
        Use this function during module import to create shortcuts for typenames users can be expected to interact with directly.
     
    .PARAMETER AliasName
        The short and useful alias for the type.
     
    .PARAMETER TypeName
        The full name of the type.
        Example: 'System.IO.FileInfo'
     
    .PARAMETER Mapping
        A hashtable of alias to typename mappings.
        Useful to registering a full set of type aliases.
     
    .EXAMPLE
        PS C:\> Set-PSFTypeAlias -AliasName 'file' -TypeName 'System.IO.File'
     
        Creates an alias for the type 'System.IO.File' named 'file'
     
    .EXAMPLE
        PS C:\> Set-PSFTypeAlias -Mapping @{
            file = 'System.IO.File'
            path = 'System.IO.Path'
        }
     
        Creates an alias for the type 'System.IO.File' named 'file'
        Creates an alias for the type 'System.IO.Path' named 'path'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Name', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTypeAlias')]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name', ValueFromPipelineByPropertyName = $true)]
        [string]
        $AliasName,
        
        [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'Name', ValueFromPipelineByPropertyName = $true)]
        [string]
        $TypeName,
        
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Hashtable')]
        [hashtable]
        $Mapping
    )
    
    begin
    {
        # Obtain a reference to the TypeAccelerators type
        $TypeAcceleratorType = [psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators")
    }
    process
    {
        foreach ($key in $Mapping.Keys)
        {
            $TypeAcceleratorType::Add($key, $Mapping[$key])
        }
        if ($AliasName)
        {
            $TypeAcceleratorType::Add($AliasName, $TypeName)
        }
    }
}

function Get-PSFLicense
{
<#
    .SYNOPSIS
        Returns registered licenses
     
    .DESCRIPTION
        Returns all matching licenses from the PSFramework internal license cache.
     
    .PARAMETER Filter
        Default: "*"
        Filters for the name of the product. Uses the -like operator.
     
    .PARAMETER ProductType
        Only licenses of products for any of the specified types are considered.
     
    .PARAMETER LicenseType
        Only licenses of any matching type are returned.
     
    .PARAMETER Manufacturer
        Default: "*"
        Only licenses for products of a matching manufacturer are returned. Uses the -like operator for comparisons.
     
    .EXAMPLE
        PS C:\> Get-PSFLicense *Microsoft*
     
        Returns all registered licenses for products with the string "Microsoft" in their name
     
    .EXAMPLE
        PS C:\> Get-PSFLicense -LicenseType Commercial -ProductType Library
     
        Returns a list of all registered licenses for products that have commercial licenses and are libraries.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFLicense')]
    [OutputType([PSFramework.License.License])]
    param (
        [Parameter(Position = 0)]
        [Alias('Product')]
        [String]
        $Filter = "*",
        
        [PSFramework.License.ProductType[]]
        $ProductType,
        
        [PSFramework.License.LicenseType]
        $LicenseType,
        
        [String]
        $Manufacturer = "*"
    )
    
    process
    {
        [PSFramework.License.LicenseHost]::Get() | Where-Object {
            if ($_.Product -notlike $Filter) { return $false }
            if ($_.Manufacturer -notlike $Manufacturer) { return $false }
            if ($ProductType -and ($_.ProductType -notin $ProductType)) { return $false }
            if ($licenseType -and -not ($_.LicenseType -band $LicenseType)) { return $false }
            return $true
        }
    }
}


function New-PSFLicense
{
<#
    .SYNOPSIS
        Creates a new license object and registers it
     
    .DESCRIPTION
        This function creates a new license object used by the PSFramework licensing component. The license is automatically registered in the current process' license store.
     
    .PARAMETER Product
        The product that is being licensed
     
    .PARAMETER Manufacturer
        The entity that produced the licensed product
     
    .PARAMETER ProductVersion
        The version of the licensed product
     
    .PARAMETER ProductType
        What kind of product is te license for?
        Options: Module, Script, Library, Application, Other
     
    .PARAMETER Name
        Most licenses used have a name. Feel free to register that name as well.
     
    .PARAMETER Version
        What version is the license?
     
    .PARAMETER Date
        When was the product licensed with the registered license
     
    .PARAMETER Type
        Default: Free
        This shows the usual limitations that apply to this license. By Default, licenses are considered free and may be modified, but require attribution when used in your own product.
     
    .PARAMETER Text
        The full text of your license.
     
    .PARAMETER Description
        A description of the content. Useful when describing how some license is used within your own product.
     
    .PARAMETER Parent
        The license of the product within which the product of this license is used.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> New-PSFLicense -Product 'Awesome Test Product' -Manufacturer 'Awesome Inc.' -ProductVersion '1.0.1.0' -ProductType Application -Name FreeBSD -Version "3.0.0.0" -Date (Get-Date -Year 2016 -Month 11 -Day 28 -Hour 0 -Minute 0 -Second 0) -Text @"
        Copyright (c) 2016, Awesome Inc.
        All rights reserved.
 
        Redistribution and use in source and binary forms, with or without
        modification, are permitted provided that the following conditions are met:
 
        1. Redistributions of source code must retain the above copyright notice, this
           list of conditions and the following disclaimer.
        2. Redistributions in binary form must reproduce the above copyright notice,
           this list of conditions and the following disclaimer in the documentation
           and/or other materials provided with the distribution.
 
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
        ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
        ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
        (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
        LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
        ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
        The views and conclusions contained in the software and documentation are those
        of the authors and should not be interpreted as representing official policies,
        either expressed or implied, of the FreeBSD Project.
        "@
     
        This registers the Awesome Test Product as licensed under the common FreeBSD license.
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFLicense')]
    [OutputType([PSFramework.License.License])]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $Product,
        
        [String]
        $Manufacturer = "ACME ltd.",
        
        [Version]
        $ProductVersion = "1.0.0.0",
        
        [Parameter(Mandatory = $true)]
        [PSFramework.License.ProductType]
        $ProductType,
        
        [String]
        $Name = "Unknown",
        
        [Version]
        $Version = "1.0.0.0",
        
        [DateTime]
        $Date = (Get-Date -Year 1989 -Month 10 -Day 3 -Hour 0 -Minute 0 -Second 0),
        
        [PSFramework.License.LicenseType]
        $Type = "Free",
        
        [Parameter(Mandatory = $true)]
        [String]
        $Text,
        
        [string]
        $Description,
        
        [PSFramework.License.License]
        $Parent
    )
    
    # Create and fill object
    $license = New-Object PSFramework.License.License -Property @{
        Product           = $Product
        Manufacturer   = $Manufacturer
        ProductVersion = $ProductVersion
        ProductType    = $ProductType
        LicenseName    = $Name
        LicenseVersion = $Version
        LicenseDate    = $Date
        LicenseType    = $Type
        LicenseText    = $Text
        Description    = $Description
        Parent           = $Parent
    }
    if (Test-PSFShouldProcess -Action 'Create License' -Target $license -PSCmdlet $PSCmdlet)
    {
        if (-not ([PSFramework.License.LicenseHost]::Get($license)))
        {
            [PSFramework.License.LicenseHost]::Add($license)
        }
        
        return $license
    }
}


function Remove-PSFLicense
{
<#
    .SYNOPSIS
        Removes a registered license from the license store
     
    .DESCRIPTION
        Removes a registered license from the license store
     
    .PARAMETER License
        The license to remove
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Get-PSFLicense "FooBar" | Remove-PSFLicense
     
        Removes the license for the product "FooBar" from the license store.
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Remove-PSFLicense')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSFramework.License.License[]]
        $License,
        
        [switch]
        $EnableException
    )
    
    Process
    {
        foreach ($licenseObject in $License)
        {
            if (Test-PSFShouldProcess -Action 'Remove License' -Target $licenseObject -PSCmdlet $PSCmdlet)
            {
                try { [PSFramework.License.LicenseHost]::Remove($licenseObject) }
                catch
                {
                    Stop-PSFFunction -Message "Failed to remove license" -ErrorRecord $_ -EnableException $EnableException -Target $licenseObject -Continue
                }
            }
        }
    }
}


function Get-PSFLocalizedString
{
<#
    .SYNOPSIS
        Returns the localized strings of a module.
     
    .DESCRIPTION
        Returns the localized strings of a module.
        By default, it creates a variable that has access to each localized string in the module (with string name as propertyname).
        Alternatively, by specifying a specific string, that string can instead be returned.
     
    .PARAMETER Module
        The name of the module to map.
     
    .PARAMETER Name
        The name of the string to return
     
    .EXAMPLE
        PS C:\> Get-PSFLocalizedString -Module 'MyModule'
     
        Returns an object that can be used to access any localized string.
     
    .EXAMPLE
        PS C:\> Get-PSFLocalizedString -Module 'MyModule' -Name 'ErrorValidation'
     
        Returns the string for the module 'MyModule' that is stored under the 'ErrorValidation' name.
#>

    [OutputType([PSFramework.Localization.LocalStrings], ParameterSetName = 'Default')]
    [OutputType([System.String], ParameterSetName = 'Name')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Default')]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name
    )
    
    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            'Default' { New-Object PSFramework.Localization.LocalStrings($Module) }
            'Name' { (New-Object PSFramework.Localization.LocalStrings($Module)).$Name }
        }
    }
}

function Import-PSFLocalizedString
{
<#
    .SYNOPSIS
        Imports a set of localized strings from a PowerShell data file.
     
    .DESCRIPTION
        Imports a set of localized strings from a PowerShell data file.
        This is used to feed the localized string feature set.
        Always import for all languages, do not select by current language - the system handles language selection.
     
        Strings are process wide, so loading additional languages can be offloaded into a background task.
     
    .PARAMETER Path
        The path to the psd1 file to import as strings file.
     
    .PARAMETER Module
        The module for which to import the strings.
     
    .PARAMETER Language
        The language of the specific strings file.
        Defaults to en-US.
     
    .EXAMPLE
        PS C:\> Import-PSFLocalizedString -Path '$moduleRoot\strings.psd1' -Module 'MyModule'
     
        Imports the strings stored in strings.psd1 for the module MyModule as 'en-US' language strings.
     
    .NOTES
        This command is not safe to expose in a JEA endpoint.
        In its need to maintain compatibility it allows for a path for arbitrary code execution.
#>

    [PSFramework.PSFCore.NoJeaCommand()]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Module,
        
        #[PsfValidateSet(TabCompletion = 'PSFramework-LanguageNames', NoResults = 'Continue')]
        [string]
        $Language = 'en-US'
    )
    
    begin
    {
        try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem }
        catch { Stop-PSFFunction -Message "Failed to resolve path: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ }
    }
    process
    {
        foreach ($pathItem in $resolvedPath)
        {
            $data = Import-PSFPowerShellDataFile -Path $pathItem
            foreach ($key in $data.Keys)
            {
                [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key])
            }
        }
    }
}

function Add-PSFLoggingProviderRunspace
{
<#
    .SYNOPSIS
        Adds a runspace to the list of dynamically included runspaces of an active logging provider instance.
     
    .DESCRIPTION
        Adds a runspace to the list of dynamically included runspaces of an active logging provider instance.
        This is designed to allow runspaces to add themselves "on the fly" to a specific logging provider.
     
        Consider this scenario:
        You have a large workload you spread across many runspaces.
        However, each workload item might perform one out of three different categories of tasks.
        You want each of these categories to log into a dedicated logfile and have prepared a provider for each.
        Set each such logging instance as "-RequiresInclude" so by default nothing gets logged to any of them.
        Then each workload item can call this command to add itself to the correct logging provider instance.
     
        When done, call "Remove-PSFLoggingProviderRunspace" to remove that runspace correctly from the instance.
        When using runspaces with a runspace pool, runspaces might be recycled for workitems of other categories, so cleaning it up is a useful habit.
     
        Note:
        This call will fail if the instance has not been created yet!
        After setting up the logging provider instance using Set-PSFLoggingProvider, a short delay may occur before the instance is created.
        With the default configuration, this delay should be no worse than 6 seconds and generally a lot less.
        You can use "Get-PSFLoggingProviderInstance -ProviderName <providername> -Name <instancename>" to check whether it has been created.
     
    .PARAMETER ProviderName
        Name of the logging provider the instance is part of.
     
    .PARAMETER InstanceName
        Name of the logging provider instance to target.
        Default: "default" (the instance created when you omit the instancename parameter on Set-PSFLoggingProvider)
     
    .PARAMETER Runspace
        The Runspace ID of the runspace to add.
        Defaults to the current runspace.
     
    .EXAMPLE
        PS C:\> Add-PSFLoggingProviderRunspace -ProviderName 'logfile' -InstanceName UpdateTask
     
        Adds the current runspace to the list of included runspaces on the logfile instance "UpdateTask".
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ProviderName,
        
        [string]
        $InstanceName = 'default',
        
        [guid]
        $Runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId
    )
    
    process
    {
        $instance = Get-PSFLoggingProviderInstance -ProviderName $ProviderName -Name $InstanceName
        if (-not $instance) {
            Stop-PSFFunction -String 'Add-PSFLoggingProviderRunspace.Instance.NotFound' -StringValues $ProviderName, $InstanceName -EnableException $true -Category ObjectNotFound -Cmdlet $PSCmdlet
        }
        
        $instance.AddRunspace($Runspace)
    }
}

function Get-PSFLoggingProvider
{
<#
    .SYNOPSIS
        Returns a list of the registered logging providers.
     
    .DESCRIPTION
        Returns a list of the registered logging providers.
        Those are used to log messages to whatever system they are designed to log to.
     
        PSFramework ships with a few default logging providers.
        Custom logging destinations can be created by implementing your own, custom provider and registering it using Register-PSFLoggingProvider.
     
    .PARAMETER Name
        Default: '*'
        The name to filter by
     
    .EXAMPLE
        PS C:\> Get-PSFLoggingProvider
     
        Returns all logging provider
     
    .EXAMPLE
        PS C:\> Get-PSFLoggingProvider -Name filesystem
     
        Returns the filesystem provider
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFLoggingProvider')]
    [OutputType([PSFramework.Logging.Provider])]
    Param (
        [Alias('Provider', 'ProviderName')]
        [string]
        $Name = "*"
    )
    
    process
    {
        [PSFramework.Logging.ProviderHost]::Providers.Values | Where-Object Name -Like $Name | Sort-Object ProviderVersion, Name
    }
}

function Get-PSFLoggingProviderInstance
{
<#
    .SYNOPSIS
        Returns a list of the enabled logging provider instances.
     
    .DESCRIPTION
        Returns a list of the enabled logging provider instances.
        Those are used to log messages to whatever system they are designed to log to.
     
        PSFramework ships with a few default logging providers.
        Custom logging destinations can be created by implementing your own, custom provider and registering it using Register-PSFLoggingProvider.
     
    .PARAMETER ProviderName
        Default: '*'
        The name of the provider the instance is an instance of.
     
    .PARAMETER Name
        Default: '*'
        The name of the instance to filter by.
     
    .PARAMETER Force
        Enables returning disabled instances.
     
    .EXAMPLE
        PS C:\> Get-PSFLoggingProviderInstance
     
        Returns all enabled logging provider instances.
     
    .EXAMPLE
        PS C:\> Get-PSFLoggingProviderInstance -ProviderName logfile -Force
     
        Returns all logging provider instances - enabled or not - of the logfile provider
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFLoggingProvider')]
    [OutputType([PSFramework.Logging.Provider])]
    param (
        [string]
        $ProviderName = '*',
        
        [string]
        $Name = '*',
        
        [switch]
        $Force
    )
    
    process
    {
        foreach ($provider in ([PSFramework.Logging.ProviderHost]::Providers.Values | Sort-Object Name))
        {
            if ($provider.ProviderVersion -lt 2) { continue }
            if ($provider.Name -notlike $ProviderName) { continue }
            
            foreach ($instance in ($provider.Instances.Values | Sort-Object Name))
            {
                if ($instance.Name -notlike $Name) { continue }
                if (-not ($instance.Enabled -or $Force)) { continue }
                $instance
            }
        }
    }
}

function Import-PSFLoggingProvider
{
<#
    .SYNOPSIS
        Imports logging provider code and configuration from a hosting source.
     
    .DESCRIPTION
        Imports logging provider code and configuration from a hosting source.
        This enables centrally providing logging provider settings that are retrieved by running this command.
     
        You can simply run this command with no arguments.
        It will then only do anything, if there is a defined value for the configuration setting "PSFramework.Logging.Provider.Source".
     
        If specifying a path or relying on the configuration setting above, it expects the path to ...
        - Be either a weblink or a file system path
        - Point at a json file containing the relevant provider information
        - Be accessible without specific authentication information
     
        Alternatively to specifying a path (or relying on the configured value), you can also give it the same data raw via the "-Data" parameter.
        This needs to be the exact same data layout as provided by the json file, only already readied as PowerShell objects.
     
        In both cases, you provide one or multiple items which may contain the following Properties (all others will be ignored):
        - ProviderPath
        - ProviderName
        - InstallationConfig
        - ProviderConfig
     
        # Providerpath
        #---------------
     
        The ProviderPath property is a full or relative path to a scriptfile that contains LoggingProvider code.
        A relative path would be relative to the path of the json file originally retrieved.
        If calling this command with the "-Data" parameter, relative paths are not supported.
        The scriptfile must be valid PowerShell code, however the original extension matters not.
        The file will be run as untrusted code, so it will fail in Constraiend Language Mode, unless you sign the provider script with a whitelisted publisher certificate.
     
        # ProviderName
        #---------------
     
        The name of the provider to install/configure.
        This property is needed in order to use the subsequent two configuration properties.
         
        Note: If specifying both ProviderPath and ProviderName, it will FIRST install the new provider.
        You can thus deploy and configure a provider in the same setting.
     
        # InstallationConfig
        #---------------------
     
        A PSObject with properties of its own.
        These properties should contain the property & values you would use in Install-PSFLoggingProvider.
        Invalid entries (property-names that do not match a parameter on Install-PSFLoggingProvider) in this call will cause an error loading the setting.
     
        # ProviderConfig
        #-----------------
         
        A PSObject with properties of its own.
        Or an array thereof, if you want to configure multiple instances of the same provider in one go.
        Similar to the InstallationConfig property, these property/value pairs are used to dynamically bind to Set-PSFLoggingProvider, configuring the provider.
     
     
        Example json file:
        [
            {
                "ProviderName": "logfile",
                "ProviderConfig": {
                    "InstanceName": "SystemLogInstance",
                    "FilePath": "C:\\logs\\MyTask-%date%.csv",
                    "TimeFormat": "yyyy-MM-dd HH:mm:ss.fff",
                    "Enabled": true
                }
            }
        ]
     
    .PARAMETER Path
        Path to a json file providing logging provider settings or new logging providers to load.
        Can be either a weblink or a file system path.
        See description for details on how the json file should look like.
     
    .PARAMETER Data
        The finished provider data to process.
        The PowerShell object version of the json data otherwise provided through a path.
        See description for details on how the data should look like.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Import-PSFLoggingProvider
     
        Imports the preconfigured logging provider resource file (or silently does nothing if none is configured).
     
    .EXAMPLE
        PS C:\> Import-PSFLoggingProvider -Path \\server\share\psframework\logging.json
     
        Imports the logging provider resource file from the specified network path.
#>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param (
        [Parameter(ParameterSetName = 'Path')]
        [PsfValidateScript('PSFramework.Validate.Uri.Absolute', ErrorString = 'PSFramework.Validate.Uri.Absolute')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Data')]
        $Data,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Helper Functions
        function Import-ProviderData
        {
            [CmdletBinding()]
            param (
                $Data,
                
                [AllowEmptyString()]
                [string]
                $ConfigPath
            )
            
            if ($Data.ProviderPath)
            {
                try { Install-ProviderFile -Path $Data.ProviderPath -ConfigPath $ConfigPath }
                catch { throw }
            }
            
            if ($Data.ProviderName)
            {
                if ($Data.InstallationConfig)
                {
                    $config = $Data.InstallationConfig | ConvertTo-PSFHashtable
                    Install-PSFLoggingProvider -Name $Data.ProviderName @config
                }
                foreach ($instance in $Data.ProviderConfig)
                {
                    $config = $instance | ConvertTo-PSFHashtable
                    Set-PSFLoggingProvider -Name $Data.ProviderName @config
                }
            }
        }
        
        function Install-ProviderFile
        {
            [CmdletBinding()]
            param (
                [string]
                $Path,
                
                [AllowEmptyString()]
                [string]
                $ConfigPath
            )
            
            #region Resolve Path and get code data
            $basePath = ""
            if ($ConfigPath) { $basePath = $ConfigPath -replace '[\\/][^\\/]+$' }
            
            [uri]$uri = $Path
            if (-not $uri.IsAbsoluteUri -and $ConfigPath)
            {
                switch (([uri]$basePath).Scheme)
                {
                    'https' { $uri = '{0}/{1}' -f $basePath, $Path }
                    'file' { $uri = '{0}{1}{2}' -f $basePath, [System.IO.Path]::DirectorySeparatorChar, $Path }
                }
            }
            if (-not $uri.IsAbsoluteUri) { throw "Invalid path: $Path - Cannot resolve absolute path!" }
            
            try
            {
                if ($uri.Scheme -eq 'file') { [string]$dataReceived = Get-Content -Path $uri -ErrorAction Stop -Raw }
                else { [string]$dataReceived = Invoke-WebRequest -Uri $uri -UseBasicParsing -ErrorAction Stop }
            }
            catch { throw }
            #endregion Resolve Path and get code data
            
            #region Execute provider scriptcode
            $errors = $null
            $null = [System.Management.Automation.Language.Parser]::ParseInput($dataReceived, [ref]$null, [ref]$errors)
            if ($errors) { throw "Syntax error in file processed from $uri" }
            
            $tempPath = Get-PSFPath -Name Temp
            $scriptPath = Join-Path -Path $tempPath -ChildPath "provider-$(Get-Random).ps1"
            $encoding = New-Object System.Text.UTF8Encoding($true)
            [System.IO.File]::WriteAllText($scriptPath, $dataReceived, $encoding)
            
            # Loading a file from within the module context runs the provider script from within that (trusted) context as well.
            # This has various nasty consequences in Constrained language Mode
            # We avoid this by rehoming the scriptblock to the global sessionstate
            $scriptBlock = { & $args[0] }
            [PSFramework.Utility.UtilityHost]::ImportScriptBlock($scriptBlock, $true) # $true = Import into global, rather than local sessionstate
            try { $scriptBlock.Invoke($scriptPath) }
            catch { throw }
            Remove-Item -Path $scriptPath -Force -ErrorAction Ignore
            #endregion Execute provider scriptcode
        }
        #endregion Helper Functions
    }
    process
    {
        $effectivePath = ""
        switch ($PSCmdlet.ParameterSetName)
        {
            #region Process path-based imports
            'Path'
            {
                $effectivePath = $Path
                if (-not $effectivePath) { $effectivePath = Get-PSFConfigValue -FullName 'PSFramework.Logging.Provider.Source' }
                
                # This case is relevant when adding the command "just in case", where in some environments the configuration may be provided and in others not.
                if (-not $effectivePath) { return }
                
                [uri]$uri = $effectivePath
                try
                {
                    if ($uri.Scheme -eq 'file') { $dataReceived = Get-Content -Path $effectivePath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
                    else { $dataReceived = Invoke-WebRequest -Uri $uri -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
                }
                catch
                {
                    Stop-PSFFunction -String 'Import-PSFLoggingProvider.Import.Error' -StringValues $effectivePath -ErrorRecord $_ -EnableException $EnableException
                    return
                }
            }
            #endregion Process path-based imports
            #region Process offered data
            'Data'
            {
                $dataReceived = $Data
            }
            #endregion Process offered data
        }
        foreach ($datum in $dataReceived)
        {
            try { Import-ProviderData -Data $datum }
            catch { Stop-PSFFunction -String 'Import-PSFLoggingProvider.Datum.Error' -EnableException $EnableException -ErrorRecord $_ -Continue -Target $datum }
        }
    }
}

function Install-PSFLoggingProvider
{
    <#
        .SYNOPSIS
            Installs a logging provider for the PSFramework.
         
        .DESCRIPTION
            This command installs a logging provider registered with the PSFramework.
             
            Some providers may require special installation steps, that cannot be handled by the common initialization / configuration.
            For example, a provider may require installation of binaries that require elevation.
     
            In order to cover those scenarios, a provider can include an installation script, which is called by this function.
            It can also provide additional parameters to this command, which are dynamically provided once the -Name parameter has been passed.
     
            When registering the logging provider (Using Register-PSFLoggingProvider), you can specify the logic executed by this command with these parameters:
            - IsInstalledScript : Must return $true when installation has already been performed. If this returns not $false, then this command will do nothing at all.
            - InstallationScript : The script performing the actual installation
            - InstallationParameters : A script that returns dynamic parameters. This can be used to generate additional parameters that can modify the installation process.
             
            NOTE:
            This module does not contain help/guidance on how to generate dynamic parameters!
         
        .PARAMETER Name
            The name of the provider to install
         
        .PARAMETER EnableException
            This parameters disables user-friendly warnings and enables the throwing of exceptions.
            This is less user friendly, but allows catching exceptions in calling scripts.
     
        .EXAMPLE
            PS C:\> Install-PSFLoggingProvider -Name Eventlog
     
            Installs a logging provider named 'eventlog'
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Install-PSFLoggingProvider')]
    param (
        [Alias('Provider', 'ProviderName')]
        [string]
        $Name,
        
        [switch]
        $EnableException
    )
    
    dynamicparam
    {
        if ($Name -and ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name)))
        {
            [PSFramework.Logging.ProviderHost]::Providers[$Name].InstallationParameters.InvokeGlobal()
        }
    }
    
    process
    {
        if (-not ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name)))
        {
            Stop-PSFFunction -String 'Install-PSFLoggingProvider.Provider.NotFound' -StringValues $Name -EnableException $EnableException -Category InvalidArgument -Target $Name -Tag 'logging', 'provider', 'install'
            return
        }
        
        $provider = [PSFramework.Logging.ProviderHost]::Providers[$Name]
        
        if (-not $provider.IsInstalledScript.InvokeGlobal())
        {
            try { $provider.InstallationScript.InvokeGlobal($PSBoundParameters) }
            catch
            {
                Stop-PSFFunction -String 'Install-PSFLoggingProvider.Installation.Error' -StringValues $Name -EnableException $EnableException -Target $Name -ErrorRecord $_ -Tag 'logging', 'provider', 'install'
                return
            }
        }
    }
}

function Register-PSFLoggingProvider
{
<#
    .SYNOPSIS
        Registers a new logging provider to the PSFramework logging system.
     
    .DESCRIPTION
        This function registers all components of the PSFramework logging provider systems.
        It allows you to define your own logging destination and configuration and tie them into the default logging system.
         
        In order to properly utilize its power, it becomes necessary to understand how the logging works beneath the covers:
        - On Start of the logging script, it runs a one-time scriptblock per enabled provider (this will also occur when later enabling a provider)
        - Thereafter the script will continue, logging in cycles of Start > Log all Messages > Log all Errors > End
        Each of those steps has its own event, allowing for fine control over what happens where.
        - Finally, on shutdown of a provider it again offers an option to execute some code (to dispose/free resources in use)
         
        NOTE: Logging Provider Versions
        There are two versions / generations of logging providers, that are fundamentally different from each other:
         
        Version 1
        ---------
         
        All providers share the same scope for the execution of ALL of those actions/scriptblocks!
        This makes it important to give your variables/functions a unique name, in order to avoid conflicts.
        General Guideline:
        - All variables should start with the name of the provider and an underscore. Example: $filesystem_root
        - All functions should use the name of the provider as prefix. Example: Clean-FileSystemErrorXml
         
        Version 2
        ---------
         
        Each provider runs in an isolated module context.
        A provider can have multiple instances of itself active at the same time, each with separate resource isolation.
        Additional tooling provided makes it also easier to publish complex logging providers.
        Share variables between events by making them script-scope (e.g.: $script:path)
     
    .PARAMETER Name
        A unique name for your provider. Registering a provider under a name already registered, NOTHING will happen.
        This function will instead silently terminate.
     
    .PARAMETER Version2
        Flags the provider as a second generation logging provider.
        This reduces the complexity and improves the overall user experience while adding multi-instance capability to the service.
        All new providers should be built as version2 providers.
        Generation 1 legacy providers are still supported under the PSFramework Reliability Promise
     
    .PARAMETER Enabled
        Setting this will enable the provider on registration.
     
    .PARAMETER ConfigurationRoot
        Provider instance information is stored in the configuration system.
        Assuming you would store the path location for the provider under this config setting:
        'PSFramework.Logging.LogFile.FilePath'
        Then the ConfigurationRoot would be:
        'PSFramework.Logging.LogFile'
         
        For more information on the configuration system, see:
        https://psframework.org/documentation/documents/psframework/configuration.html
     
    .PARAMETER InstanceProperties
        The properties needed to define an instance of a provider.
        Examples from the default providers:
        LogFile: 'CsvDelimiter','FilePath','FileType','Headers','IncludeHeader','Logname','TimeFormat'
        GELF: 'Encrypt','GelfServer','Port'
     
    .PARAMETER ConfigurationDefaultValues
        A hashtable containing the default values to assume when creating a new instance of a logging provider.
        This data is used during Set-PSFLoggingProvider when nothing in particular is specified for a given value.
        Instances that are defined through configuration are responsible for their full configuration set and will not be provided these values.
     
    .PARAMETER FunctionDefinitions
        If your provider instances need access to helper functions, the easiest way is to provide them using this parameter.
        Specify a scriptblock that contains your function statements with the full definition, they will be made available to the provider instances.
        Note: All logging provider instances are isolated from each other.
        Even though multiple instances will have access to equal instances, they will not share access to variables and such.
     
    .PARAMETER RegistrationEvent
        Scriptblock that should be executed on registration.
        This allows you to perform installation actions synchronously, with direct user interaction.
        At the same time, by adding it as this parameter, it will only performed on the initial registration, rather than every time the provider is registered (runspaces, Remove-Module/Import-Module)
     
    .PARAMETER BeginEvent
        The actions that should be taken once when setting up the logging.
        Can well be used to register helper functions or loading other resources that should be loaded on start only.
     
    .PARAMETER StartEvent
        The actions taken at the beginning of each logging cycle.
        Typically used to establish connections or do some necessary pre-connections.
     
    .PARAMETER MessageEvent
        The actions taken to process individual messages.
        The very act of writing logs.
        This scriptblock receives a message object (As returned by Get-PSFMessage) as first and only argument.
        Under some circumstances, this message may be a $null object, your scriptblock must be able to handle this.
     
    .PARAMETER ErrorEvent
        The actions taken to process individual error messages.
        The very act of writing logs.
        This scriptblock receives a message object (As returned by 'Get-PSFMessage -Errors') as first and only argument.
        Under some circumstances, this message may be a $null object, your scriptblock must be able to handle this.
        This consists of complex, structured data and may not be suitable to all logging formats.
        However all errors are ALWAYS accompanied by a message, making integrating this optional.
     
    .PARAMETER EndEvent
        Actions taken when finishing up a logging cycle. Can be used to close connections.
     
    .PARAMETER FinalEvent
        Final action to take when the logging terminates.
        This should release all resources reserved.
        This event will fire when:
        - The console is being closed
        - The logging script is stopped / killed
        - The logging provider is disabled
     
    .PARAMETER ConfigurationParameters
        The function Set-PSFLoggingProvider can be used to configure this logging provider.
        Using this parameter it is possible to register dynamic parameters when configuring your provider.
     
    .PARAMETER ConfigurationScript
        When using Set-PSFLoggingProvider, this script can be used to input given by the dynamic parameters generated by the -ConfigurationParameters parameter.
     
    .PARAMETER IsInstalledScript
        A scriptblock verifying that all prerequisites are properly installed.
     
    .PARAMETER InstallationScript
        A scriptblock performing the installation of the provider's prerequisites.
        Used by Install-PSFProvider in conjunction with the script provided by -InstallationParameters
     
    .PARAMETER InstallationParameters
        A scriptblock returning dynamic parameters that are offered when running Install-PSFprovider.
        Those can then be used by the installation scriptblock specified in the aptly named '-InstallationScript' parameter.
     
    .PARAMETER ConfigurationSettings
        This is executed before actually registering the scriptblock.
        It allows you to include any logic you wish, but it is specifically designed for configuration settings using Set-PSFConfig with the '-Initialize' parameter.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Register-PSFLoggingProvider -Name "filesystem" -Enabled $true -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings
         
        Registers the filesystem provider, providing events for every single occasion.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [PSFramework.PSFCore.NoJeaCommandAttribute()]
    [CmdletBinding(DefaultParameterSetName = 'Version1', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFLoggingProvider')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(ParameterSetName = 'Version2')]
        [switch]
        $Version2,
        
        [switch]
        $Enabled,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Version2')]
        [string]
        $ConfigurationRoot,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Version2')]
        [string[]]
        $InstanceProperties,
        
        [Parameter(ParameterSetName = 'Version2')]
        [Hashtable]
        $ConfigurationDefaultValues = @{ },
        
        [Parameter(ParameterSetName = 'Version2')]
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $FunctionDefinitions = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $RegistrationEvent,
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $BeginEvent = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $StartEvent = { },
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $MessageEvent,
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $ErrorEvent = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $EndEvent = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $FinalEvent = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $ConfigurationParameters = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $ConfigurationScript = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $IsInstalledScript = { $true },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $InstallationScript = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $InstallationParameters = { },
        
        [System.Management.Automation.ScriptBlock]
        [PsfValidateLanguageMode()]
        $ConfigurationSettings,
        
        [switch]
        $EnableException
    )
    
    if ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name))
    {
        return
    }
    
    if ($ConfigurationSettings) { & $ConfigurationSettings }
    if (Test-PSFParameterBinding -ParameterName Enabled)
    {
        Set-PSFConfig -FullName "LoggingProvider.$Name.Enabled" -Value $Enabled.ToBool() -DisableHandler
    }
    
    switch ($PSCmdlet.ParameterSetName)
    {
        #region Implement Version 1 Logging Provider (legacy)
        'Version1'
        {
            $provider = New-Object PSFramework.Logging.Provider
            $provider.Name = $Name
            $provider.BeginEvent = $BeginEvent
            $provider.StartEvent = $StartEvent
            $provider.MessageEvent = $MessageEvent
            $provider.ErrorEvent = $ErrorEvent
            $provider.EndEvent = $EndEvent
            $provider.FinalEvent = $FinalEvent
            $provider.ConfigurationParameters = $ConfigurationParameters
            $provider.ConfigurationScript = $ConfigurationScript
            $provider.IsInstalledScript = $IsInstalledScript
            $provider.InstallationScript = $InstallationScript
            $provider.InstallationParameters = $InstallationParameters
            
            $provider.IncludeModules = Get-PSFConfigValue -FullName "LoggingProvider.$Name.IncludeModules" -Fallback @()
            $provider.ExcludeModules = Get-PSFConfigValue -FullName "LoggingProvider.$Name.ExcludeModules" -Fallback @()
            $provider.IncludeTags = Get-PSFConfigValue -FullName "LoggingProvider.$Name.IncludeTags" -Fallback @()
            $provider.ExcludeTags = Get-PSFConfigValue -FullName "LoggingProvider.$Name.ExcludeTags" -Fallback @()
            
            $provider.InstallationOptional = Get-PSFConfigValue -FullName "LoggingProvider.$Name.InstallOptional" -Fallback $false
            
            [PSFramework.Logging.ProviderHost]::Providers[$Name] = $provider
        }
        #endregion Implement Version 1 Logging Provider (legacy)
        
        #region Implement Version 2 Logging Provider
        'Version2'
        {
            # Initialize default config for logging providers
            Set-PSFConfig -Module LoggingProvider -Name "$Name.Enabled" -Value $false -Initialize -Validation "bool" -Description "Whether the logging provider should be enabled on registration"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.AutoInstall" -Value $false -Initialize -Validation "bool" -Description "Whether the logging provider should be installed on registration"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.InstallOptional" -Value $false -Initialize -Validation "bool" -Description "Whether installing the logging provider is mandatory, in order for it to be enabled"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.IncludeModules" -Value @() -Initialize -Validation "stringarray" -Description "Module whitelist. Only messages from listed modules will be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.ExcludeModules" -Value @() -Initialize -Validation "stringarray" -Description "Module blacklist. Messages from listed modules will not be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.IncludeFunctions" -Value @() -Initialize -Validation "stringarray" -Description "Function whitelist. Only messages from listed functions will be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.ExcludeFunctions" -Value @() -Initialize -Validation "stringarray" -Description "Function blacklist. Messages from listed functions will not be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.IncludeTags" -Value @() -Initialize -Validation "stringarray" -Description "Tag whitelist. Only messages with these tags will be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.ExcludeTags" -Value @() -Initialize -Validation "stringarray" -Description "Tag blacklist. Messages with these tags will not be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.IncludeRunspaces" -Value @() -Initialize -Validation "guidarray" -Description "Runpace whitelist. Only messages from listed runspace guids will be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.ExcludeRunspaces" -Value @() -Initialize -Validation "guidarray" -Description "Runpace blacklist. Messages from listed runspace guids will not be logged"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.IncludeWarning" -Value $true -Initialize -Validation "bool" -Description "Whether to log warning messages"
            Set-PSFConfig -Module LoggingProvider -Name "$Name.MinLevel" -Value 1 -Initialize -Validation "integer1to9" -Description "The minimum message level to include in logs. Lower means more important - eg: Verbose is level 5, Host is level 2. Levels range from 1 through 9, Warning level messages are not included in this scale."
            Set-PSFConfig -Module LoggingProvider -Name "$Name.MaxLevel" -Value 9 -Initialize -Validation "integer1to9" -Description "The maximum message level to include in logs. Lower means more important - eg: Verbose is level 5, Host is level 2. Levels range from 1 through 9, Warning level messages are not included in this scale."
            Set-PSFConfig -Module LoggingProvider -Name "$Name.RequiresInclude" -Value $false -Initialize -Validation "bool" -Description "Whether any include rule must exist - and be met - before a message is accepted for logging"
            
            # Initialize custom config defined by logging provider
            foreach ($property in $InstanceProperties)
            {
                Set-PSFConfig -FullName "$ConfigurationRoot.$property" -Value $ConfigurationDefaultValues[$property] -Initialize
            }
            
            $provider = New-Object PSFramework.Logging.ProviderV2
            $provider.Name = $Name
            $provider.ConfigurationRoot = $ConfigurationRoot.Trim('.')
            $provider.InstanceProperties = $InstanceProperties
            $provider.ConfigurationDefaultValues = $ConfigurationDefaultValues
            $provider.BeginEvent = $BeginEvent
            $provider.StartEvent = $StartEvent
            $provider.MessageEvent = $MessageEvent
            $provider.ErrorEvent = $ErrorEvent
            $provider.EndEvent = $EndEvent
            $provider.FinalEvent = $FinalEvent
            $provider.Functions = $FunctionDefinitions
            $provider.ConfigurationParameters = $ConfigurationParameters
            $provider.ConfigurationScript = $ConfigurationScript
            $provider.IsInstalledScript = $IsInstalledScript
            $provider.InstallationScript = $InstallationScript
            $provider.InstallationParameters = $InstallationParameters
            $provider.InstallationOptional = Get-PSFConfigValue -FullName "LoggingProvider.$Name.InstallOptional" -Fallback $false
            
            [PSFramework.Logging.ProviderHost]::Providers[$Name] = $provider
        }
        #endregion Implement Version 2 Logging Provider
    }
    
    
    try { if ($RegistrationEvent) { & $RegistrationEvent } }
    catch
    {
        $dummy = $null
        $null = [PSFramework.Logging.ProviderHost]::Providers.TryRemove($Name, [ref]$dummy)
        Stop-PSFFunction -String 'Register-PSFLoggingProvider.RegistrationEvent.Failed' -StringValues $Name -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'register'
        return
    }
    
    #region Auto-Install & Enable
    $shouldEnable = Get-PSFConfigValue -FullName "LoggingProvider.$Name.Enabled" -Fallback $false
    $isInstalled = $provider.IsInstalledScript.InvokeGlobal()
    
    if (-not $isInstalled -and (Get-PSFConfigValue -FullName "LoggingProvider.$Name.AutoInstall" -Fallback $false))
    {
        try
        {
            Install-PSFLoggingProvider -Name $Name -EnableException
            $isInstalled = $provider.IsInstalledScript.InvokeGlobal()
        }
        catch
        {
            if ($provider.InstallationOptional)
            {
                Write-PSFMessage -Level Warning -String 'Register-PSFLoggingProvider.Installation.Failed' -StringValues $Name -ErrorRecord $_ -Tag 'logging', 'provider', 'fail', 'install' -EnableException $EnableException
            }
            else
            {
                Stop-PSFFunction -String 'Register-PSFLoggingProvider.Installation.Failed' -StringValues $Name -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'install'
                return
            }
        }
    }
    
    if ($shouldEnable)
    {
        if ($isInstalled -or $provider.InstallationOptional) { $provider.Enabled = $true }
        else
        {
            Stop-PSFFunction -String 'Register-PSFLoggingProvider.NotInstalled.Termination' -StringValues $Name -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'install'
            return
        }
    }
    #endregion Auto-Install & Enable
}

function Remove-PSFLoggingProviderRunspace
{
<#
    .SYNOPSIS
        Removes a runspace from the list of dynamically included runspaces of an active logging provider instance.
     
    .DESCRIPTION
        Removes a runspace from the list of dynamically included runspaces of an active logging provider instance.
        See the help on Add-PSFLoggingProviderRunspace for details on how and why this is desirable.
     
    .PARAMETER ProviderName
        Name of the logging provider the instance is part of.
     
    .PARAMETER InstanceName
        Name of the logging provider instance to target.
        Default: "default" (the instance created when you omit the instancename parameter on Set-PSFLoggingProvider)
     
    .PARAMETER Runspace
        The Runspace ID of the runspace to remove.
        Defaults to the current runspace.
     
    .EXAMPLE
        PS C:\> Remove-PSFLoggingProviderRunspace -ProviderName 'logfile' -InstanceName UpdateTask
     
        Removes the current runspace from the list of included runspaces on the logfile instance "UpdateTask".
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $ProviderName,
        
        [string]
        $InstanceName = 'default',
        
        [guid]
        $Runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId
    )
    
    process
    {
        $instance = Get-PSFLoggingProviderInstance -ProviderName $ProviderName -Name $InstanceName
        if ($instance) {
            $instance.RemoveRunspace($Runspace)
        }
    }
}

function Set-PSFLoggingProvider
{
<#
    .SYNOPSIS
        Configures a logging provider.
     
    .DESCRIPTION
        This command allows configuring the way a logging provider works.
        This grants the ability to ...
        - Enable / Disable a provider
        - Set additional parameters defined by the provider (each provider may implement its own settings, exposed through dynamic parameters)
        - Configure filters about what messages get sent to a given provider.
     
    .PARAMETER Name
        The name of the provider to configure
     
    .PARAMETER InstanceName
        A description of the InstanceName parameter.
     
    .PARAMETER Enabled
        Whether the provider should be enabled or disabled.
     
    .PARAMETER IncludeModules
        Only messages from modules listed here will be logged.
        Exact match only, an empty list results in all modules being logged.
     
    .PARAMETER ExcludeModules
        Messages from excluded modules will not be logged using this provider.
        Overrides -IncludeModules in case of overlap.
     
    .PARAMETER IncludeFunctions
        Only messages from functions that match at least one entry noted here will be logged.
        Uses wildcard expressions.
     
    .PARAMETER ExcludeFunctions
        Messages from functions that match at least one entry noted here will NOT be logged.
        Uses wildcard expressions.
     
    .PARAMETER IncludeRunspaces
        Only messages that come from one of the defined runspaces will be logged.
     
    .PARAMETER ExcludeRunspaces
        Messages that come from one of the defined runspaces will NOT be logged.
     
    .PARAMETER IncludeTags
        Only messages containing the listed tags will be logged.
        Exact match only, only a single match is required for a message to qualify.
     
    .PARAMETER ExcludeTags
        Messages containing any of the listed tags will not be logged.
        Overrides -IncludeTags in case of overlap.
     
    .PARAMETER MinLevel
        The minimum level of a message that will be logged.
        Note: The lower the message level, the MORE important it is.
        Levels range from 1 through 9:
        - InternalComment: 9
        - Debug: 8
        - Verbose: 5
        - Host: 2
        - Critical: 1
        The level "Warning" is not represented on this scale.
     
    .PARAMETER MaxLevel
        The maximum level of a message that will be logged.
        Note: The lower the message level, the MORE important it is.
        Levels range from 1 through 9:
        - InternalComment: 9
        - Debug: 8
        - Verbose: 5
        - Host: 2
        - Critical: 1
        The level "Warning" is not represented on this scale.
     
    .PARAMETER RequiresInclude
        By default, messages will be written to a logging provider, unless a specific exclude rule was met or any include rule was not met.
        That means, if no exclude and include rules exist at a given time, all messages will be written to the logging provider instance.
        Setting this to true will instead require at least one include rule to exist - and be met - before logging a message.
        This is designed for in particular for runspace-bound logging providers, which might at runtime swiftly gain or lose included runspaces.
     
    .PARAMETER ExcludeWarning
        Whether to exclude warnings from the logging provider / instance.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Set-PSFLoggingProvider -Name filesystem -Enabled $false
         
        Disables the filesystem provider.
     
    .EXAMPLE
        PS C:\> Set-PSFLoggingProvider -Name filesystem -ExcludeModules "PSFramework"
         
        Prevents all messages from the PSFramework module to be logged to the file system
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFLoggingProvider')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Provider', 'ProviderName')]
        [string]
        $Name,
        
        [string]
        $InstanceName,
        
        [bool]
        $Enabled,
        
        [string[]]
        $IncludeModules,
        
        [string[]]
        $ExcludeModules,
        
        [string[]]
        $IncludeFunctions,
        
        [string[]]
        $ExcludeFunctions,
        
        [guid[]]
        $IncludeRunspaces,
        
        [guid[]]
        $ExcludeRunspaces,
        
        [string[]]
        $IncludeTags,
        
        [string[]]
        $ExcludeTags,
        
        [ValidateRange(1,9)]
        [int]
        $MinLevel,
        
        [ValidateRange(1, 9)]
        [int]
        $MaxLevel,
        
        [switch]
        $RequiresInclude,
        
        [switch]
        $ExcludeWarning,
        
        [switch]
        $EnableException
    )
    
    dynamicparam
    {
        if ($Name -and ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name)))
        {
            $provider = [PSFramework.Logging.ProviderHost]::Providers[$Name]
            $results = $provider.ConfigurationParameters.InvokeGlobal() | Where-Object { $_ -is [System.Management.Automation.RuntimeDefinedParameterDictionary] }
            if (-not $results) { $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary }
            
            #region Process V2 Properties
            # Since V1 Providers do not have the property, this loop will never execute for them
            foreach ($propertyName in $provider.InstanceProperties)
            {
                $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute
                $parameterAttribute.ParameterSetName = '__AllParameterSets'
                $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                $attributesCollection.Add($parameterAttribute)
                $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($propertyName, [object], $attributesCollection)
                
                $results.Add($propertyName, $RuntimeParam)
            }
            #endregion Process V2 Properties
            
            $results
        }
    }
    
    begin
    {
        if (-not ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name)))
        {
            Stop-PSFFunction -String 'Set-PSFLoggingProvider.Provider.NotFound' -StringValues $Name -EnableException $EnableException -Category InvalidArgument -Target $Name
            return
        }
        
        $provider = [PSFramework.Logging.ProviderHost]::Providers[$Name]
        if ($InstanceName -and $provider.ProviderVersion -eq 'Version_1')
        {
            Stop-PSFFunction -String 'Set-PSFLoggingProvider.Provider.V1NoInstance' -StringValues $Name -EnableException $EnableException -Category InvalidArgument -Target $Name
            return
        }
        
        [PSFramework.Utility.UtilityHost]::ImportScriptBlock($provider.IsInstalledScript, $true)
        
        if ((-not $provider.Enabled) -and (-not $provider.IsInstalledScript.InvokeGlobal()) -and $Enabled)
        {
            Stop-PSFFunction -String 'Set-PSFLoggingProvider.Provider.NotInstalled' -StringValues $Name -EnableException $EnableException -Category InvalidOperation -Target $Name
            return
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        $provider.ConfigurationScript.InvokeGlobal($PSBoundParameters)
        
        $instanceAffix = ''
        if ($InstanceName -and ($InstanceName -ne "Default")) { $instanceAffix = "$InstanceName." }
        
        #region V2 Instance Properties
        foreach ($propertyName in $provider.InstanceProperties)
        {
            $value = $provider.ConfigurationDefaultValues[$propertyName]
            $initialize = $true
            if (Test-PSFParameterBinding -ParameterName $propertyName)
            {
                $initialize = $false
                $value = $PSBoundParameters[$propertyName]
            }
            
            Set-PSFConfig -FullName "$($provider.ConfigurationRoot).$($instanceAffix)$($propertyName)" -Value $value -Initialize:$initialize
        }
        #endregion V2 Instance Properties
        
        #region Filter Configuration
        $setProperty = -not $InstanceName -or $InstanceName -eq "Default"
        if (Test-PSFParameterBinding -ParameterName "IncludeModules")
        {
            if ($setProperty) { $provider.IncludeModules = $IncludeModules }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeModules" -Value $IncludeModules
        }
        if (Test-PSFParameterBinding -ParameterName "ExcludeModules")
        {
            if ($setProperty) { $provider.ExcludeModules = $ExcludeModules }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeModules" -Value $ExcludeModules
        }
        
        if (Test-PSFParameterBinding -ParameterName "IncludeFunctions")
        {
            if ($setProperty) { $provider.IncludeFunctions = $IncludeFunctions }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeFunctions" -Value $IncludeFunctions
        }
        if (Test-PSFParameterBinding -ParameterName "ExcludeFunctions")
        {
            if ($setProperty) { $provider.ExcludeFunctions = $ExcludeFunctions }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeFunctions" -Value $ExcludeFunctions
        }
        
        if (Test-PSFParameterBinding -ParameterName "IncludeRunspaces")
        {
            if ($setProperty) { $provider.IncludeRunspaces = $IncludeRunspaces }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeRunspaces" -Value $IncludeRunspaces
        }
        if (Test-PSFParameterBinding -ParameterName "ExcludeRunspaces")
        {
            if ($setProperty) { $provider.ExcludeRunspaces = $ExcludeRunspaces }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeRunspaces" -Value $ExcludeRunspaces
        }
        
        if (Test-PSFParameterBinding -ParameterName "IncludeTags")
        {
            if ($setProperty) { $provider.IncludeTags = $IncludeTags }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeTags" -Value $IncludeTags
        }
        if (Test-PSFParameterBinding -ParameterName "ExcludeTags")
        {
            if ($setProperty) { $provider.ExcludeTags = $ExcludeTags }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeTags" -Value $ExcludeTags
        }
        
        if ($MinLevel)
        {
            if ($setProperty) { $provider.MinLevel = $MinLevel }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MinLevel" -Value $MinLevel
        }
        if ($MaxLevel)
        {
            if ($setProperty) { $provider.MaxLevel = $MaxLevel }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MaxLevel" -Value $MaxLevel
        }
        if (Test-PSFParameterBinding -ParameterName "ExcludeWarning")
        {
            if ($setProperty) { $provider.IncludeWarning = -not $ExcludeWarning }
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeWarning" -Value (-not $ExcludeWarning)
        }
        
        # V2 Only
        if (Test-PSFParameterBinding -ParameterName "RequiresInclude"){
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)RequiresInclude" -Value $RequiresInclude
        }
        #endregion Filter Configuration
        
        if (Test-PSFParameterBinding -ParameterName "Enabled")
        {
            Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)Enabled" -Value $Enabled
            if ($provider.ProviderVersion -eq 'Version_1') { $provider.Enabled = $Enabled }
            elseif ($provider.Instances[$InstanceName])
            {
                $provider.Instances[$InstanceName].Enabled = $Enabled
            }
        }
    }
}


function Get-PSFMessage
{
    <#
        .SYNOPSIS
            Returns log entries for the PSFramework
         
        .DESCRIPTION
            Returns log entries for the PSFramework. Handy when debugging or developing a script using it.
         
        .PARAMETER FunctionName
            Default: "*"
            Only messages written by similar functions will be returned.
         
        .PARAMETER ModuleName
            Default: "*"
            Only messages written by commands from similar modules will be returned.
         
        .PARAMETER Target
            Only messages handling the specified target will be returned.
         
        .PARAMETER Tag
            Only messages containing one of these tags will be returned.
         
        .PARAMETER Last
            Only messages written by the last X executions will be returned.
            Uses Get-History to determine execution. Ignores Get-PSFmessage commands.
            By default, this will also include messages from other runspaces. If your command executes in parallel, that's useful.
            If it doesn't and you were offloading executions to other runspaces, consider also filtering by runspace using '-Runspace'
         
        .PARAMETER Skip
            How many executions to skip when specifying '-Last'.
            Has no effect without the '-Last' parameter.
         
        .PARAMETER Runspace
            The guid of the runspace to return messages from.
            By default, messages from all runspaces are returned.
            Run the following line to see the list of guids:
     
            Get-Runspace | ft Id, Name, InstanceId -Autosize
     
        .PARAMETER Level
            Limit the message selection by level.
            Message levels have a numeric value, making it easier to select a range:
             
              -Level (1..6)
     
            Will select the first 6 levels (Critical - SomewhatVerbose).
         
        .PARAMETER Errors
            Instead of log entries, the error entries will be retrieved
         
        .EXAMPLE
            Get-PSFMessage
             
            Returns all log entries currently in memory.
     
        .EXAMPLE
            Get-PSFMessage -Target "a" -Last 1 -Skip 1
     
            Returns all log entries that targeted the object "a" in the second last execution sent.
     
        .EXAMPLE
            Get-PSFMessage -Tag "fail" -Last 5
     
            Returns all log entries within the last 5 executions that contained the tag "fail"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFMessage')]
    param (
        [string]
        $FunctionName = "*",
        
        [string]
        $ModuleName = "*",
        
        [AllowNull()]
        $Target,
        
        [string[]]
        $Tag,
        
        [int]
        $Last,
        
        [int]
        $Skip = 0,
        
        [guid]
        $Runspace,
        
        [PSFramework.Message.MessageLevel[]]
        $Level,
        
        [switch]
        $Errors
    )
    
    process
    {
        if ($Errors) { $messages = [PSFramework.Message.LogHost]::GetErrors() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } }
        else { $messages = [PSFramework.Message.LogHost]::GetLog() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } }
        
        if (Test-PSFParameterBinding -ParameterName Target)
        {
            $messages = $messages | Where-Object TargetObject -EQ $Target
        }
        
        if (Test-PSFParameterBinding -ParameterName Tag)
        {
            $messages = $messages | Where-Object { $_.Tags | Where-Object { $_ -in $Tag } }
        }
        
        if (Test-PSFParameterBinding -ParameterName Runspace)
        {
            $messages = $messages | Where-Object Runspace -EQ $Runspace
        }
        
        if (Test-PSFParameterBinding -ParameterName Last)
        {
            $history = Get-History | Where-Object CommandLine -NotLike "Get-PSFMessage*" | Select-Object -Last $Last -Skip $Skip
            if ($history)
            {
                $start = $history[0].StartExecutionTime
                $end = $history[-1].EndExecutionTime
                
                $messages = $messages | Where-Object {
                    ($_.Timestamp -ge $start) -and ($_.Timestamp -le $end) -and ($_.Runspace -eq ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))
                }
            }
        }
        
        if (Test-PSFParameterBinding -ParameterName Level)
        {
            $messages = $messages | Where-Object Level -In $Level
        }
        
        return $messages
    }
}

function Get-PSFMessageLevelModifier
{
<#
    .SYNOPSIS
        Returns all registered message level modifiers with similar name.
     
    .DESCRIPTION
        Returns all registered message level modifiers with similar name.
     
        Message level modifiers are created using New-PSFMessageLevelModifier and allow dynamically modifying the actual message level written by commands.
     
    .PARAMETER Name
        Default: "*"
        A name filter - only commands that are similar to the filter will be returned.
     
    .EXAMPLE
        PS C:\> Get-PSFMessageLevelModifier
     
        Returns all message level filters
     
    .EXAMPLE
        PS C:\> Get-PSFmessageLevelModifier -Name "mymodule.*"
     
        Returns all message level filters that start with "mymodule."
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFMessageLevelModifier')]
    Param (
        [string]
        $Name = "*"
    )
    
    ([PSFramework.Message.MessageHost]::MessageLevelModifiers.Values) | Where-Object Name -Like $Name
}

function New-PSFMessageLevelModifier
{
    <#
        .SYNOPSIS
            Allows modifying message levels by powerful filters.
         
        .DESCRIPTION
            Allows modifying message levels by powerful filters.
             
            This is designed to allow a developer to have more control over what is written how during the development process.
            It also allows a debug user to fine tune what he is shown.
             
            This functionality is NOT designed for default implementation within a module.
            Instead, set healthy message levels for your own messages and leave others to tend to their own levels.
         
            Note:
            Adding too many level modifiers may impact performance, use with discretion.
         
        .PARAMETER Name
            The name of the level modifier.
            Can be arbitrary, but must be unique. Not case sensitive.
         
        .PARAMETER Modifier
            The level modifier to apply.
            - Use a negative value to make a message more relevant
            - Use a positive value to make a message less relevant
            While not limited to this range, the original levels range from 1 through 9:
            - 1-3 : Written to host and debug by default
            - 4-6 : Written to verbose and debug by default
            - 7-9 : Internas, written only to debug
         
        .PARAMETER IncludeFunctionName
            Only messages from functions with one of these exact names will be considered.
         
        .PARAMETER ExcludeFunctionName
            Messages from functions with one of these exact names will be ignored.
         
        .PARAMETER IncludeModuleName
            Only messages from modules with one of these exact names will be considered.
         
        .PARAMETER ExcludeModuleName
            Messages from module with one of these exact names will be ignored.
         
        .PARAMETER IncludeTags
            Only messages that contain one of these tags will be considered.
         
        .PARAMETER ExcludeTags
            Messages that contain one of these tags will be ignored.
         
        .PARAMETER EnableException
            This parameters disables user-friendly warnings and enables the throwing of exceptions.
            This is less user friendly, but allows catching exceptions in calling scripts.
         
        .EXAMPLE
            PS C:\> New-PSFMessageLevelModifier -Name 'MyModule-Include' -Modifier -9 -IncludeModuleName MyModule
            PS C:\> New-PSFMessageLevelModifier -Name 'MyModule-Exclude' -Modifier 9 -ExcludeModuleName MyModule
             
            These settings will cause all messages from the module 'MyModule' to be highly prioritized and almost certainly written to host.
            It will also make it highly unlikely, that messages from other modules will even be considered for anything but the lowest level.
             
            This is useful when prioritizing your own module during development.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFMessageLevelModifier')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [int]
        $Modifier,
        
        [string]
        $IncludeFunctionName,
        
        [string]
        $ExcludeFunctionName,
        
        [string]
        $IncludeModuleName,
        
        [string]
        $ExcludeModuleName,
        
        [string[]]
        $IncludeTags,
        
        [string[]]
        $ExcludeTags,
        
        [switch]
        $EnableException
    )
    
    process
    {
        if (Test-PSFParameterBinding -ParameterName IncludeFunctionName, ExcludeFunctionName, IncludeModuleName, ExcludeModuleName, IncludeTags, ExcludeTags -Not)
        {
            Stop-PSFFunction -Message "Must specify at least one condition in order to apply message level modifier!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'argument', 'message', 'level'
            return
        }
        
        $properties = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Modifier, IncludeFunctionName, ExcludeFunctionName, IncludeModuleName, ExcludeModuleName, IncludeTags, ExcludeTags
        $levelModifier = New-Object PSFramework.Message.MessageLevelModifier -Property $properties
        
        [PSFramework.Message.MessageHost]::MessageLevelModifiers[$levelModifier.Name] = $levelModifier
        
        $levelModifier
    }
}

function Register-PSFMessageEvent
{
    <#
        .SYNOPSIS
            Registers an event to when a message is written.
         
        .DESCRIPTION
            Registers an event to when a message is written.
            These events will fire whenever the written message fulfills the specified filter criteria.
     
            This allows integrating direct alerts and reactions to messages as they occur.
     
            Warnings:
            - Adding many subscriptions can impact overall performance, even without triggering.
            - Events are executed synchronously. executing complex operations may introduce a significant delay to the command execution.
     
            It is recommended to push processing that involves outside resources to a separate runspace, then use the event to pass the object as trigger.
            The TaskEngine component may prove to be just what is needed to accomplish this.
         
        .PARAMETER Name
            The name of the subscription.
            Each subscription must have a name, subscriptions of equal name will overwrite each other.
            This is in order to avoid having runspace uses explode the number of subscriptions on each invocation.
         
        .PARAMETER ScriptBlock
            The scriptblock to execute.
            It will receive the message entry (as returned by Get-PSFMessage) as its sole argument.
         
        .PARAMETER MessageFilter
            Filter by message content. Understands wildcards, but not regex.
         
        .PARAMETER ModuleNameFilter
            Filter by Name of the module, from which the message comes. Understands wildcards, but not regex.
         
        .PARAMETER FunctionNameFilter
            Filter by Name of the function, from which the message comes. Understands wildcards, but not regex.
         
        .PARAMETER TargetFilter
            Filter by target object. Performs equality comparison on an object level.
         
        .PARAMETER LevelFilter
            Include only messages of the specified levels.
         
        .PARAMETER TagFilter
            Only include messages with any of the specified tags.
         
        .PARAMETER RunspaceFilter
            Only include messages which were written by the specified runspace.
            You can find out the current runspace ID by running this:
              [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId
            You can retrieve the primary runspace - the Guid used by the runspace the user sees - by running this:
              [PSFramework.Utility.UtilityHost]::PrimaryRunspace
         
        .EXAMPLE
            PS C:\> Register-PSFMessageEvent -Name 'Mymodule.OffloadTrigger' -ScriptBlock $ScriptBlock -Tag 'engine' -Module 'MyModule' -Level Warning
     
            Registers an event subscription ...
            - Under the name 'Mymodule.OffloadTrigger' ...
            - To execute $ScriptBlock ...
            - Whenever a message is written with the tag 'engine' by the module 'MyModule' at the level 'Warning'
    #>

    [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFMessageEvent')]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,
        
        [string]
        $MessageFilter,
        
        [string]
        $ModuleNameFilter,
        
        [string]
        $FunctionNameFilter,
        
        $TargetFilter,
        
        [PSFramework.Message.MessageLevel[]]
        $LevelFilter,
        
        [string[]]
        $TagFilter,
        
        [System.Guid]
        $RunspaceFilter
    )
    
    process
    {
        $properties = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, ScriptBlock, MessageFilter, ModuleNameFilter, FunctionNameFilter, TargetFilter, LevelFilter, TagFilter, RunspaceFilter
        $eventSubscription = New-Object PSFramework.Message.MessageEventSubscription -Property $properties
        
        [PSFramework.Message.MessageHost]::Events[$Name] = $eventSubscription
    }
}

function Register-PSFMessageTransform
{
    <#
        .SYNOPSIS
            Registers a scriptblock that can transform message content.
         
        .DESCRIPTION
            Registers a scriptblock that can transform message content.
            This can be used to convert some kinds of input. Specifically:
             
            Target:
            When specifying a target, this target may require some conversion.
            For example, an object containing a live connection may need to have a static copy stored instead,
            as otherwise its export on a different runspace may cause access violations.
             
            Exceptions:
            Some exceptions may need transforming.
            For example some APIs might wrap the actual exception into a common wrapper.
            In this scenario you may want the actual exception in order to provide more specific information.
             
            In all instances, the scriptblock will be called, receiving only the relevant object as its sole input.
             
            Note: This transformation is performed synchronously on the active runspace. Complex scriptblocks may delay execution times when a matching object is passed.
         
        .PARAMETER TargetType
            The full typename of the target object to apply the scriptblock to.
            All objects of that typename will be processed through that scriptblock.
         
        .PARAMETER ExceptionType
            The full typename of the exception object to apply the scriptblock to.
            All objects of that typename will be processed through that scriptblock.
            Note: In case of error records, the type of the Exception Property is inspected. The error record as a whole will not be touched, except for having its exception exchanged.
         
        .PARAMETER ScriptBlock
            The scriptblock that performs the transformation.
         
        .PARAMETER TargetTypeFilter
            A filter for the typename of the target object to transform.
            Supports wildcards, but not regex.
            WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them!
         
        .PARAMETER ExceptionTypeFilter
            A filter for the typename of the exception object to transform.
            Supports wildcards, but not regex.
            WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them!
         
        .PARAMETER FunctionNameFilter
            Default: "*"
            Allows filtering by function name, in order to consider whether the function is affected.
            Supports wildcards, but not regex.
            WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them!
         
        .PARAMETER ModuleNameFilter
            Default: "*"
            Allows filtering by module name, in order to consider whether the function is affected.
            Supports wildcards, but not regex.
            WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them!
         
        .EXAMPLE
            PS C:\> Register-PSFMessageTransform -TargetType 'mymodule.category.classname' -ScriptBlock $ScriptBlock
             
            Whenever a target object of type 'mymodule.category.classname' is specified, invoke $ScriptBlock (with the object as sole argument) and store the result as target instead.
         
        .EXAMPLE
            PS C:\> Register-PSFMessageTransform -ExceptionType 'mymodule.category.exceptionname' -ScriptBlock $ScriptBlock
             
            Whenever an exception or error record of type 'mymodule.category.classname' is specified, invoke $ScriptBlock (with the object as sole argument) and store the result as exception instead.
            If the full error record is specified, only the updated exception will be inserted
     
        .EXAMPLE
            PS C:\> Register-PSFMessageTransform -TargetTypeFilter 'mymodule.category.*' -ScriptBlock $ScriptBlock
     
            Adds a transform for all target objects that are of a type whose full name starts with 'mymodule.category.'
            All target objects matching that typename will be run through the specified scriptblock, which in return generates the new target object.
    #>

    [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFMessageTransform')]
    Param (
        [Parameter(Mandatory = $true, ParameterSetName = "Target")]
        [string]
        $TargetType,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Exception")]
        [string]
        $ExceptionType,
        
        [Parameter(Mandatory = $true)]
        [ScriptBlock]
        $ScriptBlock,
        
        [Parameter(Mandatory = $true, ParameterSetName = "TargetFilter")]
        [string]
        $TargetTypeFilter,
        
        [Parameter(Mandatory = $true, ParameterSetName = "ExceptionFilter")]
        [string]
        $ExceptionTypeFilter,
        
        [Parameter(ParameterSetName = "TargetFilter")]
        [Parameter(ParameterSetName = "ExceptionFilter")]
        $FunctionNameFilter = "*",
        
        [Parameter(ParameterSetName = "TargetFilter")]
        [Parameter(ParameterSetName = "ExceptionFilter")]
        $ModuleNameFilter = "*"
    )
    
    process
    {
        if ($TargetType) { [PSFramework.Message.MessageHost]::TargetTransforms[$TargetType] = $ScriptBlock }
        if ($ExceptionType) { [PSFramework.Message.MessageHost]::ExceptionTransforms[$ExceptionType] = $ScriptBlock }
        
        if ($TargetTypeFilter)
        {
            $condition = New-Object PSFramework.Message.TransformCondition($TargetTypeFilter, $ModuleNameFilter, $FunctionNameFilter, $ScriptBlock, "Target")
            [PSFramework.Message.MessageHost]::TargetTransformList.Add($condition)
        }
        
        if ($ExceptionTypeFilter)
        {
            $condition = New-Object PSFramework.Message.TransformCondition($ExceptionTypeFilter, $ModuleNameFilter, $FunctionNameFilter, $ScriptBlock, "Exception")
            [PSFramework.Message.MessageHost]::ExceptionTransformList.Add($condition)
        }
    }
}

function Remove-PSFMessageLevelModifier
{
    <#
        .SYNOPSIS
            Removes a message level modifier.
         
        .DESCRIPTION
            Removes a message level modifier.
     
            Message Level Modifiers can be created by using New-PSFMessageLevelModifier.
            They are used to emphasize or deemphasize messages, in order to help with debugging.
         
        .PARAMETER Name
            Name of the message level modifier to remove.
         
        .PARAMETER Modifier
            The actual modifier to remove, as returned by Get-PSFMessageLevelModifier.
         
        .PARAMETER EnableException
            This parameters disables user-friendly warnings and enables the throwing of exceptions.
            This is less user friendly, but allows catching exceptions in calling scripts.
         
        .EXAMPLE
            PS C:\> Get-PSFMessageLevelModifier | Remove-PSFMessageLevelModifier
     
            Removes all message level modifiers, restoring everything to their default levels.
     
        .EXAMPLE
            PS C:\> Remove-PSFMessageLevelModifier -Name "mymodule.foo"
     
            Removes the message level modifier named "mymodule.foo"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Remove-PSFMessageLevelModifier')]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        [PSFramework.Message.MessageLevelModifier[]]
        $Modifier,
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($item in $Name)
        {
            if ($item -eq "PSFramework.Message.MessageLevelModifier") { continue }
            
            if ([PSFramework.Message.MessageHost]::MessageLevelModifiers.ContainsKey($item))
            {
                $dummy = $null
                $null = [PSFramework.Message.MessageHost]::MessageLevelModifiers.TryRemove($item, [ref] $dummy)
            }
            else
            {
                Stop-PSFFunction -Message "No message level modifier of name $item found!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail','input','level','message' -Continue
            }
        }
        foreach ($item in $Modifier)
        {
            if ([PSFramework.Message.MessageHost]::MessageLevelModifiers.ContainsKey($item.Name))
            {
                $dummy = $null
                $null = [PSFramework.Message.MessageHost]::MessageLevelModifiers.TryRemove($item.Name, [ref]$dummy)
            }
            else
            {
                Stop-PSFFunction -Message "No message level modifier of name $($item.Name) found!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'input', 'level', 'message' -Continue
            }
        }
    }
}

function Wait-PSFMessage
{
<#
    .SYNOPSIS
        Waits until the PSFramework log queue has been flushed.
     
    .DESCRIPTION
        Waits until the PSFramework log queue has been flushed.
        Also supports ending the logging runspace.
     
        This is designed to explicitly handle script termination for tasks that run in custom hosts that do not properly fire runspace termination events, leading to infinitely hanging tasks.
     
    .PARAMETER Timeout
        Maximum duration for the command to wait until it terminates even if there are messages left.
     
    .PARAMETER Terminate
        If this parameter is specified it will terminate the running logging runspace.
        Use this if your script will run in a powershell host that does not properly execute termination events.
        Danger!!!! Should never be used in a script that might be called by other scripts, as this might prematurely end logging!
     
    .EXAMPLE
        PS C:\> Wait-PSFMessage
     
        Waits until all pending messages are logged.
     
    .EXAMPLE
        PS C:\> Wait-PSFMessage -Timeout 1m -Terminate
     
        Waits up to one minute for all messages to be flushed, then terminates the logging runspace
#>

    [CmdletBinding()]
    param (
        [PSFDateTime]
        $Timeout = "5m",
        
        [switch]
        $Terminate
    )
    
    begin
    {
        #region Helper Functions
        function Test-LogFlushed
        {
            [OutputType([bool])]
            [CmdletBinding()]
            param (
                
            )
            
            # Catch pending messages
            if ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) { return $false }
            if ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0) { return $false }
            
            # Catch whether currently processing a message
            if ([PSFramework.Logging.ProviderHost]::LoggingState -like 'Writing') { return $false }
            if ([PSFramework.Logging.ProviderHost]::LoggingState -like 'Initializing') { return $false }
            
            return $true
        }
        #endregion Helper Functions
    }
    process
    {
        if (([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) -or ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0))
        {
            if ((Get-PSFRunspace -Name 'psframework.logging').State -notlike 'Running') { Start-PSFRunspace -Name 'psframework.logging' -NoMessage }
        }
        while ($Timeout.Value -gt (Get-Date))
        {
            if (Test-LogFlushed)
            {
                break
            }
            Start-Sleep -Milliseconds 50
        }
        
        if ($Terminate)
        {
            Stop-PSFRunspace -Name 'psframework.logging'
        }
    }
}

function Write-PSFHostColor
{
<#
    .SYNOPSIS
        Function that recognizes html-style tags to insert color into printed text.
     
    .DESCRIPTION
        Function that recognizes html-style tags to insert color into printed text.
         
        Color tags should be designed to look like this:
        <c="<console color>">Text</c>
        For example this would be a valid string:
        "This message should <c="red">partially be painted in red</c>!"
         
        This allows specifying color within strings and avoids having to piece together colored text in multiple calls to Write-Host.
        Only colors that are part of the ConsoleColor enumeration can be used. Bad colors will be ignored in favor of the default color.
     
    .PARAMETER String
        The message to write to host.
     
    .PARAMETER DefaultColor
        Default: (Get-DbaConfigValue -Name "message.infocolor")
        The color to write stuff to host in when no (or bad) color-code was specified.
     
    .PARAMETER NoNewLine
        Specifies that the content displayed in the console does not end with a newline character.
     
    .PARAMETER Level
        By default, all messages to Write-PSFHostColor will be printed to host.
        By specifying a level, it will only print the text if that level is within the range visible to the user.
     
        Visibility is controlled by the following two configuration settings:
          psframework.message.info.maximum
          psframework.message.info.minimum
     
    .EXAMPLE
        Write-PSFHostColor -String 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color'
         
        Will print the specified line in multiple colors
     
    .EXAMPLE
        $string1 = 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color'
        $string2 = '<c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color'
        $string3 = 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c>'
        $string1, $string2, $string3 | Write-PSFHostColor -DefaultColor "Magenta"
         
        Will print all three lines, respecting the color-codes, but use the color "Magenta" as default color.
     
    .EXAMPLE
        $stringLong = @"
        Dear <c="red">Sirs</c><c="green"> and</c> <c="blue">Madams</c>,
         
        it has come to our attention that you are not sufficiently <c="darkblue">awesome!</c>
        Kindly improve your <c="yellow">AP</c> (<c="magenta">awesome-ness points</c>) by at least 50% to maintain you membership in Awesome Inc!
         
        You have <c="green">27 3/4</c> days time to meet this deadline. <c="darkyellow">After this we will unfortunately be forced to rend you assunder and sacrifice your remains to the devil</c>.
         
        Best regards,
        <c="red">Luzifer</c>
        "@
        Write-PSFHostColor -String $stringLong
         
        Will print a long multiline text in its entirety while still respecting the colorcodes
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Write-PSFHostColor')]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $String,
        
        [ConsoleColor]
        $DefaultColor = (Get-PSFConfigValue -FullName "psframework.message.info.color"),
        
        [switch]
        $NoNewLine,
        
        [PSFramework.Message.MessageLevel]
        $Level
    )
    begin
    {
        $em = [PSFramework.Message.MessageHost]::InfoColorEmphasis
        $sub = [PSFramework.Message.MessageHost]::InfoColorSubtle
        
        $max_info = [PSFramework.Message.MessageHost]::MaximumInformation
        $min_info = [PSFramework.Message.MessageHost]::MinimumInformation
    }
    process
    {
        if ($Level)
        {
            if (($max_info -lt $Level) -or ($min_info -gt $Level)) { return }
        }
        
        foreach ($line in $String)
        {
            foreach ($row in $line.Split("`n")) #.Split([environment]::NewLine))
            {
                if ($row -notlike '*<c=["'']*["'']>*</c>*') { Microsoft.PowerShell.Utility\Write-Host -Object $row -ForegroundColor $DefaultColor -NoNewline:$NoNewLine }
                else
                {
                    $row = $row -replace '<c=["'']em["'']>', "<c='$em'>" -replace '<c=["'']sub["'']>', "<c='$sub'>"
                    $match = ($row | Select-String '<c=["''](.*?)["'']>(.*?)</c>' -AllMatches).Matches
                    $index = 0
                    $count = 0
                    
                    while ($count -le $match.Count)
                    {
                        if ($count -lt $Match.Count)
                        {
                            Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index, ($match[$count].Index - $Index)) -ForegroundColor $DefaultColor -NoNewline
                            try { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $match[$count].Groups[1].Value -NoNewline -ErrorAction Stop }
                            catch { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $DefaultColor -NoNewline -ErrorAction Stop }
                            
                            $index = $match[$count].Index + $match[$count].Length
                            $count++
                        }
                        else
                        {
                            Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index) -ForegroundColor $DefaultColor -NoNewline:$NoNewLine
                            $count++
                        }
                    }
                }
            }
        }
    }
}


function Write-PSFMessageProxy
{
<#
    .SYNOPSIS
        A proxy command that allows smoothly redirecting messages to Write-PSFMessage.
     
    .DESCRIPTION
        This function is designed to pick up the alias it was called by and to redirect the message that was sent to Write-PSFMessage.
        For example, by creating an alias for Write-Host pointing at 'Write-PSFMessageProxy' will cause it to redirect the message at 'Important' level (which is written to host by default, but also logged).
         
        By creating those aliases, it becomes easy to shift current scripts to use the logging, without having to actually update the code.
     
    .PARAMETER Message
        The message to write.
     
    .PARAMETER NoNewline
        Dummy parameter to make Write-Host redirection happy.
        IT WILL BE IGNORED!
     
    .PARAMETER Separator
        Dummy parameter to make Write-Host redirection happy.
        IT WILL BE IGNORED!
     
    .PARAMETER ForegroundColor
        Configure the foreground color for host messages.
     
    .PARAMETER BackgroundColor
        Dummy parameter to make Write-Host redirection happy.
        IT WILL BE IGNORED!
     
    .PARAMETER Tags
        Add tags to the messages.
     
    .EXAMPLE
        PS C:\> Write-PSFMessageProxy "Example Message"
         
        Will write the message "Example Message" to verbose.
     
    .EXAMPLE
        PS C:\> Set-Alias Write-Host Write-PSFMessageProxy
        PS C:\> Write-Host "Example Message"
         
        This will create an alias named "Write-Host" pointing at "Write-PSFMessageProxy".
        Then it will write the message "Example Message", which is automatically written to Level "Important" (which by default will be written to host).
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Write-PSFMessageProxy')]
    param (
        [Parameter(Position = 0)]
        [Alias('Object', 'MessageData')]
        [string]
        $Message,
        
        [switch]
        $NoNewline,
        
        $Separator,
        
        [System.ConsoleColor]
        $ForegroundColor,
        
        [System.ConsoleColor]
        $BackgroundColor,
        
        [string[]]
        $Tags = 'proxied'
    )
    
    begin
    {
        $call = (Get-PSCallStack)[0].InvocationInfo
        $callStack = (Get-PSCallStack)[1]
        $FunctionName = $callStack.Command
        $ModuleName = $callstack.InvocationInfo.MyCommand.ModuleName
        if (-not $ModuleName) { $ModuleName = "<Unknown>" }
        $File = $callStack.Position.File
        $Line = $callStack.Position.StartLineNumber
        
        $splatParam = @{
            Tag             = $Tags
            FunctionName = $FunctionName
            ModuleName   = $ModuleName
            File         = $File
            Line         = $Line
        }
        
        # Adapt chosen forgroundcolor
        if (Test-PSFParameterBinding -ParameterName ForegroundColor)
        {
            $Message = "<c='$($ForegroundColor)'>{0}</c>" -f $Message
        }
    }
    process
    {
        switch ($call.InvocationName)
        {
            "Write-Host" { Write-PSFMessage -Level Important -Message $Message @splatParam }
            "Write-Verbose" { Write-PSFMessage -Level Verbose -Message $Message @splatParam }
            "Write-Warning" { Write-PSFMessage -Level Warning -Message $Message @splatParam }
            "Write-Debug" { Write-PSFMessage -Level System -Message $Message @splatParam }
            "Write-Information" { Write-PSFMessage -Level Important -Message $Message @splatParam }
            default { Write-PSFMessage -Level Verbose -Message $Message @splatParam }
        }
    }
}

function Get-PSFPipeline
{
<#
    .SYNOPSIS
        Generates meta-information for the pipeline from the calling command.
     
    .DESCRIPTION
        Generates meta-information for the pipeline from the calling command.
     
    .EXAMPLE
        PS C:\> Get-Pipeline
     
        Generates meta-information for the pipeline from the calling command.
#>

    [OutputType([PSFramework.Meta.Pipeline])]
    [CmdletBinding()]
    param (
        
    )
    
    begin
    {
        function Get-PrivateProperty
        {
            [CmdletBinding()]
            param (
                $Object,
                
                [string]
                $Name,
                
                [ValidateSet('Any', 'Field', 'Property')]
                [string]
                $Type = 'Any'
            )
            
            if ($null -eq $Object) { return }
            
            $typeObject = $Object.GetType()
            [System.Reflection.BindingFlags]$flags = "NonPublic, Instance"
            switch ($Type)
            {
                'Field'
                {
                    $field = $typeObject.GetField($Name, $flags)
                    $field.GetValue($Object)
                }
                'Property'
                {
                    $property = $typeObject.GetProperty($Name, $flags)
                    $property.GetValue($Object)
                }
                'Any'
                {
                    $field = $typeObject.GetField($Name, $flags)
                    if ($field) { return $field.GetValue($Object) }
                    $property = $typeObject.GetProperty($Name, $flags)
                    $property.GetValue($Object)
                }
            }
        }
    }
    process
    {
        $callerCmdlet = (Get-PSCallStack)[1].GetFrameVariables()["PSCmdlet"].Value
        
        $commandRuntime = Get-PrivateProperty -Object $callerCmdlet -Name _commandRuntime -Type Field
        $pipelineProcessor = Get-PrivateProperty -Object $commandRuntime -Name PipelineProcessor -Type Property
        $localPipeline = Get-PrivateProperty -Object $pipelineProcessor -Name LocalPipeline -Type Property
        
        $pipeline = New-Object PSFramework.Meta.Pipeline -Property @{
            InstanceId = $localPipeline.InstanceId
            StartTime  = Get-PrivateProperty -Object $localPipeline -Name _pipelineStartTime -Type Field
            Text       = Get-PrivateProperty -Object $localPipeline -Name HistoryString -Type Property
            PipelineItem = $localPipeline
        }
        
        if ($pipeline.Text)
        {
            $tokens = $null
            $errorItems = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($pipeline.Text, [ref]$tokens, [ref]$errorItems)
            $pipeline.Ast = $ast
            
            $baseItem = $ast.EndBlock.Statements[0]
            if ($baseItem -is [System.Management.Automation.Language.AssignmentStatementAst])
            {
                $pipeline.OutputAssigned = $true
                $pipeline.OutputAssignedTo = $baseItem.Left
                $baseItem = $baseItem.Right.PipelineElements
            }
            else { $baseItem = $baseItem.PipelineElements }
            
            if ($baseItem[0] -is [System.Management.Automation.Language.CommandExpressionAst])
            {
                if ($baseItem[0].Expression -is [System.Management.Automation.Language.VariableExpressionAst])
                {
                    $pipeline.InputFromVariable = $true
                    $pipeline.InputVariable = $baseItem[0].Expression.VariablePath.UserPath
                }
                else { $pipeline.InputDirect = $true }
                if ($baseItem[0].Expression -is [System.Management.Automation.Language.ConstantExpressionAst])
                {
                    $pipeline.InputValue = $baseItem[0].Expression.Value
                }
                elseif ($baseItem[0].Expression -is [System.Management.Automation.Language.ArrayLiteralAst])
                {
                    $pipeline.InputValue = @()
                    foreach ($element in $baseItem[0].Expression.Elements)
                    {
                        if ($element -is [System.Management.Automation.Language.ConstantExpressionAst])
                        {
                            $pipeline.InputValue += $element.Value
                        }
                        else { $pipeline.InputValue += $element }
                    }
                }
                else { $pipeline.InputValue = $baseItem[0].Expression }
            }
        }
        
        $commands = Get-PrivateProperty -Object $pipelineProcessor -Name Commands -Type Property
        $index = 0
        foreach ($command in $commands)
        {
            $commandItem = Get-PrivateProperty -Object $command -Name Command
            $pipeline.Commands.Add((New-Object PSFramework.Meta.PipelineCommand($pipeline.InstanceId, $index, (Get-PrivateProperty -Object $command -Name CommandInfo), $commandItem.MyInvocation, $commandItem)))
            $index++
        }
        
        $pipeline
    }
}

function Clear-PSFResultCache
{
    <#
        .SYNOPSIS
            Clears the result cache
         
        .DESCRIPTION
            Clears the result cache, which can come in handy if you have a huge amount of data stored within and want to free the memory.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Clear-PSFResultCache
     
            Clears the result cache, freeing up any used memory.
    #>

    [CmdletBinding(ConfirmImpact = 'Low', SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Clear-PSFresultCache')]
    param (
        
    )
    
    process
    {
        if (Test-PSFShouldProcess -Target 'Result Cache' -ActionString 'Clear-PSFResultCache.Clear' -PSCmdlet $PSCmdlet)
        {
            [PSFramework.ResultCache.ResultCache]::Clear()
        }
    }
}

function Get-PSFResultCache
{
<#
    .SYNOPSIS
        Returns the last stored result
     
    .DESCRIPTION
        Functions that implement the result cache store their information in the cache. This can then be retrieved by the user running this command.
        This forgives the user for forgetting to store the output in a variable and is especially precious when running commands that take a while to execute.
     
    .PARAMETER Type
        Default: Value
        Options: All, Value
        By default, this function will return the output that was cached during the last execution. However, this mode can be switched:
        - All: Returns everything that has been cached. This includes the name of the command calling Set-PFSResultCache as well as the timestamp when it was called.
        - Value: Returns just the object(s) that were written to cache
     
    .EXAMPLE
        PS C:\> Get-PSFResultCache
     
        Returns the latest cached result.
     
    .EXAMPLE
        PS C:\> Get-PSFResultCache -Type 'All'
     
        Returns a meta-information object containing the last result, when it was written and which function did the writing.
#>

    
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFResultCache')]
    param (
        [ValidateSet('Value','All')]
        [string]
        $Type = 'Value'
    )
    
    switch ($Type)
    {
        'All'
        {
            [pscustomobject]@{
                Result    = ([PSFramework.ResultCache.ResultCache]::Result)
                Function  = ([PSFramework.ResultCache.ResultCache]::Function)
                Timestamp = ([PSFramework.ResultCache.ResultCache]::Timestamp)
            }
        }
        'Value'
        {
            [PSFramework.ResultCache.ResultCache]::Result
        }
    }
}
if (-not (Test-Path "alias:Get-LastResult")) { New-Alias -Name Get-LastResult -Value Get-PSFResultCache -Description "A more intuitive name for users to call Get-PSFResultCache" }
if (-not (Test-Path "alias:glr")) { New-Alias -Name glr -Value Get-PSFResultCache -Description "A faster name for users to call Get-PSFResultCache" }

function Set-PSFResultCache
{
<#
    .SYNOPSIS
        Stores a result in the result cache
     
    .DESCRIPTION
        Stores a result in the result cache.
        This function is designed for use in other functions, a user should never have cause to use it directly himself.
     
    .PARAMETER InputObject
        The value to store in the result cache.
     
    .PARAMETER DisableCache
        Allows you to control, whether the function actually writes to the cache. Useful when used in combination with -PassThru.
        Does not suppress output via -PassThru. However in combination, these two parameters make caching within a pipeline practical.
     
    .PARAMETER PassThru
        The objects that are being cached are passed through this function.
        By default, Set-PSFResultCache doesn't have any output.
     
    .PARAMETER CommandName
        Default: (Get-PSCallStack)[0].Command
        The name of the command that called Set-PSFResultCache.
        Is automatically detected and usually doesn't need to be changed.
     
    .EXAMPLE
        PS C:\> Set-PSFResultCache -InputObject $Results -DisableCache $NoRes
         
        Stores the contents of $Results in the result cache, but does nothing if $NoRes is $true (the default Switch-name for disabling the result cache)
     
    .EXAMPLE
        PS C:\> Get-ChildItem $path | Get-Acl | Set-PSFResultCache -DisableCache $NoRes -PassThru
         
        Gets all items in $Path, then retrieves each of their Acl, finally it stores those in the result cache (if it isn't disabled via $NoRes) and finally passes each Acl through for the user to see.
        This will return all objects, even if $NoRes is set to $True.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFResultCache')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [AllowNull()]
        [Alias('Value')]
        [Object]
        $InputObject,
        
        [boolean]
        $DisableCache = $false,
        
        [Switch]
        $PassThru,
        
        [string]
        $CommandName = (Get-PSCallStack)[0].Command
    )
    
    begin
    {
        $isPipeline = -not $PSBoundParameters.ContainsKey("InputObject")
        [PSFramework.ResultCache.ResultCache]::Function = $CommandName
        
        if ($isPipeline -and -not $DisableCache)
        {
            [PSFramework.ResultCache.ResultCache]::Result = [System.Collections.ArrayList]@()
        }
    }
    process
    {
        if (-not $DisableCache)
        {
            if ($isPipeline) { $null = [PSFramework.ResultCache.ResultCache]::Result.Add($InputObject) }
            else { [PSFramework.ResultCache.ResultCache]::Result = $InputObject }
        }
        if ($PassThru) { $InputObject }
    }
    end
    {
        if ($isPipeline -and -not $DisableCache)
        {
            [PSFramework.ResultCache.ResultCache]::Result = [PSFramework.ResultCache.ResultCache]::Result.ToArray()
        }
    }
}

function Get-PSFDynamicContentObject
{
<#
    .SYNOPSIS
        Retrieves a named value object that can be updated from another runspace.
     
    .DESCRIPTION
        Retrieves a named value object that can be updated from another runspace.
     
        This comes in handy to have a variable that is automatically updated.
        Use this function to receive an object under a given name.
        Use Set-PSFDynamicContentObject to update the value of the object.
     
        It matters not from what runspace you update the object.
     
        Note:
        When planning to use such an object, keep in mind that it can easily change its content at any given time.
     
    .PARAMETER Name
        The name of the object to retrieve.
        Will create an empty value object if the object doesn't already exist.
     
    .EXAMPLE
        PS C:\> Get-PSFDynamicContentObject -Name "Test"
     
        Returns the Dynamic Content Object named "test"
#>

    [OutputType([PSFramework.Utility.DynamicContentObject])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFDynamicContentObject')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name
    )
    
    process
    {
        foreach ($item in $Name)
        {
            [PSFramework.Utility.DynamicContentObject]::Get($Name)
        }
    }
}

function Get-PSFRunspace
{
<#
    .SYNOPSIS
        Returns registered runspaces.
     
    .DESCRIPTION
        Returns a list of runspaces that have been registered with the PSFramework
     
    .PARAMETER Name
        Default: "*"
        Only registered runspaces of similar names are returned.
     
    .EXAMPLE
        PS C:\> Get-PSFRunspace
     
        Returns all registered runspaces
     
    .EXAMPLE
        PS C:\> Get-PSFRunspace -Name 'mymodule.maintenance'
     
        Returns the runspace registered under the name 'mymodule.maintenance'
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFRunspace')]
    Param (
        [string]
        $Name = "*"
    )
    
    process
    {
        [PSFramework.Runspace.RunspaceHost]::Runspaces.Values | Where-Object Name -Like $Name
    }
}

function Register-PSFRunspace
{
<#
    .SYNOPSIS
        Registers a scriptblock to run in the background.
     
    .DESCRIPTION
        This function registers a scriptblock to run in separate runspace.
        This is different from most runspace solutions, in that it is designed for permanent background tasks that need to be done.
        It guarantees a single copy of the task to run within the powershell process, even when running the same module in many runspaces in parallel.
         
        The scriptblock must be built with some rules in mind, for details on using this system run:
        Get-Help about_psf_runspace
     
        Updating:
        If this function is called multiple times, targeting the same name, it will update the scriptblock.
        - If that scriptblock is the same as the previous scriptblock, nothing changes
        - If that scriptblock is different from the previous ones, it will be registered, but will not be executed right away!
          Only after stopping and starting the runspace will it operate under the new scriptblock.
     
    .PARAMETER ScriptBlock
        The scriptblock to run in a dedicated runspace.
     
    .PARAMETER Name
        The name to register the scriptblock under.
     
    .PARAMETER NoMessage
        Setting this will prevent messages be written to the message / logging system.
        This is designed to make the PSFramework not flood the log on each import.
     
    .EXAMPLE
        PS C:\> Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'mymodule.maintenance'
     
        Registers the script defined in $scriptBlock under the name 'mymodule.maintenance'
        It does not start the runspace yet. If it already exists, it will overwrite the scriptblock without affecting the running script.
     
    .EXAMPLE
        PS C:\> Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'mymodule.maintenance'
        PS C:\> Start-PSFRunspace -Name 'mymodule.maintenance'
     
        Registers the script defined in $scriptBlock under the name 'mymodule.maintenance'
        Then it starts the runspace, running the registered $scriptBlock
#>

    [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFRunspace')]
    param
    (
        [Parameter(Mandatory = $true)]
        [Scriptblock]
        $ScriptBlock,
        
        [Parameter(Mandatory = $true)]
        [String]
        $Name,
        
        [switch]
        $NoMessage
    )
    
    if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($Name))
    {
        if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Updating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' }
        [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name].SetScript($ScriptBlock)
    }
    else
    {
        if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Creating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' }
        [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name] = New-Object PSFramework.Runspace.RunspaceContainer($Name, $ScriptBlock)
    }
}

function Set-PSFDynamicContentObject
{
<#
    .SYNOPSIS
        Updates a value object that can easily be accessed on another runspace.
     
    .DESCRIPTION
        Updates a value object that can easily be accessed on another runspace.
         
        The Dynamic Content Object system allows the user to easily have the content of a variable updated in the background.
        The update is performed by this very function.
     
    .PARAMETER Name
        The name of the value to update.
        Not case sensitive.
     
    .PARAMETER Object
        The value object to update
     
    .PARAMETER Value
        The value to apply
     
    .PARAMETER Queue
        Set the object to be a threadsafe queue.
        Safe to use in multiple runspaces in parallel.
        Will not apply changes if the current value is already such an object.
     
    .PARAMETER Stack
        Set the object to be a threadsafe stack.
        Safe to use in multiple runspaces in parallel.
        Will not apply changes if the current value is already such an object.
     
    .PARAMETER List
        Set the object to be a threadsafe list.
        Safe to use in multiple runspaces in parallel.
        Will not apply changes if the current value is already such an object.
     
    .PARAMETER Dictionary
        Set the object to be a threadsafe dictionary.
        Safe to use in multiple runspaces in parallel.
        Will not apply changes if the current value is already such an object.
     
    .PARAMETER PassThru
        Has the command returning the object just set.
     
    .PARAMETER Reset
        Clears the dynamic content object's collection objects.
        Use this to ensure the collection is actually empty.
        Only used in combination of either -Queue, -Stack, -List or -Dictionary.
     
    .EXAMPLE
        PS C:\> Set-PSFDynamicContentObject -Name Test -Value $Value
         
        Sets the Dynamic Content Object named "test" to the value $Value.
     
    .EXAMPLE
        PS C:\> Set-PSFDynamicContentObject -Name MyModule.Value -Queue
         
        Sets the Dynamic Content Object named "MyModule.Value" to contain a threadsafe queue.
        This queue will be safe to enqueue and dequeue from, no matter the number of runspaces accessing it simultaneously.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [OutputType([PSFramework.Utility.DynamicContentObject])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFDynamicContentObject')]
    Param (
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        [PSFramework.Utility.DynamicContentObject[]]
        $Object,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Value')]
        [AllowNull()]
        $Value = $null,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Queue')]
        [switch]
        $Queue,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Stack')]
        [switch]
        $Stack,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'List')]
        [switch]
        $List,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Dictionary')]
        [switch]
        $Dictionary,
        
        [switch]
        $PassThru,
        
        [switch]
        $Reset
    )
    
    process
    {
        foreach ($item in $Name)
        {
            if ($Queue) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Queue') }
            elseif ($Stack) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Stack') }
            elseif ($List) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'List') }
            elseif ($Dictionary) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Dictionary') }
            else { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Common') }
            
            if ($PassThru) { [PSFramework.Utility.DynamicContentObject]::Get($item) }
        }
        
        foreach ($item in $Object)
        {
            $item.Value = $Value
            if ($Queue) { $item.ConcurrentQueue($Reset) }
            if ($Stack) { $item.ConcurrentStack($Reset) }
            if ($List) { $item.ConcurrentList($Reset) }
            if ($Dictionary) { $item.ConcurrentDictionary($Reset) }
            
            if ($PassThru) { $item }
        }
    }
}

function Start-PSFRunspace
{
<#
    .SYNOPSIS
        Starts a runspace that was registered to the PSFramework
     
    .DESCRIPTION
        Starts a runspace that was registered to the PSFramework
        Simply registering does not automatically start a given runspace. Only by executing this function will it take effect.
     
    .PARAMETER Name
        The name of the registered runspace to launch
     
    .PARAMETER Runspace
        The runspace to launch. Returned by Get-PSFRunspace
     
    .PARAMETER NoMessage
        Setting this will prevent messages be written to the message / logging system.
        This is designed to make the PSFramework not flood the log on each import.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Start-PSFRunspace -Name 'mymodule.maintenance'
         
        Starts the runspace registered under the name 'mymodule.maintenance'
#>

    [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Start-PSFRunspace')]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        [PSFramework.Runspace.RunspaceContainer[]]
        $Runspace,
        
        [switch]
        $NoMessage,
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($item in $Name)
        {
            # Ignore all output from Get-PSFRunspace - it'll be handled by the second loop
            if ($item -eq "psframework.runspace.runspacecontainer") { continue }
            
            if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($item))
            {
                if ($PSCmdlet.ShouldProcess($item, "Starting Runspace"))
                {
                    try
                    {
                        if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Start-PSFRunspace.Starting' -StringValues ($item) -Target $item -Tag "runspace", "start" }
                        [PSFramework.Runspace.RunspaceHost]::Runspaces[$item].Start()
                    }
                    catch
                    {
                        Stop-PSFFunction -String 'Start-PSFRunspace.Starting.Failed' -StringValues $item -ErrorRecord $_ -EnableException $EnableException -Tag "fail", "argument", "runspace", "start" -Target $item -Continue
                    }
                }
            }
            else
            {
                Stop-PSFFunction -String 'Start-PSFRunspace.UnknownRunspace' -StringValues $item -EnableException $EnableException -Category InvalidArgument -Tag "fail", "argument", "runspace", "start" -Target $item -Continue
            }
        }
        
        foreach ($item in $Runspace)
        {
            if ($PSCmdlet.ShouldProcess($item.Name, "Starting Runspace"))
            {
                try
                {
                    if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Start-PSFRunspace.Starting' -StringValues $item.Name -Target $item -Tag "runspace", "start" }
                    $item.Start()
                }
                catch
                {
                    Stop-PSFFunction -String 'Start-PSFRunspace.Starting.Failed' -StringValues $item.Name -EnableException $EnableException -Tag "fail", "argument", "runspace", "start" -Target $item -Continue
                }
            }
        }
    }
}

function Stop-PSFRunspace
{
<#
    .SYNOPSIS
        Stops a runspace that was registered to the PSFramework
     
    .DESCRIPTION
        Stops a runspace that was registered to the PSFramework
        Will not cause errors if the runspace is already halted.
         
        Runspaces may not automatically terminate immediately when calling this function.
        Depending on the implementation of the scriptblock, this may in fact take a little time.
        If the scriptblock hasn't finished and terminated the runspace in a seemingly time, it will be killed by the system.
        This timeout is by default 30 seconds, but can be altered by using the Configuration System.
        For example, this line will increase the timeout to 60 seconds:
        Set-PSFConfig -FullName PSFramework.Runspace.StopTimeout -Value 60
     
    .PARAMETER Name
        The name of the registered runspace to stop
     
    .PARAMETER Runspace
        The runspace to stop. Returned by Get-PSFRunspace
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Stop-PSFRunspace -Name 'mymodule.maintenance'
         
        Stops the runspace registered under the name 'mymodule.maintenance'
     
    .NOTES
        Additional information about the function.
#>

    [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFRunspace')]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        [PSFramework.Runspace.RunspaceContainer[]]
        $Runspace,
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($item in $Name)
        {
            # Ignore all output from Get-PSFRunspace - it'll be handled by the second loop
            if ($item -eq "psframework.runspace.runspacecontainer") { continue }
            
            if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($item))
            {
                if ($PSCmdlet.ShouldProcess($item, "Stopping Runspace"))
                {
                    try
                    {
                        Write-PSFMessage -Level Verbose -String 'Stop-PSFRunspace.Stopping' -StringValues ($item) -Target $item -Tag "runspace", "stop"
                        [PSFramework.Runspace.RunspaceHost]::Runspaces[$item].Stop()
                    }
                    catch
                    {
                        Stop-PSFFunction -String 'Stop-PSFRunspace.Stopping.Failed' -StringValues ($item) -EnableException $EnableException -Tag "fail", "argument", "runspace", "stop" -Target $item -Continue -ErrorRecord $_
                    }
                }
            }
            else
            {
                Stop-PSFFunction -String 'Stop-PSFRunspace.UnknownRunspace' -StringValues ($item) -EnableException $EnableException -Category InvalidArgument -Tag "fail", "argument", "runspace", "stop" -Target $item -Continue
            }
        }
        
        foreach ($item in $Runspace)
        {
            if ($PSCmdlet.ShouldProcess($item.Name, "Stopping Runspace"))
            {
                try
                {
                    Write-PSFMessage -Level Verbose -String 'Stop-PSFRunspace.Stopping' -StringValues $item.Name -Target $item -Tag "runspace", "stop"
                    $item.Stop()
                }
                catch
                {
                    Stop-PSFFunction -String 'Stop-PSFRunspace.Stopping.Failed' -StringValues $item.Name -EnableException $EnableException -Tag "fail", "argument", "runspace", "stop" -Target $item -Continue -ErrorRecord $_
                }
            }
        }
    }
}

function ConvertFrom-PSFClixml
{
<#
    .SYNOPSIS
        Converts data that was serialized from an object back into that object.
     
    .DESCRIPTION
        Converts data that was serialized from an object back into that object.
     
        Use Import-PSFclixml to restore objects serialized and written to file.
        This command is designed for converting serialized data in memory, for example to expand objects returned by a network api.
     
    .PARAMETER InputObject
        The serialized data to restore to objects.
     
    .EXAMPLE
        PS C:\> $data | ConvertFrom-PSFClixml
     
        Converts the data stored in $data back into objects
#>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        $InputObject
    )
    
    begin
    {
        $byteList = New-Object System.Collections.ArrayList
        
        function Convert-Item
        {
            [CmdletBinding()]
            param (
                $Data
            )
            
            if ($Data -is [System.String])
            {
                try { [PSFramework.Serialization.ClixmlSerializer]::FromStringCompressed($Data) }
                catch { [PSFramework.Serialization.ClixmlSerializer]::FromString($Data) }
            }
            else
            {
                try { [PSFramework.Serialization.ClixmlSerializer]::FromByteCompressed($Data) }
                catch { [PSFramework.Serialization.ClixmlSerializer]::FromByte($Data) }
            }
        }
    }
    process
    {
        if ($InputObject -is [string]) { Convert-Item -Data $InputObject }
        elseif ($InputObject -is [System.Byte[]]) { Convert-Item -Data $InputObject }
        elseif ($InputObject -is [System.Byte]) { $null = $byteList.Add($InputObject) }
        else { Stop-PSFFunction -String 'ConvertFrom-PSFClixml.BadInput' -EnableException $true }
    }
    end
    {
        if ($byteList.Count -gt 0)
        {
            Convert-Item -Data ([System.Byte[]]$byteList.ToArray())
        }
    }
}

function ConvertTo-PSFClixml
{
<#
    .SYNOPSIS
        Converts an input object into a serialized string or byte array.
     
    .DESCRIPTION
        Converts an input object into a serialized string or byte array.
        Works analogous to Export-PSFClixml, only it does not require being written to file.
     
    .PARAMETER Depth
        Specifies how many levels of contained objects are included in the XML representation. The default value is 2.
     
    .PARAMETER InputObject
        The object(s) to serialize.
     
    .PARAMETER Style
        Whether to export as byte (better compression) or as string (often easier to transmit using other utilities/apis).
     
    .PARAMETER NoCompression
        By default, exported data is compressed, saving a lot of storage at the cost of some CPU cycles.
        This switch disables this compression, making string-style exports compatible with Import-Clixml.
     
    .EXAMPLE
        PS C:\> Get-ChildItem | ConvertTo-PSFClixml
     
        Scans all items in the current folder and then converts that into a compressed clixml string.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding()]
    param (
        [int]
        $Depth,
        
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [PSFramework.Serialization.ClixmlDataStyle]
        $Style = 'String',
        
        [switch]
        $NoCompression
    )
    
    begin
    {
        $data = @()
    }
    process
    {
        $data += $InputObject
    }
    end
    {
        try
        {
            if ($Style -like 'Byte')
            {
                if ($NoCompression)
                {
                    if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToByte($data, $Depth) }
                    else { [PSFramework.Serialization.ClixmlSerializer]::ToByte($data) }
                }
                else
                {
                    if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data, $Depth) }
                    else { [PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data) }
                }
            }
            else
            {
                if ($NoCompression)
                {
                    if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToString($data, $Depth) }
                    else { [PSFramework.Serialization.ClixmlSerializer]::ToString($data) }
                }
                else
                {
                    if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data, $Depth) }
                    else { [PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data) }
                }
            }
        }
        catch
        {
            Stop-PSFFunction -String 'ConvertTo-PSFClixml.Conversion.Error' -ErrorRecord $_ -EnableException $true -Target $resolvedPath -Cmdlet $PSCmdlet
        }
    }
}

function Export-PSFClixml
{
<#
    .SYNOPSIS
        Writes objects to the filesystem.
     
    .DESCRIPTION
        Writes objects to the filesystem.
        In opposite to the default Export-Clixml cmdlet, this function offers data compression as the default option.
         
        Exporting to regular clixml is still supported though.
     
    .PARAMETER Path
        The path to write to.
     
    .PARAMETER Depth
        Specifies how many levels of contained objects are included in the XML representation. The default value is 2.
     
    .PARAMETER InputObject
        The object(s) to serialize.
     
    .PARAMETER Style
        Whether to export as byte (better compression) or as string (often easier to transmit using other utilities/apis).
     
    .PARAMETER NoCompression
        By default, exported data is compressed, saving a lot of storage at the cost of some CPU cycles.
        This switch disables this compression, making string-style exports compatible with Import-Clixml.
     
    .PARAMETER PassThru
        Passes all objects along the pipeline.
        By default, Export-PSFClixml does not produce output.
     
    .PARAMETER Encoding
        The encoding to use when using string-style export.
        By default, it exports as UTF8 encoding.
     
    .EXAMPLE
        PS C:\> Get-ChildItem | Export-PSFClixml -Path 'C:\temp\data.byte'
         
        Exports a list of all items in the current path as compressed binary file to C:\temp\data.byte
     
    .EXAMPLE
        PS C:\> Get-ChildItem | Export-PSFClixml -Path C:\temp\data.xml -Style 'String' -NoCompression
         
        Exports a list of all items in the current path as a default clixml readable by Import-Clixml
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Export-PSFClixml')]
    param (
        [PsfValidateScript('PSFramework.Validate.FSPath.FileOrParent', ErrorString = 'PSFramework.Validate.FSPath.FileOrParent')]
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Path,
        
        [int]
        $Depth,
        
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [PSFramework.Serialization.ClixmlDataStyle]
        $Style = 'Byte',
        
        [switch]
        $NoCompression,
        
        [switch]
        $PassThru,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.DefaultWrite')
    )
    
    begin
    {
        try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem -NewChild }
        catch { Stop-PSFFunction -String 'Validate.FSPath.FileOrParent' -StringValues $Path -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ }
        [System.Collections.ArrayList]$data = @()
    }
    process
    {
        $null = $data.Add($InputObject)
        if ($PassThru) { $InputObject }
    }
    end
    {
        try
        {
            Write-PSFMessage -Level Verbose -String 'Export-PSFClixml.Exporting' -StringValues $resolvedPath
            if ($Style -like 'Byte')
            {
                if ($NoCompression)
                {
                    if ($Depth) { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByte($data.ToArray(), $Depth))) }
                    else { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByte($data.ToArray()))) }
                }
                else
                {
                    if ($Depth) { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data.ToArray(), $Depth))) }
                    else { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data.ToArray()))) }
                }
            }
            else
            {
                if ($NoCompression)
                {
                    if ($Depth) { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToString($data.ToArray(), $Depth)), $Encoding) }
                    else { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToString($data.ToArray())), $Encoding) }
                }
                else
                {
                    if ($Depth) { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data.ToArray(), $Depth)), $Encoding) }
                    else { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data.ToArray())), $Encoding) }
                }
            }
        }
        catch
        {
            Stop-PSFFunction -String 'Export-PSFClixml.Exporting.Failed' -ErrorRecord $_ -EnableException $true -Target $resolvedPath -Cmdlet $PSCmdlet
        }
    }
}

function Get-PSFTypeSerializationData
{
<#
    .SYNOPSIS
        Creates a type extension XML for serializing an object
     
    .DESCRIPTION
        Creates a type extension XML for serializing an object
        Use this to register a type with a type serializer, so it will retain its integrity across process borders.
     
        This is relevant in order to have an object retain its type when ...
        - sending it over PowerShell Remoting
        - writing it to file via Export-Clixml and reading it later via Import-Clixml
     
        Note:
        In the default serializer, all types registered must:
        - Have all public properties be read & writable (the write needs not do anything, but it must not throw an exception).
        - All non-public properties will be ignored.
        - Come from an Assembly with a static name (like an existing dll file, not compiled at runtime).
     
    .PARAMETER InputObject
        The type to serialize.
        - Accepts a type object
        - The string name of the type
        - An object, whose type will then be determined
     
    .PARAMETER Mode
        Whether all types listed should be generated as a single definition ('Grouped'; default) or as one definition per type.
        Since multiple files have worse performance, it is generally recommended to group them all in a single file.
     
    .PARAMETER Fragment
        By setting this, the type XML is emitted without the outer XML shell, containing only the <Type> node(s).
        Use this if you want to add the output to existing type extension xml.
     
    .PARAMETER Serializer
        The serializer to use for the conversion.
        By default, the PSFramework serializer is used, which should work well enough, but requires the PSFramework to be present.
     
    .PARAMETER Method
        The serialization method to use.
        By default, the PSFramework serialization method is used, which should work well enough, but requires the PSFramework to be present.
     
    .EXAMPLE
        PS C:\> Get-PSFTypeSerializationData -InputObject 'My.Custom.Type'
     
        Generates an XML text that can be used to register via Update-TypeData.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTypeSerializationData')]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [object[]]
        $InputObject,
        
        [ValidateSet('Grouped','SingleItem')]
        [string]
        $Mode = "Grouped",
        
        [switch]
        $Fragment,
        
        [string]
        $Serializer = "PSFramework.Serialization.SerializationTypeConverter",
        
        [string]
        $Method = "GetSerializationData"
    )
    
    begin
    {
        #region XML builder functions
        function Get-XmlHeader
        {
            <#
                .SYNOPSIS
                    Returns the header for a types XML file
            #>

            [OutputType([string])]
            [CmdletBinding()]
            Param (
                
            )
            
            @"
<?xml version="1.0" encoding="utf-8"?>
<Types>
 
"@

        }
        
        function Get-XmlBody
        {
            <#
                .SYNOPSIS
                    Processes a type into proper XML
            #>

            [OutputType([string])]
            [CmdletBinding()]
            Param (
                [string]
                $Type,
                
                [string]
                $Serializer,
                
                [string]
                $Method
            )
            
            @"
 
  <!-- $Type -->
  <Type>
    <Name>Deserialized.$Type</Name>
    <Members>
      <MemberSet>
        <Name>PSStandardMembers</Name>
        <Members>
          <NoteProperty>
            <Name>
              TargetTypeForDeserialization
            </Name>
            <Value>
              $Type
            </Value>
          </NoteProperty>
        </Members>
      </MemberSet>
    </Members>
  </Type>
  <Type>
    <Name>$Type</Name>
    <Members>
      <CodeProperty IsHidden="true">
        <Name>SerializationData</Name>
        <GetCodeReference>
          <TypeName>$Serializer</TypeName>
          <MethodName>$Method</MethodName>
        </GetCodeReference>
      </CodeProperty>
    </Members>
    <TypeConverter>
      <TypeName>$Serializer</TypeName>
    </TypeConverter>
  </Type>
 
"@

        }
        
        function Get-XmlFooter
        {
            <#
                .SYNOPSIS
                    Returns the footer for a types XML file
            #>

            [OutputType([string])]
            [CmdletBinding()]
            Param (
                
            )
            @"
</Types>
"@

        }
        #endregion XML builder functions
        
        $types = @()
        if ($Mode -eq 'Grouped')
        {
            if (-not $Fragment) { $xml = Get-XmlHeader }
            else { $xml = "" }
        }
    }
    process
    {
        foreach ($item in $InputObject)
        {
            if ($null -eq $item) { continue }
            $type = $null
            if ($res = $item -as [System.Type]) { $type = $res }
            else { $type = $item.GetType() }
            
            if ($type -in $types) { continue }
            
            switch ($Mode)
            {
                'Grouped' { $xml += Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName }
                'SingleItem'
                {
                    if (-not $Fragment)
                    {
                        $xml = Get-XmlHeader
                        $xml += Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName
                        $xml += Get-XmlFooter
                        $xml
                    }
                    else
                    {
                        Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName
                    }
                }
            }
            
            $types += $type
        }
    }
    end
    {
        if ($Mode -eq 'Grouped')
        {
            if (-not $Fragment) { $xml += Get-XmlFooter }
            $xml
        }
    }
}

function Import-PSFClixml
{
<#
    .SYNOPSIS
        Imports objects serialized using Export-Clixml or Export-PSFClixml.
     
    .DESCRIPTION
        Imports objects serialized using Export-Clixml or Export-PSFClixml.
     
        It can handle compressed and non-compressed exports.
     
    .PARAMETER Path
        Path to the files to import.
     
    .PARAMETER Encoding
        Text-based files might be stored with any arbitrary encoding chosen.
        By default, this function assumes UTF8 encoding (the default export encoding for Export-PSFClixml).
     
    .EXAMPLE
        PS C:\> Import-PSFClixml -Path '.\object.xml'
     
        Imports the objects serialized to object.xml in the current folder.
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFClixml')]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName 'psframework.text.encoding.defaultread' -Fallback 'utf-8')
    )
    
    process
    {
        try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem }
        catch { Stop-PSFFunction -String 'Import-PSFClixml.Path.Resolution' -StringValues $Path -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $Path }
        
        foreach ($pathItem in $resolvedPath)
        {
            if ((Get-Item $pathItem).PSIsContainer)
            {
                Stop-PSFFunction -String 'Import-PSFClixml.Path.NotFile' -StringValues $pathItem -EnableException $true -Target $pathItem
            }
            Write-PSFMessage -Level Verbose -String 'Import-PSFClixml.Processing' -StringValues $pathItem -Target $pathItem
            
            [byte[]]$bytes = [System.IO.File]::ReadAllBytes($pathItem)
            
            try { [PSFramework.Serialization.ClixmlSerializer]::FromByteCompressed($bytes) }
            catch
            {
                [string]$string = [System.IO.File]::ReadAllText($pathItem, $Encoding)
                try { [PSFramework.Serialization.ClixmlSerializer]::FromString($string) }
                catch
                {
                    try { [PSFramework.Serialization.ClixmlSerializer]::FromStringCompressed($string) }
                    catch
                    {
                        try { [PSFramework.Serialization.ClixmlSerializer]::FromByte($bytes) }
                        catch
                        {
                            Stop-PSFFunction -String 'Import-PSFClixml.Conversion.Failed' -EnableException $true -Target $pathItem -Cmdlet $PSCmdlet
                        }
                    }
                }
                
            }
        }
    }
}

function Register-PSFTypeSerializationData
{
<#
    .SYNOPSIS
        Registers serialization xml Typedata.
     
    .DESCRIPTION
        Registers serialization xml Typedata.
        Use Get-PSFTypeSerializationData to generate such a string.
        When building a module, consider shipping that xml type extension in a dedicated file as part of the module and import it as part of the manifest's 'TypesToProcess' node.
     
    .PARAMETER TypeData
        The data to register.
        Generate with Get-PSFTypeSerializationData.
     
    .PARAMETER Path
        Where the file should be stored before appending.
        While type extensions can be added at runtime directly from memory, from file is more reliable.
        By default, a PSFramework path is chosen.
        The default path can be configured under the 'PSFramework.Serialization.WorkingDirectory' confguration setting.
     
    .EXAMPLE
        PS C:\> Get-PSFTypeSerializationData -InputObject 'My.Custom.Type' | Register-PSFTypeSerializationData
     
        Generates a custom type serialization xml and registers it.
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTypeSerializationData')]
    Param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [string[]]
        $TypeData,
        
        [string]
        $Path = (Get-PSFConfigValue -FullName 'PSFramework.Serialization.WorkingDirectory' -Fallback $script:path_typedata)
    )
    
    begin
    {
        if (-not (Test-Path $Path -PathType Container))
        {
            $null = New-Item -Path $Path -ItemType Directory -Force
        }
    }
    process
    {
        foreach ($item in $TypeData)
        {
            $name = $item -split "`n" | Select-String "<Name>(.*?)</Name>" | Where-Object { $_ -notmatch "<Name>Deserialized.|<Name>PSStandardMembers</Name>|<Name>SerializationData</Name>" } | Select-Object -First 1 | ForEach-Object { $_.Matches[0].Groups[1].Value }
            $fullName = Join-Path $Path.Trim "$($name).Types.ps1xml"
            
            $item | Set-Content -Path $fullName -Force -Encoding UTF8
            Update-TypeData -AppendPath $fullName
        }
    }
}

function New-PSFTeppCompletionResult
{
    <#
        .SYNOPSIS
            Generates a completion result for psframework internal tab completion.
         
        .DESCRIPTION
            Generates a completion result for psframework internal tab completion.
         
        .PARAMETER CompletionText
            The text to propose.
         
        .PARAMETER ToolTip
            The tooltip to show in tooltip-aware hosts (ISE, mostly)
         
        .PARAMETER ListItemText
            ???
         
        .PARAMETER CompletionResultType
            The type of object that is being completed.
            By default it generates one of type paramter value.
         
        .PARAMETER NoQuotes
            Whether to put the result in quotes or not.
         
        .EXAMPLE
            New-PSFTeppCompletionResult -CompletionText 'master' -ToolTip 'master'
     
            Returns a CompletionResult with the text and tooltip 'master'
    #>

    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $CompletionText,
        
        [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true)]
        [string]
        $ToolTip,
        
        [Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)]
        [string]
        $ListItemText,
        
        [System.Management.Automation.CompletionResultType]
        $CompletionResultType = [System.Management.Automation.CompletionResultType]::ParameterValue,
        
        [switch]
        $NoQuotes
    )
    
    process
    {
        $toolTipToUse = if ($ToolTip -eq '') { $CompletionText }
        else { $ToolTip }
        $listItemToUse = if ($ListItemText -eq '') { $CompletionText }
        else { $ListItemText }
        
        # If the caller explicitly requests that quotes
        # not be included, via the -NoQuotes parameter,
        # then skip adding quotes.
        
        if ($CompletionResultType -eq [System.Management.Automation.CompletionResultType]::ParameterValue -and -not $NoQuotes)
        {
            # Add single quotes for the caller in case they are needed.
            # We use the parser to robustly determine how it will treat
            # the argument. If we end up with too many tokens, or if
            # the parser found something expandable in the results, we
            # know quotes are needed.
            
            $tokens = $null
            $null = [System.Management.Automation.Language.Parser]::ParseInput("echo $CompletionText", [ref]$tokens, [ref]$null)
            if ($tokens.Length -ne 3 -or ($tokens[1] -is [System.Management.Automation.Language.StringExpandableToken] -and $tokens[1].Kind -eq [System.Management.Automation.Language.TokenKind]::Generic))
            {
                $CompletionText = "'$CompletionText'"
            }
        }
        return New-Object System.Management.Automation.CompletionResult($CompletionText, $listItemToUse, $CompletionResultType, $toolTipToUse.Trim())
    }
}

function Register-PSFTeppArgumentCompleter
{
    <#
        .SYNOPSIS
            Registers a parameter for a prestored Tepp.
         
        .DESCRIPTION
            Registers a parameter for a prestored Tepp.
            This function allows easily registering a function's parameter for Tepp in the function-file, rather than in a centralized location.
         
        .PARAMETER Command
            Name of the command whose parameter should receive Tepp.
         
        .PARAMETER Parameter
            Name of the parameter that should be Tepp'ed.
         
        .PARAMETER Name
            Name of the Tepp Completioner to use.
            Use the same name as was assigned in Register-PSFTeppScriptblock (which needs to be called first).
         
        .EXAMPLE
            Register-PSFTeppArgumentCompleter -Command Get-Test -Parameter Example -Name MyModule.Example
     
            Registers the parameter 'Example' of the command 'Get-Test' to receive the tab completion registered to 'MyModule.Example'
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTeppArgumentCompleter')]
    Param (
        [Parameter(Mandatory = $true)]
        [string[]]
        $Command,
        
        [Parameter(Mandatory = $true)]
        [string[]]
        $Parameter,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )
    process
    {
        foreach ($Param in $Parameter)
        {
            $scriptBlock = [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$Name].ScriptBlock
            if ([PSFramework.TabExpansion.TabExpansionHost]::Scripts[$Name].InnerScriptBlock)
            {
                [PSFramework.Utility.UtilityHost]::ImportScriptBlock($scriptBlock, $true)
            }
            Register-ArgumentCompleter -CommandName $Command -ParameterName $Param -ScriptBlock $scriptBlock
        }
    }
}

function Register-PSFTeppScriptblock
{
    <#
        .SYNOPSIS
            Registers a scriptblock under name, to later be available for TabExpansion.
         
        .DESCRIPTION
            Registers a scriptblock under name, to later be available for TabExpansion.
     
            This system supports two separate types of input: Full or Simple.
     
            Simple:
            The scriptblock simply must return string values.
            PSFramework will then do the rest of the processing when the user asks for tab completion.
            This is the simple-most way to implement tab completion, for a full example, look at the first example in this help.
     
            Full:
            A full scriptblock implements all that is needed to provide Tab Expansion.
            For more details and guidance, see the following concept help:
                Get-Help about_psf_tabexpansion
         
        .PARAMETER ScriptBlock
            The scriptblock to register.
         
        .PARAMETER Name
            The name under which the scriptblock should be registered.
            It is recommended to prefix the name with the module (e.g.: mymodule.<name>), as names are shared across all implementing modules.
     
        .PARAMETER Mode
            Whether the script provided is a full or simple scriptblock.
            By default, this function automatically detects this, but just in case, you can override this detection.
     
        .PARAMETER CacheDuration
            How long a tab completion result is valid.
            By default, PSFramework tab completion will run the scriptblock on each call.
            This can be used together with a background refresh mechanism to offload the cost of expensive queries into the background.
            See Set-PSFTeppResult for details on how to refresh the cache.
     
        .PARAMETER Global
            Whether the scriptblock should be executed in the global context.
            This parameter is needed to reliably execute in background runspaces, but means no direct access to module content.
     
        .EXAMPLE
            Register-PSFTeppScriptblock -Name "psalcohol-liquids" -ScriptBlock { "beer", "mead", "wine", "vodka", "whiskey", "rum" }
            Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name "psalcohol-liquids"
     
            In step one we set a list of questionable liquids as the list of available beverages for parameter 'Type' on the command 'Get-Alcohol'
         
        .EXAMPLE
            Register-PSFTeppScriptblock -ScriptBlock $scriptBlock -Name MyFirstTeppScriptBlock
     
            Stores the scriptblock stored in $scriptBlock under the name "MyFirstTeppScriptBlock"
     
        .EXAMPLE
            $scriptBlock = { (Get-ChildItem (Get-PSFConfigValue -FullName mymodule.path.scripts -Fallback "$env:USERPROFILE\Documents\WindowsPowerShell\Scripts")).FullName }
            Register-PSFTeppScriptblock -Name mymodule-scripts -ScriptBlock $scriptBlock -Mode Simple
     
            Stores a simple scriptblock that will return a list of strings under the name "mymodule-scripts".
            The system will wrap all the stuff around this that is necessary to provide Tab Expansion and filter out output that doesn't fit the user input so far.
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTeppScriptblock')]
    Param (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [PSFramework.TabExpansion.TeppScriptMode]
        $Mode = "Auto",
        
        [PSFramework.Parameter.TimeSpanParameter]
        $CacheDuration = 0,
        
        [switch]
        $Global
    )
    
    process
    {
        [PSFramework.TabExpansion.TabExpansionHost]::RegisterCompletion($Name, $ScriptBlock, $Mode, $CacheDuration, $Global)
    }
}


function Set-PSFTeppResult
{
<#
    .SYNOPSIS
        Refreshes the tab completion value cache.
     
    .DESCRIPTION
        Refreshes the tab completion value cache for the specified completion scriptblock.
     
        Tab Completion scriptblocks can be configured to retrieve values from a dedicated cache.
        This allows seamless background refreshes of completion data and eliminates all waits for the user.
     
    .PARAMETER TabCompletion
        The name of the completion script to set the last results for.
     
    .PARAMETER Value
        The values to set.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Set-PSFTeppResult -TabCompletion 'MyModule.Computer' -Value (Get-ADComputer -Filter *).Name
     
        Stores the names of all computers in AD into the tab completion cache of the completion scriptblock 'MyModule.Computer'
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTeppResult')]
    param (
        [Parameter(Mandatory = $true)]
        [PSFramework.Validation.PsfValidateSetAttribute(ScriptBlock = { [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys } )]
        [string]
        $TabCompletion,
        
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [string[]]
        $Value
    )
    
    process
    {
        if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $TabCompletion -ActionString 'Set-PSFTeppResult.UpdateValue')
        {
            [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$TabCompletion].LastResult = $Value
            [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$TabCompletion].LastExecution = ([System.DateTime]::Now)
        }
    }
}

function Disable-PSFTaskEngineTask
{
<#
    .SYNOPSIS
        Disables a task registered to the PSFramework task engine.
     
    .DESCRIPTION
        Disables a task registered to the PSFramework task engine.
     
    .PARAMETER Name
        Name of the task to disable.
     
    .PARAMETER Task
        The task registered. Must be a task object returned by Get-PSFTaskEngineTask.
     
    .EXAMPLE
        PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.maintenance' | Disable-PSFTaskEngineTask
         
        Disables the task named 'mymodule.maintenance'
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Disable-PSFTaskEngineTask')]
    param (
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        [PSFramework.TaskEngine.PsfTask[]]
        $Task
    )
    
    process
    {
        foreach ($item in $Task)
        {
            if ($item.Enabled)
            {
                Write-PSFMessage -Level Verbose -String 'Disable-PSFTaskEngineTask.Disabling' -StringValues $item.Name -Tag 'disable', 'taskengine', 'task'
                $item.Enabled = $false
            }
        }
        foreach ($taskName in $Name)
        {
            foreach ($taskObject in Get-PSFTaskEngineTask -Name $taskName)
            {
                Write-PSFMessage -Level Verbose -String 'Disable-PSFTaskEngineTask.Disabling' -StringValues $taskObject.Name -Tag 'disable', 'taskengine', 'task'
                $taskObject.Enabled = $false
            }
        }
    }
}

function Enable-PSFTaskEngineTask
{
    <#
        .SYNOPSIS
            Enables a task registered to the PSFramework task engine.
         
        .DESCRIPTION
            Enables a task registered to the PSFramework task engine.
     
            Note:
            Tasks are enabled by default. Use this function to re-enable a task disabled by Disable-PSFTaskEngineTask.
     
        .PARAMETER Name
            Name of the task to enable.
         
        .PARAMETER Task
            The task registered. Must be a task object returned by Get-PSFTaskEngineTask.
         
        .EXAMPLE
            PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.maintenance' | Enable-PSFTaskEngineTask
     
            Enables the task named 'mymodule.maintenance'
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Enable-PSFTaskEngineTask')]
    param (
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [PSFramework.TaskEngine.PsfTask[]]
        $Task
    )
    
    begin
    {
        $didSomething = $false
    }
    process
    {
        foreach ($item in $Task)
        {
            if (-not $item.Enabled)
            {
                Write-PSFMessage -Level Verbose -String 'Enable-PSFTaskEngineTask.Enable' -StringValues $item.Name -Tag 'enable', 'taskengine', 'task'
                $item.Enabled = $true
                $didSomething = $true
            }
        }
        
        foreach ($taskName in $Name)
        {
            foreach ($taskObject in Get-PSFTaskEngineTask -Name $taskName)
            {
                if (-not $taskObject.Enabled)
                {
                    Write-PSFMessage -Level Verbose -String 'Enable-PSFTaskEngineTask.Enable' -StringValues $taskObject.Name -Tag 'enable', 'taskengine', 'task'
                    $taskObject.Enabled = $true
                    $didSomething = $true
                }
            }
        }
    }
    end
    {
        # If we enabled any task, start the runspace again, in case it isn't already running (no effect if it is)
        if ($didSomething) { Start-PSFRunspace -Name 'psframework.taskengine' }
    }
}

function Get-PSFTaskEngineCache
{
    <#
        .SYNOPSIS
            Retrieve values from the cache for a task engine task.
         
        .DESCRIPTION
            Retrieve values from the cache for a task engine task.
            Tasks scheduled under the PSFramework task engine do not have a way to directly pass information to the primary runspace.
            Using Set-PSFTaskEngineCache, they can store the information somewhere where the main runspace can retrieve it using this function.
         
        .PARAMETER Module
            The name of the module that generated the task.
            Use scriptname in case of using this within a script.
            Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache'
         
        .PARAMETER Name
            The name of the task for which the cache is.
            Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache'
         
        .EXAMPLE
            PS C:\> Get-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask'
     
            Retrieves the information stored under 'mymodule.maintenancetask'
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTaskEngineCache')]
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )
    
    process
    {
        $cacheItem = [PSFramework.TaskEngine.TaskHost]::GetCacheItem($Module, $Name)
        if (-not $cacheItem) { return }
        
        $value = $cacheItem.GetValue()
        if ($null -ne $value) { $value }
    }
}


function Get-PSFTaskEngineTask
{
    <#
        .SYNOPSIS
            Returns tasks registered for the task engine
         
        .DESCRIPTION
            Returns tasks registered for the task engine
         
        .PARAMETER Name
            Default: "*"
            Only tasks with similar names are returned.
         
        .EXAMPLE
            PS C:\> Get-PSFTaskEngineTask
     
            Returns all tasks registered to the task engine
     
        .EXAMPLE
            PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.*'
     
            Returns all tasks registered to the task engine whose name starts with 'mymodule.'
            (It stands to reason that only tasks belonging to the module 'mymodule' would be returned that way)
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTaskEngineTask')]
    Param (
        [string]
        $Name = "*"
    )
    
    process
    {
        [PSFramework.TaskEngine.TaskHost]::Tasks.Values | Where-Object Name -Like $Name
    }
}

function Register-PSFTaskEngineTask
{
    <#
        .SYNOPSIS
            Allows scheduling PowerShell tasks, that are perfomed in the background.
         
        .DESCRIPTION
            Allows scheduling PowerShell tasks, that are perfomed in the background.
     
            All scriptblocks scheduled like this will be performed on a separate runspace.
            None of the scriptblocks will affect the main session (so you cannot manipulate variables, etc.)
     
            This system is designed for two use-cases:
            - Reducing module import time by off-loading expensive one-time actions (such as building a cache) in the background
            - Scheduling periodic script executions that should occur while the process is running (e.g.: continuous maintenance, cache updates, ...)
     
            It also avoids overloading the client computer by executing too many tasks at the same time, as multiple modules running code in the background might.
            Instead tasks that are due simultaneously are processed by priority.
         
        .PARAMETER Name
            The name of the task.
            Must be unique, otherwise it will update the existing task.
     
        .PARAMETER Description
            Description of the task.
            Helps documenting the task and what it is supposed to be doing.
         
        .PARAMETER ScriptBlock
            The task/scriptblock that should be performed as a background task.
         
        .PARAMETER Once
            Whether the task should be performed only once.
         
        .PARAMETER Interval
            The interval at which the task should be repeated.
         
        .PARAMETER Delay
            How far after the initial registration should the task script wait before processing this.
            This can be used to delay background stuff that should not content with items that would be good to have as part of the module import.
         
        .PARAMETER Priority
            How important is this task?
            If multiple tasks are due at the same maintenance cycle, the more critical one will be processed first.
     
        .PARAMETER ResetTask
            If the task already exists, it will be reset by setting this parameter (this switch is ignored when creating new tasks).
            This allows explicitly registering tasks for re-execution, even though they were set to execute once only.
         
        .PARAMETER EnableException
            This parameters disables user-friendly warnings and enables the throwing of exceptions.
            This is less user friendly, but allows catching exceptions in calling scripts.
         
        .EXAMPLE
            PS C:\> Register-PSFTaskEngineTask -Name 'mymodule.buildcache' -ScriptBlock $ScriptBlock -Once -Description 'Builds the object cache used by the mymodule module'
     
            Registers the task contained in $ScriptBlock under the name 'mymodule.buildcache' to execute once at the system's earliest convenience in a medium (default) priority.
     
        .EXAMPLE
            PS C:\> Register-PSFTaskEngineTask -Name 'mymodule.maintenance' -ScriptBlock $ScriptBlock -Interval "00:05:00" -Delay "00:01:00" -Priority Critical -Description 'Performs critical system maintenance in order for the mymodule module to function'
     
            Registers the task contained in $ScriptBlock under the name 'mymodule.maintenance'
            - Sets it to execute every 5 minutes
            - Sets it to wait for 1 minute after registration before starting the first execution
            - Sets it to priority "Critical", ensuring it takes precedence over most other tasks.
    #>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTaskEngineTask')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [string]
        $Description,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Once")]
        [switch]
        $Once,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Repeating")]
        [PsfValidateScript('PSFramework.Validate.TimeSpan.Positive', ErrorString = 'PSFramework.Validate.TimeSpan.Positive')]
        [PSFTimeSpan]
        $Interval,
        
        [PSFTimeSpan]
        $Delay,
        
        [PSFramework.TaskEngine.Priority]
        $Priority = "Medium",
        
        [switch]
        $ResetTask,
        
        [switch]
        $EnableException
    )
    
    process
    {
        
        #region Case: Task already registered
        if ([PSFramework.TaskEngine.TaskHost]::Tasks.ContainsKey($Name))
        {
            $task = [PSFramework.TaskEngine.TaskHost]::Tasks[$Name]
            if (Test-PSFParameterBinding -ParameterName Description) { $task.Description = $Description }
            if ($task.ScriptBlock -ne $ScriptBlock) { $task.ScriptBlock = $ScriptBlock }
            if (Test-PSFParameterBinding -ParameterName Once) { $task.Once = $Once }
            if (Test-PSFParameterBinding -ParameterName Interval)
            {
                $task.Once = $false
                $task.Interval = $Interval
            }
            if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay }
            if (Test-PSFParameterBinding -ParameterName Priority) { $task.Priority = $Priority }
            
            if ($ResetTask)
            {
                $task.Registered = Get-Date
                $task.LastExecution = New-Object System.DateTime(0)
                $task.State = 'Pending'
            }
        }
        #endregion Case: Task already registered
        
        #region New Task
        else
        {
            $task = New-Object PSFramework.TaskEngine.PsfTask
            $task.Name = $Name
            if (Test-PSFParameterBinding -ParameterName Description) { $task.Description = $Description }
            $task.ScriptBlock = $ScriptBlock
            if (Test-PSFParameterBinding -ParameterName Once) { $task.Once = $true }
            if (Test-PSFParameterBinding -ParameterName Interval) { $task.Interval = $Interval }
            if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay }
            $task.Priority = $Priority
            $task.Registered = Get-Date
            [PSFramework.TaskEngine.TaskHost]::Tasks[$Name] = $task
        }
        #endregion New Task
    }
    end { Start-PSFRunspace -Name "psframework.taskengine" -EnableException:$EnableException }
}

function Set-PSFTaskEngineCache
{
<#
    .SYNOPSIS
        Sets values and configuration for a cache entry.
     
    .DESCRIPTION
        Allows applying values and settings for a cache.
        This allows applying a lifetime to cached data or offering a mechanism to automatically refresh it on retrieval.
     
        This feature is specifically designed to interact with the Task Engine powershell task scheduler (See Register-PSFTaskEngineTask for details).
        However it is open for interaction with all tools.
        In particular, the cache is threadsafe to use through these functions.
        The cache is global to the process, NOT the current runspace.
        Background runspaces access the same data in a safe manner.
     
    .PARAMETER Module
        The name of the module that generated the task.
        Use scriptname in case of using this within a script.
     
    .PARAMETER Name
        The name of the task for which the cache is.
     
    .PARAMETER Value
        The value to set this cache to.
     
    .PARAMETER Lifetime
        How long values stored in this cache should remain valid.
     
    .PARAMETER Collector
        A scriptblock that is used to refresh the data cached.
        Should return values in a save manner, will be called if retrieving data on a cache that has expired.
     
    .PARAMETER CollectorArgument
        An argument to pass to the collector script.
        Allows passing in values as argument to the collector script.
        The arguments are stored persistently and are not subject to expiration.
     
    .EXAMPLE
        PS C:\> Set-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask' -Value $results
         
        Stores the content of $results in the cache 'mymodule / maintenancetask'
        These values can now be retrieved using Get-PSFTaskEngineCache.
     
    .EXAMPLE
        PS C:\> Set-PSFTaskEngineCache -Module MyModule -Name DomainController -Lifetime 8h -Collector { Get-ADDomainController }
     
        Registers a cache that lists all domain controllers in the current domain, keeping the data valid for 8 hours before refreshing it.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTaskEngineCache')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,
        
        [AllowNull()]
        [object]
        $Value,
        
        [PsfValidateScript('PSFramework.Validate.TimeSpan.Positive', ErrorString = 'PSFramework.Validate.TimeSpan.Positive')]
        [PSFTimespan]
        $Lifetime,
        
        [System.Management.Automation.ScriptBlock]
        $Collector,
        
        [object]
        $CollectorArgument
    )
    
    process
    {
        if ([PSFramework.TaskEngine.TaskHost]::TestCacheItem($Module, $Name))
        {
            $cacheItem = [PSFramework.TaskEngine.TaskHost]::GetCacheItem($Module, $Name)
        }
        else { $cacheItem = [PSFramework.TaskEngine.TaskHost]::NewCacheItem($Module, $Name) }
        if (Test-PSFParameterBinding -ParameterName Value) { $cacheItem.Value = $Value }
        if (Test-PSFParameterBinding -ParameterName Lifetime) { $cacheItem.Expiration = $Lifetime }
        if (Test-PSFParameterBinding -ParameterName Collector) { $cacheItem.Collector = $Collector }
        if (Test-PSFParameterBinding -ParameterName CollectorArgument) { $cacheItem.CollectorArgument = $CollectorArgument }
    }
}

function Test-PSFTaskEngineCache
{
    <#
        .SYNOPSIS
            Tests, whether the specified task engine cache-entry has been written.
         
        .DESCRIPTION
            Tests, whether the specified task engine cache-entry has been written.
         
        .PARAMETER Module
            The name of the module that generated the task.
            Use scriptname in case of using this within a script.
            Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache'
         
        .PARAMETER Name
            The name of the task for which the cache is.
            Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache'
         
        .EXAMPLE
            PS C:\> Test-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask'
     
            Returns, whether the cache has been set for the module 'mymodule' and the task 'maintenancetask'
            Does not require the cache to actually contain a value, but must exist.
    #>

    [OutputType([System.Boolean])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFTaskEngineCache')]
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Module,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )
    
    process
    {
        [PSFramework.TaskEngine.TaskHost]::TestCacheItem($Module, $Name)
    }
}

function Test-PSFTaskEngineTask
{
    <#
        .SYNOPSIS
            Tests, whether the specified task has already been executed.
         
        .DESCRIPTION
            Tests, whether the specified task has already been executed.
            Returns false, if the task doesn't exist.
         
        .PARAMETER Name
            Name of the task to test
         
        .EXAMPLE
            PS C:\> Test-PSFTaskEngineTask -Name 'mymodule.maintenance'
     
            Returns, whether the task named 'mymodule.maintenance' has already been executed at least once.
    #>

    [OutputType([System.Boolean])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFTaskEngineTask')]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )
    
    process
    {
        if (-not ([PSFramework.TaskEngine.TaskHost]::Tasks.ContainsKey($Name)))
        {
            return $false
        }
        
        $task = [PSFramework.TaskEngine.TaskHost]::Tasks[$Name]
        $task.LastExecution -gt $task.Registered
    }
}

function Compare-PSFArray
{
    <#
    .SYNOPSIS
        Compares two arrays.
     
    .DESCRIPTION
        Compares two arrays.
     
    .PARAMETER ReferenceObject
        The first array to compare with the second array.
     
    .PARAMETER DifferenceObject
        The second array to compare with the first array.
     
    .PARAMETER OrderSpecific
        Makes the comparison order specific.
        By default, the command does not care for the order the objects are stored in.
     
    .PARAMETER Quiet
        Rather than returning a delta report object, return a single truth statement:
        - $true if the two arrays are equal
        - $false if the two arrays are NOT equal.
     
    .EXAMPLE
        PS C:\> Compare-PSFArray -ReferenceObject $arraySource -DifferenceObject $arrayDestination -Quiet -OrderSpecific
 
        Compares the two sets of objects, and returns ...
        - $true if both sets contains the same objects in the same order
        - $false if they do not
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [object[]]
        $ReferenceObject,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [object[]]
        $DifferenceObject,
        
        [switch]
        $OrderSpecific,
        
        [switch]
        $Quiet
    )
    
    process
    {
        #region Not Order Specific
        if (-not $OrderSpecific)
        {
            $delta = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject
            if ($delta)
            {
                if ($Quiet) { return $false }
                [PSCustomObject]@{
                    ReferenceObject  = $ReferenceObject
                    DifferenceObject = $DifferenceObject
                    Delta             = $delta
                    IsEqual             = $false
                }
                return
            }
            else
            {
                if ($Quiet) { return $true }
                [PSCustomObject]@{
                    ReferenceObject  = $ReferenceObject
                    DifferenceObject = $DifferenceObject
                    Delta             = $delta
                    IsEqual             = $true
                }
                return
            }
        }
        #endregion Not Order Specific
        
        #region Order Specific
        else
        {
            if ($Quiet -and ($ReferenceObject.Count -ne $DifferenceObject.Count)) { return $false }
            $result = [PSCustomObject]@{
                ReferenceObject  = $ReferenceObject
                DifferenceObject = $DifferenceObject
                Delta             = @()
                IsEqual             = $true
            }
            
            $maxCount = [math]::Max($ReferenceObject.Count, $DifferenceObject.Count)
            [System.Collections.ArrayList]$indexes = @()
            
            foreach ($number in (0 .. ($maxCount - 1)))
            {
                if ($number -ge $ReferenceObject.Count)
                {
                    $null = $indexes.Add($number)
                    continue
                }
                if ($number -ge $DifferenceObject.Count)
                {
                    $null = $indexes.Add($number)
                    continue
                }
                if ($ReferenceObject[$number] -ne $DifferenceObject[$number])
                {
                    if ($Quiet) { return $false }
                    $null = $indexes.Add($number)
                    continue
                }
            }
            
            if ($indexes.Count -gt 0)
            {
                $result.IsEqual = $false
                $result.Delta = $indexes.ToArray()
            }
            
            $result
        }
        #endregion Order Specific
    }
}


function ConvertFrom-PSFArray
{
<#
    .SYNOPSIS
        Flattens properties that have array values.
     
    .DESCRIPTION
        Flattens properties that have array values.
        With this you can prepare objects for export to systems that cannot handle collection in propertyvalues.
        This flattening happens using a string join operation, so the output on modified properties is guaranteed to be a string.
     
    .PARAMETER JoinBy
        The string sequence to join values by.
        Defaults to ", "
     
    .PARAMETER PropertyName
        The properties to affect.
        Interprets wildcards, defaults to '*'.
     
    .PARAMETER InputObject
        The objects the properties of which to flatten.
     
    .EXAMPLE
        PS C:\> Get-Something | ConvertFrom-PSFArray | Export-Csv -Path .\output.csv
     
        Processes the output of Get-Something in order to produce a flat table to export data to csv without trimming collections.
#>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]
        $JoinBy = ', ',
        
        [Parameter(Position = 1)]
        [string[]]
        $PropertyName = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    
    process
    {
        $props = [ordered]@{ }
        foreach ($property in $InputObject.PSObject.Properties)
        {
            #region Skip non-collection properties
            if ($property.Value -isnot [System.Collections.ICollection])
            {
                $props[$property.Name] = $property.Value
                continue
            }
            #endregion Skip non-collection properties
            
            #region Handle whether the property should be processed at all
            $found = $false
            foreach ($name in $PropertyName)
            {
                if ($property.Name -like $name)
                {
                    $found = $true
                    break
                }
            }
            if (-not $found)
            {
                $props[$property.Name] = $property.Value
                continue
            }
            #endregion Handle whether the property should be processed at all
            
            $props[$property.Name] = $property.Value -join $JoinBy
        }
        [PSCustomObject]$props
    }
}

function Get-PSFPath
{
<#
    .SYNOPSIS
        Access a configured path.
     
    .DESCRIPTION
        Access a configured path.
        Paths can be configured using Set-PSFPath or using the configuration system.
        To register a path using the configuration system create a setting key named like this:
        "PSFramework.Path.<PathName>"
        For example the following setting points at the temp path:
        "PSFramework.Path.Temp"
     
    .PARAMETER Name
        Name of the path to retrieve.
     
    .EXAMPLE
        PS C:\> Get-PSFPath -Name 'temp'
     
        Returns the temp path.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Name
    )
    
    process
    {
        Get-PSFConfigValue -FullName "PSFramework.Path.$Name"
    }
}

function Get-PSFScriptblock
{
<#
    .SYNOPSIS
        Access the scriptblocks stored with Set-PSFScriptblock.
     
    .DESCRIPTION
        Access the scriptblocks stored with Set-PSFScriptblock.
     
        Use this command to access scriptblocks designed for easy, processwide access.
     
    .PARAMETER Name
        The name of the scriptblock to request.
        It's mandatory for explicitly requesting a scriptblock, but optional to use with -List as a filter.
     
    .PARAMETER List
        Instead of requesting a specific scriptblock, list the available ones.
        This can be further filtered by using a wildcard supporting string as -Name.
 
    .PARAMETER Tag
        Filter scriptblocks by their tags.
        This can be further filtered by using a wildcard supporting string as -Name.
 
    .PARAMETER Description
        Filter scriptblocks by their description using wildcard characters.
        This can be further filtered by using a wildcard supporting string as -Name.
     
    .PARAMETER Container
        Return the scriptblock container item rather than the scriptblock directly.
     
    .EXAMPLE
        PS C:\> Get-PSFScriptblock -Name 'MyModule.TestServer'
     
        Returns the scriptblock stored as 'MyModule.TestServer'
     
    .EXAMPLE
        PS C:\> Get-PSFScriptblock -List
     
        Returns a list of all scriptblocks
     
    .EXAMPLE
        PS C:\> Get-PSFScriptblock -List -Name 'MyModule.TestServer'
     
        Returns scriptblock and meta information for the MyModule.TestServer scriptblock.
     
    .EXAMPLE
        PS C:\> Get-PSFScriptblock -Name 'MyModule.*' -Tag StateChanging, Networking
     
        Returns scriptblock and meta information for all scriptblocks tagged StateChanging
        or Networking and with a name starting with MyModule.
     
    .EXAMPLE
        PS C:\> Get-PSFScriptblock -Description '*Infrastructure Script*'
     
        Returns scriptblock and meta information for all script blocks containing the
        description '*Infrastructure Script*'.
#>

    [OutputType([PSFramework.Utility.ScriptBlockItem], ParameterSetName = 'Search')]
    [OutputType([PSFramework.Utility.ScriptBlockItem], ParameterSetName = 'Container')]
    [OutputType([System.Management.Automation.ScriptBlock], ParameterSetName = 'Name')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueForMandatoryParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [PsfArgumentCompleter('PSFramework.Utility.Scriptblock.Name')]
        [Parameter(ParameterSetName = 'Search', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'Name', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'Container', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name = '*',
        
        [Parameter(ParameterSetName = 'Search')]
        [switch]
        $List,

        [Parameter(ParameterSetName = 'Search')]
        [string]
        $Description,
        
        [PsfArgumentCompleter('PSFramework.Utility.Scriptblock.Tag')]
        [Parameter(ParameterSetName = 'Search')]
        [string[]]
        $Tag,
        
        [Parameter(ParameterSetName = 'Container')]
        [switch]
        $Container
    )
    
    begin
    {
        [System.Collections.ArrayList]$sent = @()
        $allItems = [PSFramework.Utility.UtilityHost]::ScriptBlocks.Values
    }
    process
    {
        :main foreach ($nameText in $Name)
        {
            switch ($PSCmdlet.ParameterSetName) {
                #region Retrieve by name
                { 'Name', 'Container' -contains $_ }
                {
                    if ($sent -contains $nameText) { continue main }
                    $null = $sent.Add($nameText)
                    $scriptBlock = [PSFramework.Utility.UtilityHost]::ScriptBlocks[$nameText]
                    if (-not $scriptBlock) { continue main }
                    # If not available in the current runspace, skip it
                    if (-not $scriptBlock.IsAvailable()) { continue main }
                    if ($Container) { $scriptBlock }
                    else { $scriptBlock.ScriptBlock }
                }
                #endregion Retrieve by name
                #region Search by filters
                'Search'
                {
                    foreach ($item in $allItems) {
                        # If not available in the current runspace, skip it
                        if (-not $item.IsAvailable()) { continue }
                        
                        if ($item.Name -notlike $nameText) { continue }
                        if ($sent -contains $item.Name) { continue }
                        
                        if ($Tag) {
                            $found = $false
                            foreach ($tagString in $Tag) {
                                if ($item.Tag -contains $tagString) { $found = $true }
                            }
                            if (-not $found) { continue }
                        }
                        if ($Description -and $item.Description -notlike $Description) { continue }
                        
                        $null = $sent.Add($item.Name)
                        $item
                    }
                }
                #endregion Search by filters
            }
        }
    }
}

function Import-PSFPowerShellDataFile
{
<#
    .SYNOPSIS
        A wrapper command around Import-PowerShellDataFile
     
    .DESCRIPTION
        A wrapper command around Import-PowerShellDataFile
        This enables use of the command on PowerShell 3+ as well as during JEA endpoints.
     
        Note: The protective value of Import-PowerShellDataFile is only offered when run on PS5+.
        This is merely meant to provide compatibility in the scenarios, where the original command would fail!
        If you care about PowerShell security, update to the latest version (in which case this command is still as secure as the default command, as that is what will actually be run.
     
    .PARAMETER Path
        The path from which to load the data file.
     
    .PARAMETER LiteralPath
        The path from which to load the data file.
        In opposite to the Path parameter, input here will not be interpreted.
     
    .EXAMPLE
        PS C:\> Import-PSFPowerShellDataFile -Path .\data.psd1
     
        Safely loads the data stored in data.psd1
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    Param (
        [Parameter(ParameterSetName = 'ByPath')]
        [string[]]
        $Path,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByLiteralPath')]
        [Alias('PSPath')]
        [string[]]
        $LiteralPath
    )
    
    process
    {
        # If launched in JEA Endpoint, Import-PowerShellDataFile is unavailable due to a bug
        # It is important to check the initial sessionstate, as the module's current state will be 'FullLanguage' instead.
        # Import-PowerShellDataFile is also unavailable before PowerShell v5
        if (([runspace]::DefaultRunspace.InitialSessionState.LanguageMode -eq 'NoLanguage') -or ($PSVersionTable.PSVersion.Major -lt 5))
        {
            foreach ($resolvedPath in ($Path | Resolve-PSFPath -Provider FileSystem | Sort-Object -Unique))
            {
                Invoke-Expression (Get-Content -Path $resolvedPath -Raw)
            }
            foreach ($pathItem in $LiteralPath)
            {
                Invoke-Expression (Get-Content -Path $pathItem -Raw)
            }
        }
        else
        {
            Import-PowerShellDataFile @PSBoundParameters
        }
    }
}


function Join-PSFPath
{
<#
    .SYNOPSIS
        Performs multisegment path joins.
     
    .DESCRIPTION
        Performs multisegment path joins.
     
    .PARAMETER Path
        The basepath to join on.
     
    .PARAMETER Child
        Any number of child paths to add.
     
    .PARAMETER Normalize
        Normalizes path separators for the path segments offered.
        This ensures the correct path separators for the current OS are chosen.
     
    .EXAMPLE
        PS C:\> Join-PSFPath -Path 'C:\temp' 'Foo' 'Bar'
     
        Returns 'C:\temp\Foo\Bar'
     
    .EXAMPLE
        PS C:\> Join-PSFPath -Path 'C:\temp' 'Foo' 'Bar' -Normalize
     
        Returns 'C:\temp\Foo\Bar' on a Windows OS.
        Returns 'C:/temp/Foo/Bar' on most non-Windows OSes.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Path,
        
        [Parameter(ValueFromRemainingArguments = $true)]
        [string[]]
        $Child,
        
        [switch]
        $Normalize
    )
    
    process
    {
        $resultingPath = $Path
        
        foreach ($childItem in $Child)
        {
            $resultingPath = Join-Path -Path $resultingPath -ChildPath $childItem
        }
        
        if ($Normalize)
        {
            $defaultSeparator = [System.IO.Path]::DirectorySeparatorChar
            $altSeparator = [System.IO.Path]::AltDirectorySeparatorChar
            $resultingPath = $resultingPath.Replace($altSeparator, $defaultSeparator)
        }
        
        $resultingPath
    }
}

function New-PSFSupportPackage
{
<#
    .SYNOPSIS
        Creates a package of troubleshooting information that can be used by developers to help debug issues.
     
    .DESCRIPTION
        This function creates an extensive debugging package that can help with reproducing and fixing issues.
         
        The file will be created on the desktop by default and will contain quite a bit of information:
        - OS Information
        - Hardware Information (CPU, Ram, things like that)
        - .NET Information
        - PowerShell Information
        - Your input history
        - The In-Memory message log
        - The In-Memory error log
        - Screenshot of the console buffer (Basically, everything written in your current console, even if you have to scroll upwards to see it).
     
    .PARAMETER Path
        The folder where to place the output xml in.
        Defaults to your desktop.
     
    .PARAMETER Include
        What to include in the export.
        By default, all is included.
     
    .PARAMETER Exclude
        Anything not to include in the export.
        Use this to explicitly exclude content you do not wish to be part of the dump (for example for data protection reasons).
     
    .PARAMETER Variables
        Name of additional variables to attach.
        This allows you to add the content of variables to the support package, if you believe them to be relevant to the case.
     
    .PARAMETER ExcludeError
        By default, the content of $Error is included, as it often can be helpful in debugging, even with error handling using the message system.
        However, there can be rare instances where this will explode the total export size to gigabytes, in which case it becomes necessary to skip this.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        New-PSFSupportPackage
         
        Creates a large support pack in order to help us troubleshoot stuff.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFSupportPackage')]
    param (
        [string]
        $Path = "$($env:USERPROFILE)\Desktop",
        
        [PSFramework.Utility.SupportData]
        $Include = 'All',
        
        [PSFramework.Utility.SupportData]
        $Exclude = 'None',
        
        [string[]]
        $Variables,
        
        [switch]
        $ExcludeError,
        
        [switch]
        [Alias('Silent')]
        $EnableException
    )
    
    begin
    {
        #region Helper functions
        function Get-ShellBuffer
        {
            [CmdletBinding()]
            param ()
            
            if ($Host.Name -eq 'Windows PowerShell ISE Host')
            {
                return $psIse.CurrentPowerShellTab.ConsolePane.Text
            }
            
            try
            {
                # Define limits
                $rec = New-Object System.Management.Automation.Host.Rectangle
                $rec.Left = 0
                $rec.Right = $host.ui.rawui.BufferSize.Width - 1
                $rec.Top = 0
                $rec.Bottom = $host.ui.rawui.BufferSize.Height - 1
                
                # Load buffer
                $buffer = $host.ui.rawui.GetBufferContents($rec)
                
                # Convert Buffer to list of strings
                $int = 0
                $lines = @()
                while ($int -le $rec.Bottom)
                {
                    $n = 0
                    $line = ""
                    while ($n -le $rec.Right)
                    {
                        $line += $buffer[$int, $n].Character
                        $n++
                    }
                    $line = $line.TrimEnd()
                    $lines += $line
                    $int++
                }
                
                # Measure empty lines at the beginning
                $int = 0
                $temp = $lines[$int]
                while ($temp -eq "") { $int++; $temp = $lines[$int] }
                
                # Measure empty lines at the end
                $z = $rec.Bottom
                $temp = $lines[$z]
                while ($temp -eq "") { $z--; $temp = $lines[$z] }
                
                # Skip the line launching this very function
                $z--
                
                # Measure empty lines at the end (continued)
                $temp = $lines[$z]
                while ($temp -eq "") { $z--; $temp = $lines[$z] }
                
                # Cut results to the limit and return them
                return $lines[$int .. $z]
            }
            catch { }
        }
        #endregion Helper functions
    }
    process
    {
        $filePathXml = Join-Path $Path "powershell_support_pack_$(Get-Date -Format "yyyy_MM_dd-HH_mm_ss").cliDat"
        $filePathZip = $filePathXml -replace "\.cliDat$", ".zip"
        
        Write-PSFMessage -Level Critical -String 'New-PSFSupportPackage.Header' -StringValues $filePathZip, (Get-PSFConfigValue -FullName 'psframework.supportpackage.contactmessage' -Fallback '')
        
        $hash = @{ }
        if (($Include -band 1) -and -not ($Exclude -band 1))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.Messages'
            $hash["Messages"] = Get-PSFMessage
        }
        if (($Include -band 2) -and -not ($Exclude -band 2))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.MsgErrors'
            $hash["Errors"] = Get-PSFMessage -Errors
        }
        if (($Include -band 4) -and -not ($Exclude -band 4))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.ConsoleBuffer'
            $hash["ConsoleBuffer"] = Get-ShellBuffer
        }
        if (($Include -band 8) -and -not ($Exclude -band 8))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.OperatingSystem'
            $hash["OperatingSystem"] = if ($IsLinux -or $IsMacOs)
            {
                [PSCustomObject]@{
                    OSVersion = [System.Environment]::OSVersion
                    ProcessorCount = [System.Environment]::ProcessorCount
                    Is64Bit   = [System.Environment]::Is64BitOperatingSystem
                    LogicalDrives = [System.Environment]::GetLogicalDrives()
                    SystemDirectory = [System.Environment]::SystemDirectory
                }
            }
            else
            {
                Get-CimInstance -ClassName Win32_OperatingSystem
            }
        }
        if (($Include -band 16) -and -not ($Exclude -band 16))
        {
            $hash["CPU"] = if ($IsLinux -and (Test-Path -Path /proc/cpuinfo))
            {
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.CPU' -StringValues '/proc/cpuinfo'
                Get-Content -Raw -Path /proc/cpuinfo
            }
            else
            {
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.CPU' -StringValues Win32_Processor
                Get-CimInstance -ClassName Win32_Processor
            }
        }
        if (($Include -band 32) -and -not ($Exclude -band 32))
        {
            $hash["Ram"] = if ($IsLinux -and (Test-Path -Path /proc/meminfo))
            {
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.RAM' -StringValues '/proc/meminfo'
                Get-Content -Raw -Path /proc/meminfo
            }
            else
            {
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.RAM' -StringValues Win32_PhysicalMemory
                Get-CimInstance -ClassName Win32_PhysicalMemory
            }
        }
        if (($Include -band 64) -and -not ($Exclude -band 64))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.PSVersion'
            $hash["PSVersion"] = $PSVersionTable
        }
        if (($Include -band 128) -and -not ($Exclude -band 128))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.History'
            $hash["History"] = Get-History
        }
        if (($Include -band 256) -and -not ($Exclude -band 256))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.Modules'
            $hash["Modules"] = Get-Module
        }
        if ((($Include -band 512) -and -not ($Exclude -band 512)) -and (Get-Command -Name Get-PSSnapIn -ErrorAction SilentlyContinue))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.Snapins'
            $hash["SnapIns"] = Get-PSSnapin
        }
        if (($Include -band 1024) -and -not ($Exclude -band 1024))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.Assemblies'
            $hash["Assemblies"] = [appdomain]::CurrentDomain.GetAssemblies() | Select-Object CodeBase, FullName, Location, ImageRuntimeVersion, GlobalAssemblyCache, IsDynamic
        }
        if (Test-PSFParameterBinding -ParameterName "Variables")
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.Variables' -StringValues ($Variables -join ", ")
            $hash["Variables"] = $Variables | Get-Variable -ErrorAction Ignore
        }
        if (($Include -band 2048) -and -not ($Exclude -band 2048) -and (-not $ExcludeError))
        {
            Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.PSErrors'
            $hash["PSErrors"] = @()
            foreach ($errorItem in $global:Error) { $hash["PSErrors"] += New-Object PSFramework.Message.PsfException($errorItem) }
        }
        if (($Include -band 4096) -and -not ($Exclude -band 4096))
        {
            if (Test-Path function:Get-DbatoolsLog)
            {
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.DbaTools.Messages'
                $hash["DbatoolsMessages"] = Get-DbatoolsLog
                Write-PSFMessage -Level Important -String 'New-PSFSupportPackage.DbaTools.Errors'
                $hash["DbatoolsErrors"] = Get-DbatoolsLog -Errors
            }
        }
        
        $data = [pscustomobject]$hash
        
        try { $data | Export-PsfClixml -Path $filePathXml -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -String 'New-PSFSupportPackage.Export.Failed' -ErrorRecord $_ -Target $filePathXml -EnableException $EnableException
            return
        }
        
        try { Compress-Archive -Path $filePathXml -DestinationPath $filePathZip -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -String 'New-PSFSupportPackage.ZipCompression.Failed' -ErrorRecord $_ -Target $filePathZip -EnableException $EnableException
            return
        }
        
        Remove-Item -Path $filePathXml -ErrorAction Ignore
    }
}

function Remove-PSFAlias
{
<#
    .SYNOPSIS
        Removes an alias from the global scope.
     
    .DESCRIPTION
        Removes an alias from the global* scope.
        Please note that this always affects the global scope and should not be used lightly.
        This has the potential to break code that does not comply with PowerShell best practices and relies on the use of aliases.
     
        Refuses to delete constant aliases.
        Requires the '-Force' parameter to delete ReadOnly aliases.
     
        *This includes aliases exported by modules.
     
    .PARAMETER Name
        The name of the alias to remove.
     
    .PARAMETER Force
        Enforce removal of aliases. Required to remove ReadOnly aliases (including default aliases such as "select" or "group").
     
    .EXAMPLE
        PS C:\> Remove-PSFAlias -Name 'grep'
     
        Removes the global alias 'grep'
     
    .EXAMPLE
        PS C:\> Remove-PSFAlias -Name 'select' -Force
     
        Removes the default alias 'select'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [string[]]
        $Name,
        
        [switch]
        $Force
    )
    
    process
    {
        foreach ($alias in $Name)
        {
            try { [PSFramework.Utility.UtilityHost]::RemovePowerShellAlias($alias, $Force.ToBool()) }
            catch { Stop-PSFFunction -Message $_ -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ -OverrideExceptionMessage }
        }
    }
}

function Resolve-PSFDefaultParameterValue
{
<#
    .SYNOPSIS
        Used to filter and process default parameter values.
     
    .DESCRIPTION
        This command picks all the default parameter values from a reference hashtable.
        It then filters all that match a specified command and binds them to that specific command, narrowing its focus.
        These get merged into either a new or a specified hashtable and returned.
     
    .PARAMETER Reference
        The hashtable to pick default parameter values from.
     
    .PARAMETER CommandName
        The commands to pick default parameter values for.
     
    .PARAMETER Target
        The target hashtable to merge results into.
        By default an empty hashtable is used.
     
    .PARAMETER ParameterName
        Only resolve for specific parameter names.
     
    .EXAMPLE
        PS C:\> Resolve-PSFDefaultParameterValue -Reference $global:PSDefaultParameterValues -CommandName 'Invoke-WebRequest'
     
        Returns a hashtable containing all default parameter values in the global scope affecting the command 'Invoke-WebRequest'.
#>

    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Resolve-PSFDefaultParameterValue')]
    param (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Reference,
        
        [Parameter(Mandatory = $true)]
        [string[]]
        $CommandName,
        
        [System.Collections.Hashtable]
        $Target = @{ },
        
        [string[]]
        $ParameterName = "*"
    )
    
    begin
    {
        $defaultItems = @()
        foreach ($key in $Reference.Keys)
        {
            $defaultItems += [PSCustomObject]@{
                Key        = $key
                Value   = $Reference[$key]
                Command = $key.Split(":")[0]
                Parameter = $key.Split(":")[1]
            }
        }
    }
    process
    {
        foreach ($command in $CommandName)
        {
            foreach ($item in $defaultItems)
            {
                if ($command -notlike $item.Command) { continue }
                
                foreach ($parameter in $ParameterName)
                {
                    if ($item.Parameter -like $parameter)
                    {
                        if ($parameter -ne "*") { $Target["$($command):$($parameter)"] = $item.Value }
                        else { $Target["$($command):$($item.Parameter)"] = $item.Value }
                    }
                }
            }
        }
    }
    end
    {
        $Target
    }
}

function Resolve-PSFPath
{
<#
    .SYNOPSIS
        Resolves a path.
     
    .DESCRIPTION
        Resolves a path.
        Will try to resolve to paths including some basic path validation and resolution.
        Will fail if the path cannot be resolved (so an existing path must be reached at).
     
    .PARAMETER Path
        The path to validate.
     
    .PARAMETER Provider
        Ensure the path is of the expected provider.
        Allows ensuring one does not operate in the wrong provider.
        Common providers include the filesystem, the registry or the active directory.
     
    .PARAMETER SingleItem
        Ensure the path should resolve to a single path only.
        This may - intentionally or not - trip up wildcard paths.
     
    .PARAMETER NewChild
        Assumes one wishes to create a new child item.
        The parent path will be resolved and must validate true.
        The final leaf will be treated as a leaf item that does not exist yet.
     
    .EXAMPLE
        PS C:\> Resolve-PSFPath -Path report.log -Provider FileSystem -NewChild -SingleItem
     
        Ensures the resolved path is a FileSystem path.
        This will resolve to the current folder and the file report.log.
        Will not ensure the file exists or doesn't exist.
        If the current path is in a different provider, it will throw an exception.
     
    .EXAMPLE
        PS C:\> Resolve-PSFPath -Path ..\*
     
        This will resolve all items in the parent folder, whatever the current path or drive might be.
#>

    [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Resolve-PSFPath')]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]]
        $Path,
        
        [string]
        $Provider,
        
        [switch]
        $SingleItem,
        
        [switch]
        $NewChild
    )
    
    process
    {
        foreach ($inputPath in $Path)
        {
            if ($inputPath -eq ".")
            {
                $inputPath = (Get-Location).Path
            }
            if ($NewChild)
            {
                $parent = Split-Path -Path $inputPath
                $child = Split-Path -Path $inputPath -Leaf
                
                try
                {
                    if (-not $parent) { $parentPath = Get-Location -ErrorAction Stop }
                    else { $parentPath = Resolve-Path $parent -ErrorAction Stop }
                }
                catch { Stop-PSFFunction -String 'Resolve-PSFPath.Path.ParentExistsNot' -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet }
                
                if ($SingleItem -and (($parentPath | Measure-Object).Count -gt 1))
                {
                    Stop-PSFFunction -String 'Resolve-PSFPath.Path.MultipleParents' -EnableException $true -Cmdlet $PSCmdlet
                }
                
                if ($Provider -and ($parentPath.Provider.Name -ne $Provider))
                {
                    Stop-PSFFunction -String 'Resolve-PSFPath.Path.WrongProvider' -StringValues $parentPath.Provider.Name, $Provider -EnableException $true -Cmdlet $PSCmdlet
                }
                
                foreach ($parentItem in $parentPath)
                {
                    Join-Path $parentItem.ProviderPath $child
                }
            }
            else
            {
                try { $resolvedPaths = Resolve-Path $inputPath -ErrorAction Stop }
                catch { Stop-PSFFunction -String 'Resolve-PSFPath.Path.ExistsNot' -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet }
                
                if ($SingleItem -and (($resolvedPaths | Measure-Object).Count -gt 1))
                {
                    Stop-PSFFunction -String 'Resolve-PSFPath.Path.MultipleItems' -EnableException $true -Cmdlet $PSCmdlet
                }
                
                if ($Provider -and ($resolvedPaths.Provider.Name -ne $Provider))
                {
                    Stop-PSFFunction -String 'Resolve-PSFPath.Path.WrongProvider' -StringValues $Provider, $resolvedPaths.Provider.Name -EnableException $true -Cmdlet $PSCmdlet
                }
                
                $resolvedPaths.ProviderPath
            }
        }
    }
}

function Select-PSFPropertyValue
{
<#
    .SYNOPSIS
        Expand specific property values based on selection logic.
     
    .DESCRIPTION
        This command allows picking a set of properties and then returning ...
        - All their values
        - The value that meets specific rules
        - A composite value
     
    .PARAMETER Property
        The properties to work with, in the order they should be considered.
     
    .PARAMETER Fallback
        Whether to fall back on other properties if the first one doesn't contain values.
        This picks the value of the first property that actually has a value.
     
    .PARAMETER Select
        Select either the largest or lowest propertyvalue in the Propertynames specified.
     
    .PARAMETER JoinBy
        Joins the selected properties by the string specified.
     
    .PARAMETER FormatWith
        Formats the selected properties into the specified format string.
     
    .PARAMETER InputObject
        The object(s) whose properties to inspect.
     
    .EXAMPLE
        PS C:\> Get-ADComputer -Filter * | Select-PSFPropertyValue -Property 'DNSHostName', 'Name' -Fallback
         
        For each computer in the domain, it will pick the DNSHostName if available, otherwise the Name.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string[]]
        $Property,
        
        [Parameter(ParameterSetName = 'Fallback')]
        [switch]
        $Fallback,
        
        [Parameter(ParameterSetName = 'Select')]
        [ValidateSet('Lowest', 'Largest')]
        [string]
        $Select,
        
        [Parameter(ParameterSetName = 'Join')]
        [string]
        $JoinBy,
        
        [Parameter(ParameterSetName = 'Format')]
        [string]
        $FormatWith,
        
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    
    process
    {
        foreach ($object in $InputObject)
        {
            switch ($PSCmdlet.ParameterSetName)
            {
                'Default'
                {
                    foreach ($prop in $Property)
                    {
                        $object.$Prop
                    }
                }
                'Fallback'
                {
                    foreach ($prop in $Property)
                    {
                        if ($null -ne ($object.$Prop | Remove-PSFNull -Enumerate))
                        {
                            $object.$prop
                            break
                        }
                    }
                }
                'Select'
                {
                    $values = @()
                    foreach ($prop in $Property)
                    {
                        $values += $object.$Prop
                    }
                    if ($Select -eq 'Largest') { $values | Sort-Object -Descending | Select-Object -First 1 }
                    else { $values | Sort-Object | Select-Object -First 1 }
                    
                }
                'Join'
                {
                    $values = @()
                    foreach ($prop in $Property)
                    {
                        $values += $object.$Prop
                    }
                    $values -join $JoinBy
                }
                'Format'
                {
                    $values = @()
                    foreach ($prop in $Property)
                    {
                        $values += $object.$Prop
                    }
                    $FormatWith -f $values
                }
            }
        }
    }
}

function Set-PSFPath
{
<#
    .SYNOPSIS
        Configures or updates a path under a name.
     
    .DESCRIPTION
        Configures or updates a path under a name.
        The path can be persisted using the "-Register" command.
        Paths setup like this can be retrieved using Get-PSFPath.
     
    .PARAMETER Name
        Name the path should be stored under.
     
    .PARAMETER Path
        The path that should be returned under the name.
     
    .PARAMETER Register
        Registering a path in order for it to persist across sessions.
     
    .PARAMETER Scope
        The configuration scope it should be registered under.
        Defaults to UserDefault.
        Configuration scopes are the default locations configurations are being stored at.
        For more details see:
        https://psframework.org/documentation/documents/psframework/configuration/persistence-location.html
     
    .EXAMPLE
        PS C:\> Set-PSFPath -Name 'temp' -Path 'C:\temp'
     
        Configures C:\temp as the current temp path. (does not override $env:temp !)
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(ParameterSetName = 'Register', Mandatory = $true)]
        [switch]
        $Register,
        
        [Parameter(ParameterSetName = 'Register')]
        [PSFramework.Configuration.ConfigScope]
        $Scope = [PSFramework.Configuration.ConfigScope]::UserDefault
    )
    
    process
    {
        Set-PSFConfig -FullName "PSFramework.Path.$Name" -Value $Path
        if ($Register) { Register-PSFConfig -FullName "PSFramework.Path.$Name" -Scope $Scope }
    }
}

function Set-PSFScriptblock
{
<#
    .SYNOPSIS
        Stores a scriptblock in the central scriptblock store.
     
    .DESCRIPTION
        Stores a scriptblock in the central scriptblock store.
        This store can be accessed using Get-PSFScriptblock.
        It is used to share scriptblocks outside of scope and runspace boundaries.
        Scriptblocks thus registered can be accessed by C#-based services, such as the PsfValidateScript attribute.
     
    .PARAMETER Name
        The name of the scriptblock.
        Must be unique, it is recommended to prefix the module name:
        <Module>.<Scriptblock>
     
    .PARAMETER Scriptblock
        The scriptcode to register
     
    .PARAMETER Global
        Whether the scriptblock should be invoked in the global context.
        If defined, accessing the scriptblock will automatically globalize it before returning it.
     
    .PARAMETER Local
        Whether the scriptblock should be local to the current runspace.
        If defined, each runspace must define its own instance of the scriptblock to use it.
 
    .PARAMETER Tag
        A list of tags to apply to a scriptblock. Used for easier filtering.
 
    .PARAMETER Description
        A description for a scriptblock. Used for easier filtering and in lieu of documentation.
     
    .EXAMPLE
        PS C:\> Set-PSFScriptblock -Name 'MyModule.TestServer' -Scriptblock $Scriptblock
     
        Stores the scriptblock contained in $Scriptblock under the 'MyModule.TestServer' name.
     
    .EXAMPLE
        PS C:\> Set-PSFScriptblock -Name 'MyModule.TestServer' -Scriptblock $Scriptblock -Tag Awesome, BestCodeEver -Description 'This scriptblock is the only one you need'
     
        Stores the scriptblock contained in $Scriptblock under the 'MyModule.TestServer' name.
        Applies the tags 'Awesome' and 'BestCodeEver', as well as a descriptive text.
     
    .NOTES
        Repeatedly registering the same scriptblock (e.g. in multi-runspace scenarios) is completely safe:
        - Access is threadsafe & Runspacesafe
        - Overwriting the scriptblock does not affect the statistics
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Position = 1, Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $Scriptblock,
        
        [switch]
        $Global,
        
        [switch]
        $Local,
        
        [AllowEmptyCollection()]
        [string[]]
        $Tag,
        
        [AllowEmptyString()]
        [string]
        $Description
    )
    process
    {
        if ([PSFramework.Utility.UtilityHost]::ScriptBlocks.ContainsKey($Name))
        {
            [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Scriptblock = $Scriptblock
            if (Test-PSFParameterBinding -ParameterName Global -BoundParameters $PSBoundParameters) { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Global = $Global }
            if (Test-PSFParameterBinding -ParameterName Local -BoundParameters $PSBoundParameters) { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Local = $Local }
            if ($null -ne $Tag) { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Tag = $Tag }
            if (Test-PSFParameterBinding -ParameterName Description -BoundParameters $PSBoundParameters) { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Description = $Description }
        }
        else
        {
            [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name] = New-Object PSFramework.Utility.ScriptBlockItem($Name, $Scriptblock, $Global, $Local, $Tag, $Description)
        }
    }
}


<#
Registers the cmdlets published by this module.
Necessary for full hybrid module support.
#>

$commonParam = @{
    HelpFile  = (Resolve-Path "$($script:ModuleRoot)\en-us\PSFramework.dll-Help.xml")
    Module = $ExecutionContext.SessionState.Module
}

Import-PSFCmdlet @commonParam -Name ConvertTo-PSFHashtable -Type ([PSFramework.Commands.ConvertToPSFHashtableCommand])
Import-PSFCmdlet @commonParam -Name Invoke-PSFCallback -Type ([PSFramework.Commands.InvokePSFCallbackCommand])
Import-PSFCmdlet @commonParam -Name Invoke-PSFProtectedCommand -Type ([PSFramework.Commands.InvokePSFProtectedCommand])
Import-PSFCmdlet @commonParam -Name Remove-PSFNull -Type ([PSFramework.Commands.RemovePSFNullCommand])
Import-PSFCmdlet @commonParam -Name Select-PSFObject -Type ([PSFramework.Commands.SelectPSFObjectCommand])
Import-PSFCmdlet @commonParam -Name Set-PSFObjectOrder -Type ([PSFramework.Commands.SortPSFObjectCommand])
Import-PSFCmdlet @commonParam -Name Set-PSFConfig -Type ([PSFramework.Commands.SetPSFConfigCommand])
Import-PSFCmdlet @commonParam -Name Test-PSFShouldProcess -Type ([PSFramework.Commands.TestPSFShouldProcessCommand])
Import-PSFCmdlet @commonParam -Name Write-PSFMessage -Type ([PSFramework.Commands.WritePSFMessageCommand])

Set-Alias -Name Sort-PSFObject -Value Set-PSFObjectOrder -Force -ErrorAction SilentlyContinue

# Define our type aliases
$TypeAliasTable = @{
    PsfArgumentCompleter    = "PSFramework.TabExpansion.PsfArgumentCompleterAttribute"
    PSFComputer                = "PSFramework.Parameter.ComputerParameter"
    PSFComputerParameter    = "PSFramework.Parameter.ComputerParameter"
    PSFDateTime                = "PSFramework.Parameter.DateTimeParameter"
    PSFDateTimeParameter    = "PSFramework.Parameter.DateTimeParameter"
    PsfDynamicTransform        = 'PSFramework.Utility.DynamicTransformationAttribute'
    PSFEncoding                = "PSFramework.Parameter.EncodingParameter"
    PSFEncodingParameter    = "PSFramework.Parameter.EncodingParameter"
    PSFNumber                = 'PSFramework.Utility.Number'
    psfrgx                    = "PSFramework.Utility.RegexHelper"
    PsfScriptBlock            = 'PSFramework.Utility.PsfScriptBlock'
    PSFSize                    = "PSFramework.Utility.Size"
    PSFTimeSpan                = "PSFramework.Parameter.TimeSpanParameter"
    PSFTimeSpanParameter    = "PSFramework.Parameter.TimeSpanParameter"
    PsfValidateLanguageMode = "PSFramework.Validation.PsfValidateLanguageMode"
    PSFValidatePattern        = "PSFramework.Validation.PsfValidatePatternAttribute"
    PSFValidatePSVersion    = "PSFramework.Validation.PsfValidatePSVersion"
    PSFValidateScript        = "PSFramework.Validation.PsfValidateScriptAttribute"
    PSFValidateSet            = "PSFramework.Validation.PsfValidateSetAttribute"
}

Set-PSFTypeAlias -Mapping $TypeAliasTable

Import-PSFLocalizedString -Path "$script:ModuleRoot\en-us\*.psd1" -Module PSFramework -Language 'en-US'

$script:strings = Get-PSFLocalizedString -Module PSFramework

Register-PSFConfigSchema -Name Default -Schema {
    param (
        [string]
        $Resource,
        
        [System.Collections.Hashtable]
        $Settings
    )
    
    #region Converting parameters
    $Peek = $Settings["Peek"]
    $ExcludeFilter = $Settings["ExcludeFilter"]
    $IncludeFilter = $Settings["IncludeFilter"]
    $AllowDelete = $Settings["AllowDelete"]
    $EnableException = $Settings["EnableException"]
    Set-Location -Path $Settings["Path"]
    $PassThru = $Settings["PassThru"]
    #endregion Converting parameters
    
    #region Utility Function
    function Read-PsfConfigFile
    {
<#
    .SYNOPSIS
        Reads a configuration file and parses it.
     
    .DESCRIPTION
        Reads a configuration file and parses it.
     
    .PARAMETER Path
        The path to the file to parse.
     
    .PARAMETER WebLink
        The link to a website to download straight as raw json.
     
    .PARAMETER RawJson
        Raw json data to interpret.
     
    .EXAMPLE
        PS C:\> Read-PsfConfigFile -Path config.json
     
        Reads the config.json file and returns interpreted configuration objects.
#>

        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
            [string]
            $Path,
            
            [Parameter(Mandatory = $true, ParameterSetName = 'Weblink')]
            [string]
            $Weblink,
            
            [Parameter(Mandatory = $true, ParameterSetName = 'RawJson')]
            [string]
            $RawJson
        )
        
        #region Utility Function
        function New-ConfigItem
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $FullName,
                
                $Value,
                
                $Type,
                
                [switch]
                $KeepPersisted,
                
                [switch]
                $Enforced,
                
                [switch]
                $Policy
            )
            
            [pscustomobject]@{
                FullName      = $FullName
                Value          = $Value
                Type          = $Type
                KeepPersisted = $KeepPersisted
                Enforced      = $Enforced
                Policy          = $Policy
            }
        }
        
        function Get-WebContent
        {
            [CmdletBinding()]
            param (
                [string]
                $WebLink
            )
            
            $webClient = New-Object System.Net.WebClient
            $webClient.Encoding = [System.Text.Encoding]::UTF8
            $webClient.DownloadString($WebLink)
        }
        #endregion Utility Function
        
        if ($Path)
        {
            if (-not (Test-Path $Path)) { return }
            $data = Get-Content -Path $Path -Encoding UTF8 -Raw | ConvertFrom-Json -ErrorAction Stop
        }
        if ($Weblink)
        {
            $data = Get-WebContent -WebLink $Weblink | ConvertFrom-Json -ErrorAction Stop
        }
        if ($RawJson)
        {
            $data = $RawJson | ConvertFrom-Json -ErrorAction Stop
        }
        
        foreach ($item in $data)
        {
            #region No Version
            if (-not $item.Version)
            {
                New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type))
            }
            #endregion No Version
            
            #region Version One
            if ($item.Version -eq 1)
            {
                if ((-not $item.Style) -or ($item.Style -eq "Simple")) { New-ConfigItem -FullName $item.FullName -Value $item.Data }
                else
                {
                    if (($item.Type -eq "Object") -or ($item.Type -eq 12))
                    {
                        New-ConfigItem -FullName $item.FullName -Value $item.Value -Type "Object" -KeepPersisted
                    }
                    else
                    {
                        New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type))
                    }
                }
            }
            #endregion Version One
        }
    }
    #endregion Utility Function
    
    try
    {
        if ($Resource -like "http*") { $data = Read-PsfConfigFile -Weblink $Resource -ErrorAction Stop }
        else
        {
            $pathItem = $null
            try { $pathItem = Resolve-PSFPath -Path $Resource -SingleItem -Provider FileSystem }
            catch { }
            if ($pathItem) { $data = Read-PsfConfigFile -Path $pathItem -ErrorAction Stop }
            else { $data = Read-PsfConfigFile -RawJson $Resource -ErrorAction Stop }
        }
    }
    catch { Stop-PSFFunction -String 'Configuration.Schema.Default.ImportFailed' -StringValues $Resource -EnableException $EnableException -Tag 'fail', 'import' -ErrorRecord $_ -Continue -Target $Resource -Cmdlet $Settings["Cmdlet"] }
    
    :element foreach ($element in $data)
    {
        #region Exclude Filter
        foreach ($exclusion in $ExcludeFilter)
        {
            if ($element.FullName -like $exclusion)
            {
                continue element
            }
        }
        #endregion Exclude Filter
        
        #region Include Filter
        if ($IncludeFilter)
        {
            $isIncluded = $false
            foreach ($inclusion in $IncludeFilter)
            {
                if ($element.FullName -like $inclusion)
                {
                    $isIncluded = $true
                    break
                }
            }
            
            if (-not $isIncluded) { continue }
        }
        #endregion Include Filter
        
        if ($Peek) { $element }
        else
        {
            try
            {
                if (-not $element.KeepPersisted) { Set-PSFConfig -FullName $element.FullName -Value $element.Value -EnableException -AllowDelete:$AllowDelete -PassThru:$PassThru }
                else { Set-PSFConfig -FullName $element.FullName -PersistedValue $element.Value -PersistedType $element.Type -AllowDelete:$AllowDelete -PassThru:$PassThru }
            }
            catch
            {
                Stop-PSFFunction -String 'Configuration.Schema.Default.SetFailed' -StringValues $element.FullName -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'import' -Continue -Target $Resource -Cmdlet $Settings["Cmdlet"]
            }
        }
    }
}

Register-PSFConfigSchema -Name MetaJson -Schema {
    param (
        [string]
        $Resource,
        
        [System.Collections.Hashtable]
        $Settings
    )
    
    Write-PSFMessage -String 'Configuration.Schema.MetaJson.ProcessResource' -StringValues $Resource -ModuleName PSFramework
    
    #region Converting parameters
    $Peek = $Settings["Peek"]
    $ExcludeFilter = $Settings["ExcludeFilter"]
    $IncludeFilter = $Settings["IncludeFilter"]
    $AllowDelete = $Settings["AllowDelete"]
    $script:EnableException = $Settings["EnableException"]
    $script:cmdlet = $Settings["Cmdlet"]
    Set-Location -Path $Settings["Path"]
    $PassThru = $Settings["PassThru"]
    #endregion Converting parameters
    
    #region Utility Function
    function Read-V1Node
    {
        [CmdletBinding()]
        param (
            $NodeData,
            
            [string]
            $Path,
            
            [Hashtable]
            $Result,
            
            [string]
            $Type,
            
            [Hashtable]
            $Settings
        )
        
        Write-PSFMessage -String 'Configuration.Schema.MetaJson.ProcessFile' -StringValues $Path -ModuleName PSFramework
        
        $basePath = switch ($Type) {
            'file' { Split-Path -Path $Path }
            'weblink' { $Path -replace '/[^/]+?$' }
            default { $null }
        }
        
        if ($NodeData.ModuleName) { $moduleName = "{0}." -f $NodeData.ModuleName }
        else { $moduleName = "" }
        
        #region Import Resources
        foreach ($property in $NodeData.Static.PSObject.Properties)
        {
            $Result["$($moduleName)$($property.Name)"] = $property.Value
        }
        foreach ($property in $NodeData.Object.PSObject.Properties)
        {
            $Result["$($moduleName)$($property.Name)"] = $property.Value | ConvertFrom-PSFClixml
        }
        foreach ($property in $NodeData.Dynamic.PSObject.Properties)
        {
            $Result["$($moduleName)$(Resolve-V1String -String $property.Name)"] = Resolve-V1String -String $property.Value
        }
        foreach ($property in $NodeData.Tree.PSObject.Properties) {
            Resolve-V1Tree -Property $property -Result $Result -BaseElement @()
        }
        foreach ($property in $NodeData.DynamicTree.PSObject.Properties) {
            Resolve-V1Tree -Property $property -Result $Result -BaseElement @() -Dynamic $true
        }
        #endregion Import Resources
        
        #region Import included / linked configuration files
        :includes foreach ($include in $NodeData.Include)
        {
            $resolvedInclude = Resolve-V1String -String $include
            $uri = [uri]$resolvedInclude
            
            # Skip relative paths if we do not have a base path to place it relative to
            if (-not $uri.IsAbsoluteUri -and -not $basePath) { continue }
            #region Calculate the new include path
            if ($uri.IsAbsoluteUri) { $includePath = $resolvedInclude }
            else {
                $includePath = switch ($Type) {
                    'file'
                    {
                        $joinedPath = Join-Path -Path $basePath -ChildPath ($resolvedInclude -replace '^\.\\', '\')
                        try { Resolve-PSFPath -Path $joinedPath -Provider FileSystem -SingleItem }
                        catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.ResolveFile' -StringValues $joinedPath -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -ContinueLabel includes -Cmdlet $script:cmdlet }
                    }
                    'weblink'
                    {
                        $newPath = $basePath
                        $relParts = $resolvedInclude -split "/"
                        foreach ($part in $relParts) {
                            if ($part -eq '..') { $newPath = $newPath -replace '/[^/]+$' }
                            else { $newPath = $newPath, $part -join '/' }
                        }
                        $newPath
                    }
                }
            }
            #endregion Calculate the new include path
            
            $newSettings = $Settings | ConvertTo-PSFHashtable -Include ExcludeFilter, IncludeFilter
            try { $configData = Import-PSFConfig -Path $includePath -Peek @newSettings -Schema MetaJson -EnableException -ErrorAction Stop }
            catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.ExecuteInclude.Error' -StringValues $includePath -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -ContinueLabel includes -Cmdlet $script:cmdlet }
            foreach ($configDatum in $configData) {
                $Result[$configDatum.FullName] = $configDatum.Value
            }
        }
        #endregion Import included / linked configuration files
        
        $Result
    }
    
    function Resolve-V1String
    {
    <#
        .SYNOPSIS
            Resolves a string by inserting placeholders for environment variables.
         
        .DESCRIPTION
            Resolves a string by inserting placeholders for environment variables.
         
        .PARAMETER String
            The string to resolve.
         
        .EXAMPLE
            PS C:\> Resolve-V1String -String '.\%COMPUTERNAME%\config.json'
         
            Resolves the specified string, inserting the local computername for %COMPUTERNAME%.
    #>

        [CmdletBinding()]
        param (
            $String
        )
        if ($String -isnot [string]) { return $String }
        
        $scriptblock = {
            param (
                $Match
            )
            
            $script:envData[$Match.Value]
        }
        
        [regex]::Replace($String, $script:envDataNamesRGX, $scriptblock)
    }
    
    function Resolve-V1Tree {
        [CmdletBinding()]
        param (
            $Property,
            
            [Hashtable]
            $Result,
            
            [bool]
            $Dynamic,
            
            [AllowEmptyCollection()]
            [string[]]
            $BaseElement
        )
        
        if ($Property.TypeNameOfValue -notin 'System.Management.Automation.PSCustomObject', 'System.Collections.Hashtable') {
            $name = (@($BaseElement) + @($Property.Name)) -join "."
            if ($Dynamic) { $Result[(Resolve-V1String -String $name)] = Resolve-V1String -String $Property.Value }
            else { $Result[$name] = $Property.Value }
            return
        }
        
        $value = $Property.Value
        if ($value -is [System.Collections.Hashtable]) { $value = [pscustomobject]$value }
        
        if ($value.'!Condition') {
            $conditionSet = $null
            if ($value.'!ConditionSet') {
                $module, $name = $value.'!ConditionSet' -split ' ', 2
                $conditionSet = Get-PSFFilterConditionSet -Module $module -Name $name | Select-Object -First 1
            }
            else {
                $conditionSet = Get-PSFFilterConditionSet -Module PSFramework -Name Environment
            }
            if (-not $conditionSet) { throw "Unable to resolve Condition Set: $($value.'!ConditionSet')" }
            $filter = New-PSFFilter -Expression $value.'!Condition' -ConditionSet $conditionSet
            if (-not $filter.Evaluate()) { return }
        }
        
        foreach ($propertyObject in $value.PSObject.Properties) {
            if ($propertyObject.Name -eq '!Condition') { continue }
            if ($propertyObject.Name -eq '!ConditionSet') { continue }
            
            if ($value.'!Condition') { Resolve-V1Tree -Property $propertyObject -Result $Result -Dynamic $Dynamic -BaseElement $BaseElement }
            else { Resolve-V1Tree -Property $propertyObject -Result $Result -Dynamic $Dynamic -BaseElement (@($BaseElement) + @($Property.Name)) }
        }
    }
    #endregion Utility Function
    
    #region Utility Computation
    $script:envData = @{ }
    foreach ($envItem in (Get-ChildItem env:\))
    {
        $script:envData["%$($envItem.Name)%"] = $envItem.Value
    }
    $script:envDataNamesRGX = $script:envData.Keys -join '|'
    #endregion Utility Computation
    
    #region Accessing Content
    try {
        $null = Resolve-PSFPath -Path $Resource -Provider FileSystem -SingleItem
        $resourceType = 'File'
    }
    catch {
        if ($Resource -match '^https{0,1}://') { $resourceType = 'weblink' }
        else { $resourceType = 'Json' }
    }
    switch ($resourceType) {
        #region Weblink
        'weblink'
        {
            try { $importData = Invoke-WebRequest -Uri $Resource -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
            catch {
                Stop-PSFFunction -String 'Configuration.Schema.MetaJson.WebError' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet
                return
            }
            $resolvedPath = $Resource
        }
        #endregion Weblink
        #region File
        'file'
        {
            try { $resolvedPath = Resolve-PSFPath -Path $Resource -Provider FileSystem -SingleItem }
            catch {
                Stop-PSFFunction -String 'Configuration.Schema.MetaJson.ResolveFile' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet
                return
            }
            
            switch -regex ($resolvedPath) {
                '\.psd1$'
                {
                    try { $importData = [pscustomobject](Import-PSFPowerShellDataFile -Path $resolvedPath -ErrorAction Stop) }
                    catch {
                        Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidPsd1' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet
                        return
                    }
                    if ($importData.Static -is [hashtable]) { $importData.Static = [pscustomobject]$importData.Static }
                    if ($importData.Object -is [hashtable]) { $importData.Object = [pscustomobject]$importData.Object }
                    if ($importData.Dynamic -is [hashtable]) { $importData.Dynamic = [pscustomobject]$importData.Dynamic }
                    if ($importData.Tree -is [hashtable]) { $importData.Tree = [pscustomobject]$importData.Tree }
                    if ($importData.DynamicTree -is [hashtable]) { $importData.DynamicTree = [pscustomobject]$importData.DynamicTree }
                }
                default
                {
                    try { $importData = Get-Content -Path $resolvedPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
                    catch {
                        Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidJson' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet
                        return
                    }
                }
            }
        }
        #endregion File
        #region Straight Json
        default
        {
            try {
                $importData = $Resource | ConvertFrom-Json -ErrorAction Stop
                $resolvedPath = ''
            }
            catch {
                Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidJson' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet
                return
            }
        }
        #endregion Straight Json
    }
    #endregion Accessing Content
    
    switch ($importData.Version)
    {
        1
        {
            $configurationHash = Read-V1Node -NodeData $importData -Path $resolvedPath -Type $resourceType -Result @{ } -Settings $Settings
            $configurationItems = $configurationHash.Keys | ForEach-Object {
                [pscustomobject]@{
                    FullName = $_
                    Value = $configurationHash[$_]
                }
            }
            
            foreach ($configItem in $configurationItems)
            {
                if ($ExcludeFilter | Where-Object { $configItem.FullName -like $_ }) { continue }
                if ($IncludeFilter -and -not ($IncludeFilter | Where-Object { $configItem.FullName -like $_ })) { continue }
                if ($Peek)
                {
                    $configItem
                    continue
                }
                
                Set-PSFConfig -FullName $configItem.FullName -Value $configItem.Value -AllowDelete:$AllowDelete -PassThru:$PassThru
            }
        }
        default
        {
            Stop-PSFFunction -String 'Configuration.Schema.MetaJson.UnknownVersion' -StringValues $Resource, $importData.Version -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -Cmdlet $script:cmdlet
            return
        }
    }
}

Register-PSFConfigValidation -Name "bool" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    try
    {
        if ($Value.GetType().FullName -notin "System.Boolean", 'System.Management.Automation.SwitchParameter')
        {
            $Result.Message = "Not a boolean: $Value"
            $Result.Success = $False
            return $Result
        }
    }
    catch
    {
        $Result.Message = "Not a boolean: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $Value -as [bool]
    
    return $Result
}

Register-PSFConfigValidation -Name "consolecolor" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [System.ConsoleColor]$color = $Value }
    catch
    {
        $Result.Message = "Not a console color: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $color
    
    return $Result
}

Register-PSFConfigValidation -Name "credential" -ScriptBlock {
    param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    try
    {
        if ($Value.GetType().FullName -ne "System.Management.Automation.PSCredential")
        {
            $Result.Message = "Not a credential: $Value"
            $Result.Success = $False
            return $Result
        }
    }
    catch
    {
        $Result.Message = "Not a credential: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $Value
    
    return $Result
}

Register-PSFConfigValidation -Name "datetime" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [DateTime]$DateTime = $Value }
    catch
    {
        $Result.Message = "Not a DateTime: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $DateTime
    
    return $Result
}

Register-PSFConfigValidation -Name "double" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSOBject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [double]$number = $Value }
    catch
    {
        $Result.Message = "Not a double: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $number
    
    return $Result
}

Register-PSFConfigValidation -Name "guidarray" -ScriptBlock {
    param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try
    {
        $data = @()
        foreach ($item in $Value)
        {
            $data += [guid]$item
        }
    }
    catch
    {
        $Result.Message = "Not a guid array: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $data
    
    return $Result
}

Register-PSFConfigValidation -Name "integer" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSOBject -Property @{
        Success = $True
        Value = $null
        Message = ""
    }
    
    try { [int]$number = $Value }
    catch
    {
        $Result.Message = "Not an integer: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $number
    
    return $Result
}

Register-PSFConfigValidation -Name "integer0to9" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value = $null
        Message = ""
    }
    
    try { [int]$number = $Value }
    catch
    {
        $Result.Message = "Not an integer: $Value"
        $Result.Success = $False
        return $Result
    }
    
    if (($number -lt 0) -or ($number -gt 9))
    {
        $Result.Message = "Out of range. Specify a number ranging from 0 to 9"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $Number
    
    return $Result
}

Register-PSFConfigValidation -Name "integer1to9" -ScriptBlock {
    param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [int]$number = $Value }
    catch
    {
        $Result.Message = "Not an integer: $Value"
        $Result.Success = $False
        return $Result
    }
    
    if (($number -lt 1) -or ($number -gt 9))
    {
        $Result.Message = "Out of range. Specify a number ranging from 1 to 9"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $Number
    
    return $Result
}

Register-PSFConfigValidation -Name "integerarray" -ScriptBlock {
    param (
        $var
    )
    
    $test = $true
    try { [int[]]$res = $var }
    catch { $test = $false }
    
    [pscustomobject]@{
        Success = $test
        Value   = $res
        Message = "Casting $var as [int[]] failure. Input is being identified as $($var.GetType())"
    }
}

Register-PSFConfigValidation -Name "integerpositive" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSOBject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [int]$number = $Value }
    catch
    {
        $Result.Message = "Not an integer: $Value"
        $Result.Success = $False
        return $Result
    }
    
    if ($number -lt 0)
    {
        $Result.Message = "Negative value: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $number
    
    return $Result
}

Register-PSFConfigValidation -Name "languagecode" -ScriptBlock {
    param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    $legal = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | Where-Object { $_ -and ($_.Trim()) }
    
    if ($Value -in $legal)
    {
        $Result.Value = [string]$Value
    }
    else
    {
        $Result.Success = $false
        $Result.Message = [PSFramework.Localization.LocalizationHost]::Read('PSFramework.Configuration_ValidateLanguage')
    }
    
    return $Result
}

Register-PSFConfigValidation -Name "psframework.logfilefiletype" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success  = $True
        Value    = $null
        Message  = ""
    }
    
    try { [PSFramework.Logging.LogFileFileType]$type = $Value }
    catch
    {
        $Result.Message = "Not a logfile file type: $Value . Specify one of these values: $(([enum]::GetNames([PSFramework.Logging.LogFileFileType])) -join ", ")"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $type
    
    return $Result
}

Register-PSFConfigValidation -Name "long" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSOBject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [long]$number = $Value }
    catch
    {
        $Result.Message = "Not a long: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $number
    
    return $Result
}

Register-PSFConfigValidation -Name "sizestyle" -ScriptBlock {
    param (
        $Value
    )

    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }

    try { [PSFramework.Utility.SizeStyle]$style = $Value }
    catch {
        $Result.Message = "Not a size style: $Value"
        $Result.Success = $False
        return $Result
    }

    $Result.Value = $style

    return $Result
}

Register-PSFConfigValidation -Name "string" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try
    {
        # Seriously, this should work for almost anybody and anything
        [string]$data = $Value
    }
    catch
    {
        $Result.Message = "Not a string: $Value"
        $Result.Success = $False
        return $Result
    }
    
    if ([string]::IsNullOrEmpty($data))
    {
        $Result.Message = "Is an empty string: $Value"
        $Result.Success = $False
        return $Result
    }
    
    if ($data -eq $Value.GetType().FullName)
    {
        $Result.Message = "Is an object with no proper string representation: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $data
    
    return $Result
}

Register-PSFConfigValidation -Name "stringarray" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success  = $True
        Value    = $null
        Message  = ""
    }
    
    try
    {
        $data = @()
        # Seriously, this should work for almost anybody and anything
        foreach ($item in $Value)
        {
            $data += [string]$item
        }
    }
    catch
    {
        $Result.Message = "Not a string array: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $data
    
    return $Result
}

Register-PSFConfigValidation -Name "timespan" -ScriptBlock {
    Param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    try { [timespan]$timespan = [PSFramework.Parameter.TimeSpanParameter]$Value }
    catch
    {
        $Result.Message = "Not a Timespan: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $timespan
    
    return $Result
}

Register-PSFConfigValidation -Name "uriabsolute" -ScriptBlock {
    param (
        $Value
    )
    
    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }
    
    $stringValue = $Value -as [string]
    [uri]$uri = $stringValue
    
    if (-not $uri.IsAbsoluteUri)
    {
        $Result.Message = "Not an absolute Uri: $Value"
        $Result.Success = $False
        return $Result
    }
    
    $Result.Value = $stringValue
    
    return $Result
}

Set-PSFConfig -Module 'PSFramework' -Name 'ComputerManagement.PSSession.IdleTimeout' -Value (New-TimeSpan -Minutes 15) -Initialize -Validation 'timespan' -Handler { [PSFramework.ComputerManagement.ComputerManagementHost]::PSSessionIdleTimeout = $args[0] } -Description "The idle timeout for cached pssessions. When using Invoke-PSFCommand, it will remember sessions for up to this time after last using them, before cleaning them up."

# Unattended mode, so there is a central flag scripts & modules can detect
Set-PSFConfig -Module PSFramework -Name 'System.Unattended' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.PSFCore.PSFCoreHost]::Unattended = $args[0] } -Description "Central setting, showing whether the current execution is unattended or not. This allows scripts/modules to react to whether there is a user at the controls or not."

Set-PSFConfig -Module PSFramework -Name 'SupportPackage.ContactMessage' -Value ' ' -Initialize -Validation 'string' -Description 'Message shown when using New-PSFSUpportPackage. This allows an organization to tie information on how to submit a support package into the command that generates it'

# Encoding Settings
Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.FullTabCompletion' -Value $false -Initialize -Validation 'bool' -Description 'Whether all encodings should be part of the tab completion for encodings. By default, only a manageable subset is shown.'
Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultWrite' -Value 'utf-8' -Initialize -Validation 'string' -Description 'The default encoding to use when writing to file. Only applied by implementing commands.'
Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultRead' -Value 'utf-8' -Initialize -Validation 'string' -Description 'The default encoding to use when reading from file. Only applied by implementing commands.'

# Localization Stuff
Set-PSFConfig -Module PSFramework -Name 'Localization.Language' -Value ([System.Globalization.CultureInfo]::CurrentUICulture.Name) -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::Language = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under'
Set-PSFConfig -Module PSFramework -Name 'Localization.LoggingLanguage' -Value 'en-US' -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::LoggingLanguage = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under'

Set-PSFConfig -Module PSFramework -Name 'Logging.MaxErrorCount' -Value 128 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxErrorCount = $args[0] } -Description "The maximum number of error records maintained in-memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.MaxMessageCount' -Value 1024 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxMessageCount = $args[0] } -Description "The maximum number of messages that can be maintained in the in-memory message queue. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.MessageLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogEnabled = $args[0] } -Description "Governs, whether a log of recent messages is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.ErrorLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogEnabled = $args[0] } -Description "Governs, whether a log of recent errors is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.DisableLogFlush' -Value $false -Initialize -Validation "bool" -Description "When shutting down the process, PSFramework will by default flush the log. This ensures that all events are properly logged. If this is not desired, it can be turned off with this setting."
Set-PSFConfig -Module PSFramework -Name 'Logging.Interval' -Value 1000 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::Interval = $args[0] } -Description 'The interval at which the loging runspace runs. Increase to improve performance, reduce the minimize writing latency.'
Set-PSFConfig -Module PSFramework -Name 'Logging.Interval.Idle' -Value 5000 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::IntervalIdle = $args[0] } -Description 'The interval at which the loging runspace runs, when there is nothing to do.'
Set-PSFConfig -Module PSFramework -Name 'Logging.Interval.IdleDuration' -Value (New-TimeSpan -Minutes 2) -Initialize -Validation "timespan" -Handler { [PSFramework.Message.LogHost]::IntervalIdleDuration = $args[0] } -Description 'The time with no message written that needs to occur for the logging runspace to switch to idle mode.'
Set-PSFConfig -Module PSFramework -Name 'Logging.Provider.Source' -Value $null -Initialize -Validation 'uriabsolute' -Description 'Path where PSFramework looks for a provider index file. This file is used to load and configure additional logging providers. See "Get-Help Import-PSFLoggingProvider -Detailed" for more information'
Set-PSFConfig -Module PSFramework -Name 'Logging.Enabled' -Value $true -Initialize -Validation 'bool' -Handler {
    [PSFramework.Message.LogHost]::LoggingEnabled = $args[0]
    if ($args[0]) { Start-PSFRunspace -Name 'psframework.logging' -NoMessage }
    else { Stop-PSFRunspace -Name 'psframework.logging' }
} -Description 'Whether the PSFramework performs any logging at all. Disabling this will stop the background runspace that performs the logging.'


Set-PSFConfig -Module PSFramework -Name 'Message.Info.Minimum' -Value 1 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumInformation = $_ } -Description "The minimum required message level for messages that will be shown to the user."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Maximum' -Value 3 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumInformation = $_ } -Description "The maximum message level to still display to the user directly."
Set-PSFConfig -Module PSFramework -Name 'Message.Verbose.Minimum' -Value 4 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumVerbose = $_ } -Description "The minimum required message level where verbose information is written."
Set-PSFConfig -Module PSFramework -Name 'Message.Verbose.Maximum' -Value 6 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumVerbose = $_ } -Description "The maximum message level where verbose information is still written."
Set-PSFConfig -Module PSFramework -Name 'Message.Debug.Minimum' -Value 1 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumDebug = $_ } -Description "The minimum required message level where debug information is written."
Set-PSFConfig -Module PSFramework -Name 'Message.Debug.Maximum' -Value 9 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumDebug = $_ } -Description "The maximum message level where debug information is still written."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color' -Value 'Cyan' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColor = $_ } -Description "The color to use when writing text to the screen on PowerShell."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color.Emphasis' -Value 'green' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorEmphasis = $_ } -Description "The color to use when emphasizing written text to the screen on PowerShell."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color.Subtle' -Value 'gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorSubtle = $_ } -Description "The color to use when making writing text to the screen on PowerShell appear subtle."
Set-PSFConfig -Module PSFramework -Name 'Message.DeveloperColor' -Value 'Gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::DeveloperColor = $_ } -Description "The color to use when writing text with developer specific additional information to the screen on PowerShell."
Set-PSFConfig -Module PSFramework -Name 'Message.ConsoleOutput.Disable' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::DisableVerbosity = $_ } -Description "Global toggle that allows disabling all regular messages to screen. Messages from '-Verbose' and '-Debug' are unaffected"
Set-PSFConfig -Module PSFramework -Name 'Message.Transform.ErrorQueueSize' -Value 512 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.MessageHost]::TransformErrorQueueSize = $_ } -Description "The size of the queue for transformation errors. May be useful for advanced development, but can be ignored usually."
Set-PSFConfig -Module PSFramework -Name 'Message.NestedLevel.Decrement' -Value 0 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::NestedLevelDecrement = $_ } -Description "How many levels should be reduced per callstack depth. This makes commands less verbose, the more nested they are called"
Set-PSFConfig -Module PSFramework -Name 'Developer.Mode.Enable' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::DeveloperMode = $_ } -Description "Developermode enables advanced logging and verbosity features. There is little benefit for enabling this as a regular user. but developers can use it to more easily troubleshoot issues."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Breadcrumbs' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageBreadcrumbs = $_ } -Description "Controls how messages are displayed. Enables Breadcrumb display, showing the entire callstack. Takes precedence over command name display."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.FunctionName' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageDisplayCommand = $_ } -Description "Controls how messages are displayed. Enables command name, showing the name of the writing command. Is overwritten by enabling breadcrumbs."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageTimestamp = $_ } -Description "Controls how messages are displayed. Enables timestamp display, including a timestamp in each message."

Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessagePrefix = $_ } -Description "Controls how messages are displayed. Enables message prefix display, including a prefix in each message."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix.Error' -Value "##vso[task.logissue type=error;]" -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::PrefixValueError = $_ } -Description "Prefix value to use when the level is Warning and the tag 'error' is supplied."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix.Warning' -Value "##vso[task.logissue type=warning;]" -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::PrefixValueWarning = $_ } -Description "Prefix value to use when the level is Warning."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix.Verbose' -Value "##[debug]" -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::PrefixValueVerbose = $_ } -Description "Prefix value to use when the level is one of the three Verbose levels."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix.Host' -Value "" -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::PrefixValueHost = $_ } -Description "Prefix value to use when the level is Host."
Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix.Significant' -Value "##[section]" -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::PrefixValueSignificant = $_ } -Description "Prefix value to use when the level is significant or critical."


Set-PSFConfig -Module 'PSFramework' -Name 'Path.Temp' -Value $env:TEMP -Initialize -Validation 'string' -Description "Path pointing at the temp path. Used with Get-PSFPath."
Set-PSFConfig -Module 'PSFramework' -Name 'Path.LocalAppData' -Value $script:path_LocalAppData -Initialize -Validation 'string' -Description "Path pointing at the LocalAppData path. Used with Get-PSFPath."
Set-PSFConfig -Module 'PSFramework' -Name 'Path.AppData' -Value $script:path_AppData -Initialize -Validation 'string' -Description "Path pointing at the AppData path. Used with Get-PSFPath."
Set-PSFConfig -Module 'PSFramework' -Name 'Path.ProgramData' -Value $script:path_ProgramData -Initialize -Validation 'string' -Description "Path pointing at the ProgramData path. Used with Get-PSFPath."

#region Setting the configuration
Set-PSFConfig -Module PSFramework -Name 'Runspace.StopTimeoutSeconds' -Value 30 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Runspace.RunspaceHost]::StopTimeoutSeconds = $args[0] } -Description "Time in seconds that Stop-PSFRunspace will wait for a scriptspace to selfterminate before killing it."
#endregion Setting the configuration

# The path where type-files are stored when registered
Set-PSFConfig -Module PSFramework -Name 'Serialization.WorkingDirectory' -Value $script:path_typedata -Initialize -Validation "string" -Description "The folder in which registered type extension files are placed before import. Relevant for Register-PSFTypeSerializationData."

Set-PSFConfig -Module PSFramework -Name 'Utility.Size.Style' -Value ([PSFramework.Utility.SizeStyle]::Dynamic) -Initialize -Validation sizestyle -Handler { [PSFramework.Utility.UtilityHost]::SizeStyle = $args[0] } -Description "Controls how size objects are displayed by default. Generally, their string representation is calculated to be user friendly (dynamic), can be updated to 'plain' number or a specific size. Can be overriden on a per-object basis."
Set-PSFConfig -Module PSFramework -Name 'Utility.Size.Digits' -Value 2 -Initialize -Validation integer0to9 -Handler { [PSFramework.Utility.UtilityHost]::SizeDigits = $args[0] } -Description "How many digits are used when displaying a size object."

if (-not [PSFramework.Configuration.ConfigurationHost]::ImportFromRegistryDone)
{
    # Read config from all settings
    $config_hash = Read-PsfConfigPersisted -Scope 511
    
    foreach ($value in $config_hash.Values)
    {
        try
        {
            if (-not $value.KeepPersisted) { Set-PSFConfig -FullName $value.FullName -Value $value.Value -EnableException }
            else { Set-PSFConfig -FullName $value.FullName -PersistedValue $value.Value -PersistedType $value.Type -EnableException }
            [PSFramework.Configuration.ConfigurationHost]::Configurations[$value.FullName].PolicySet = $value.Policy
            [PSFramework.Configuration.ConfigurationHost]::Configurations[$value.FullName].PolicyEnforced = $value.Enforced
        }
        catch { }
    }
    
    [PSFramework.Configuration.ConfigurationHost]::ImportFromRegistryDone = $true
}



$FunctionDefinitions = {
    function Export-DataToAzure {
        <#
        .SYNOPSIS
            Function to send logging data to an Azure Workspace
 
        .DESCRIPTION
            This function is the main function that takes a PSFMessage object to log in an Azure workspace via Rest API call.
 
        .PARAMETER Message
            This is a PSFMessage object that will be converted to serialized to Json injected to an Azure workspace
 
        .EXAMPLE
            Export-DataToAzure -Message $objectToProcess
 
        .NOTES
            # Adapted from https://docs.microsoft.com/en-us/azure/log-analytics/log-analytics-data-collector-api
            Rest API documentation: https://docs.microsoft.com/en-us/rest/api/azure/
            Azure Monitor HTTP Data Collector API: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/data-collector-api#request-body
 
            Azure Monitor Data collection API Constrains
            --------------------------------------------
            1. Maximum of 30 MB per post to Azure Monitor Data Collector API. This is a size limit for a single post. If the data from a single post that exceeds 30 MB, you should split the data up to smaller sized chunks and send them concurrently.
            2. Maximum of 32 KB limit for field values. If the field value is greater than 32 KB, the data will be truncated.
            3. Recommended maximum number of fields for a given type is 50. This is a practical limit from a usability and search experience perspective.
            4. A table in a Log Analytics workspace only supports up to 500 columns (referred to as a field in this article).
            5. The maximum number of characters for the column name is 500.
 
            Notes on Azure workspace table
            ------------------------------
            The table in the Azure workspace will be the LogType specified in PSFConfig. The default is 'Message'
            When looking at the tables in the Azure workspace they will always have _CL appended to them. _CL stands for (for Custom Log)
            In the final table output in the Azure workspace each property imported to the table will have its own column
            and they will be specified by the property type that was inserted to the table.
            Each Azure workspace column name will be suffixed with the data type - _d for double, _b for boolean, _s for string, etc.
 
            How to register this provider
            -----------------------------
            Set-PSFLoggingProvider -Name AzureLogAnalytics -InstanceName YourInstanceName -WorkspaceId "AzureWorkspaceId" -SharedKey "AzureWorkspaceSharedKey" -LogType "Message" -enabled $True
        #>

        
        [cmdletbinding()]
        param (
            [parameter(Mandatory = $True)]
            $Message
        )
        
        begin {
            # Grab the default configuration values for the logging provider
            $WorkspaceID = Get-ConfigValue -Name 'WorkspaceId'
            $SharedKey = Get-ConfigValue -Name 'SharedKey'
            $LogType = Get-ConfigValue -Name 'LogType'
        }
        
        process {
            # Create a custom PSObject and convert it to a Json object using UTF8 encoding
            $loggingMessage = [PSCustomObject][ordered]@{
                Message = $Message.LogMessage
                Timestamp = $Message.TimeStamp.ToUniversalTime()
                Level   = $Message.Level.ToString()
                Tags    = $Message.Tags
                Data = $Message.Data
                ComputerName = $Message.ComputerName
                Runspace = $Message.Runspace
                UserName = $Message.UserName
                ModuleName = $Message.ModuleName
                FunctionName = $Message.FunctionName
                File    = $Message.File
                CallStack = $Message.CallStack
                TargetObject = $Message.TargetObject
                ErrorRecord = $Message.ErrorRecord
            }
            
            $bodyAsJson = ConvertTo-Json $loggingMessage
            $body = [System.Text.Encoding]::UTF8.GetBytes($bodyAsJson)
            
            $restMethod = "POST"
            $restContentType = "application/json"
            $restResource = "/api/logs"
            $date = [DateTime]::UtcNow.ToString("r")
            $contentLength = $body.Length
            
            $signatureArgs = @{
                WorkspaceID        = $WorkspaceID
                SharedKey        = $SharedKey
                DateAndTime        = $date
                ContentLength   = $contentLength
                RestMethod        = $restMethod
                RestContentType = $restContentType
                RestResource    = $restResource
            }
            
            # Generate a signature needed to gain access to the Azure workspace
            $signature = Get-LogSignature @signatureArgs
            
            # RestAPI headers
            $headers = @{
                "Authorization"           = $signature
                "Log-Type"               = $logType
                "x-ms-date"               = $date
                "time-generated-field" = "TimeStamp"
            }
            
            try {
                $uri = "https://$($WorkspaceID).ods.opinsights.azure.com$($restResource)?api-version=2016-04-01"
                $webResponse = Invoke-WebRequest -Uri $uri -Method $restMethod -ContentType $restContentType -Headers $headers -Body $body -UseBasicParsing
                switch ($webResponse.StatusCode) {
                    '400' {
                        switch ($webResponse.StatusDescription) {
                            'InactiveCustomer' { throw "Sucessful Post to Azure Workspace" }
                            'InvalidApiVersion' { throw "The API version that you specified was not recognized by the service." }
                            'InvalidCustomerId' { throw "The workspace ID specified is invalid." }
                            'InvalidDataFormat' { throw "Invalid JSON was submitted. The response body might contain more information about how to resolve the error." }
                            'InvalidLogType' { throw "The log type specified contained special characters or numerics." }
                            'MissingApiVersion' { throw "The API version wasn't specified." }
                            'MissingContentType' { throw "The content type wasn't specified." }
                            'MissingLogType' { throw "The required value log type wasn't specified." }
                            'UnsupportedContentType' { throw "The content type was not set to application/json." }
                        }
                    }
                    
                    '403' { throw "The service failed to authenticate the request. Verify that the workspace ID and connection key are valid." }
                    '404' { throw "Either the URL provided is incorrect, or the request is too large." }
                    '429' { throw "The service is experiencing a high volume of data from your account. Please retry the request later." }
                    '500' { throw "The service encountered an internal error. Please retry the request." }
                    '503' { throw "The service currently is unavailable to receive requests. Please retry your request." }
                }
            }
            catch { throw }
        }
    }
    
    function Get-LogSignature {
        <#
    .SYNOPSIS
        Function for computing a signature to connect to the Azure workspace
 
    .DESCRIPTION
        This function will compute a signature that will be used to connect to the Azure workspace in order to save logging data.
 
    .PARAMETER WorkspaceID
        WorkspaceID is the unique identifer for the Log Analytics workspace, and Signature is a Hash-based Message Authentication Code (HMAC) constructed from the request and computed by using the SHA256 algorithm, and then encoded using Base64 encoding.
 
    .PARAMETER SharedKey
        This is the Azure workspace shared key.
 
    .PARAMETER DateAndTime
        The name of a field in the data that contains the timestamp of the data item. If you specify a field then its contents are used for TimeGenerated. If this field isn't specified, the default for TimeGenerated is the time that the message is ingested. The contents of the message field should follow the ISO 8601 format YYYY-MM-DDThh:mm:ssZ.
 
    .PARAMETER ContentLength
        The content length of the object being injected to the Azure workspace table
 
    .PARAMETER RestMethod
        Rest Method being used in the connection.
 
    .PARAMETER RestContentType
        Rest content type being used in the connection.
 
    .PARAMETER RestResource
        The API resource name: /api/logs.
 
    .EXAMPLE
        Get-LogSignature @inParameters
 
    .NOTES
        Any request to the Log analytics HTTP Data Collector API must include the Authorization header.
        To authenticate a request, you must sign the request with either the primary or secondary key for the workspace that is making the request and pass that signature as part of the request.
    #>

        
        [cmdletbinding()]
        param (
            $WorkspaceID,
            
            $SharedKey,
            
            $DateAndTime,
            
            $ContentLength,
            
            $RestMethod,
            
            $RestContentType,
            
            $RestResource
        )
        
        process {
            $xHeaders = "x-ms-date:" + $DateAndTime
            $stringToHash = $RestMethod + "`n" + $ContentLength + "`n" + $RestContentType + "`n" + $xHeaders + "`n" + $RestResource
            $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
            $keyBytes = [Convert]::FromBase64String($sharedKey)
            $sha256 = New-Object System.Security.Cryptography.HMACSHA256
            $sha256.Key = $keyBytes
            $computedHash = $sha256.ComputeHash($bytesToHash)
            $encodedHash = [Convert]::ToBase64String($computedHash)
            $authorization = 'SharedKey {0}:{1}' -f $WorkspaceID, $encodedHash
            return $authorization
        }
    }
}

#region Events
$message_event = {
    param (
        $Message
    )
    
    Export-DataToAzure -Message $Message
}
#endregion Events

# Configuration values for the logging provider
$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.AzureLogAnalytics.WorkspaceId' -Value "" -Initialize -Validation 'string' -Description "WorkspaceId for the Azure Workspace we are logging our data objects to."
    Set-PSFConfig -Module PSFramework -Name 'Logging.AzureLogAnalytics.SharedKey' -Value "" -Initialize -Validation 'string' -Description "SharedId for the Azure Workspace we are logging our data objects to."
    Set-PSFConfig -Module PSFramework -Name 'Logging.AzureLogAnalytics.LogType' -Value "Message" -Initialize -Validation 'string' -Description "Log type we will log information to."
}

# Registered parameters for the logging provider.
# ConfigurationDefaultValues are used for all instances of the azure logging provider
$paramRegisterPSFAzureLogAnalyticsProvider = @{
    Name               = "AzureLogAnalytics"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.AzureLogAnalytics'
    InstanceProperties = 'WorkspaceId', 'SharedKey', 'LogType'
    MessageEvent       = $message_Event
    ConfigurationSettings = $configuration_Settings
    FunctionDefinitions = $functionDefinitions
    ConfigurationDefaultValues = @{
        LogType = 'Message'
    }
}

# Register the Azure logging provider
Register-PSFLoggingProvider @paramRegisterPSFAzureLogAnalyticsProvider

$message_event = {
    param (
        $Message
    )
    $style = Get-ConfigValue -Name Style
    $string = $style.Replace('%Time%',$Message.Timestamp.ToString('HH:mm:ss.fff')).Replace('%Date%',$Message.Timestamp.ToString('yyyy-MM-dd')).Replace('%Level%', $Message.Level).Replace('%Module%', $Message.ModuleName).Replace('%FunctionName%', $Message.FunctionName).Replace('%Line%', $Message.Line).Replace('%File%', $Message.File).Replace('%Tags%', ($Message.Tags -join ",")).Replace('%Message%', $Message.LogMessage)
    [System.Console]::WriteLine($string)
}

$configuration_Settings = {
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Console.Style' -Value '%Message%' -Initialize -Validation string -Description 'The style in which the message is printed. Supports several placeholders: %Message%, %Time%, %Date%, %Tags%, %Level%, %Module%, %FunctionName%, %Line%, %File%. Supports newline and tabs.'
}
$paramRegisterPSFLoggingProvider = @{
    Name               = "console"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.Console'
    InstanceProperties = 'Style'
    MessageEvent       = $message_Event
    ConfigurationSettings       = $configuration_Settings
    ConfigurationDefaultValues = @{
        Style = '%Message%'
    }
}

# Register the Console logging provider
Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

$functionDefinitions = {
    function Write-EventLogEntry
    {
        [CmdletBinding()]
        param (
            $Message
        )
        
        $level = 'Information'
        if ($Message.Level -eq 'Warning') { $level = 'Warning' }
        $errorTag = Get-ConfigValue -Name ErrorTag
        if ($Message.Tags -contains $errorTag) { $level = 'Error' }
        if ($Message.Level -eq 'Error') { $level = 'Error' }
        
        $eventID = switch ($level)
        {
            'Information' { Get-ConfigValue -Name InfoID }
            'Warning' { Get-ConfigValue -Name WarningID }
            'Error' { Get-ConfigValue -Name ErrorID }
        }
        
        $data = @(
            $Message.LogMessage
            $Message.Timestamp.ToUniversalTime().ToString((Get-ConfigValue -Name TimeFormat))
            $Message.FunctionName
            $Message.ModuleName
            ($Message.Tags -join ",")
            $Message.Level
            $Message.Runspace
            $Message.TargetObject
            $Message.File
            $Message.Line
            $Message.CallStack.ToString()
            $Message.Username
            $PID
            $script:loggingID
        )
        foreach ($key in $Message.Data.Keys)
        {
            $entry = 'Data| {0} : {1}' -f $key, $Message.Data[$key]
            # Max length of line: 31839 characters https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-reporteventa
            if ($entry.Length -gt 31839) { $entry = $entry.SubString(0, 31835) + '...' }
            $data += $entry
        }
        
        try { Write-LogEntry -LogName $script:logName -Source $script:source -Type $level -Category (Get-ConfigValue -Name Category) -EventId $eventID -Data $data }
        catch { throw }
    }
    
    function Write-LogEntry
    {
        [CmdletBinding()]
        param (
            [string]
            $LogName,
            
            [string]
            $Source,
            
            [int]
            $EventID,
            
            [int]
            $Category,
            
            [System.Diagnostics.EventLogEntryType]
            $Type,
            
            [object[]]
            $Data
        )
        $id = New-Object System.Diagnostics.EventInstance($EventID, $Category, $Type)
        $evtObject = New-Object System.Diagnostics.EventLog
        $evtObject.Log = $LogName
        $evtObject.Source = $Source
        $evtObject.WriteEvent($id, $Data)
    }
    
    function Start-EventLogging
    {
        [CmdletBinding()]
        param (
            
        )
        
        $logName = Get-ConfigValue -Name LogName
        $source = Get-ConfigValue -Name Source
        
        $script:loggingID = [System.Guid]::NewGuid()
        $startingMessage = "Starting new logging provider! | Process ID: $PID | Instance Name: $($script:Instance.Name) | Logging ID: $loggingID"
        $data = $startingMessage, $PID, $script:Instance.Name, $loggingID
        try
        {
            Write-LogEntry -LogName $logName -Source $source -Type Information -Category (Get-ConfigValue -Name Category) -EventId 999 -Data $data
            $script:logName = $logName
            $script:source = $source
            return
        }
        catch
        {
            try
            {
                [System.Diagnostics.EventLog]::CreateEventSource($source, $logName)
                Write-LogEntry -LogName $logName -Source $source -Type Information -Category (Get-ConfigValue -Name Category) -EventId 999 -Data $data
                $script:logName = $logName
                $script:source = $source
                return
            }
            catch
            {
                if (-not (Get-ConfigValue -Name UseFallback)) { throw }
                
                Write-LogEntry -LogName Application -Source Application -Type Information -Category (Get-ConfigValue -Name Category) -EventId 999 -Data $data
                $script:logName = 'Application'
                $script:source = 'Application'
            }
        }
    }
}

$begin_event = {
    Start-EventLogging
}

$message_event = {
    param (
        $Message
    )
    
    Write-EventLogEntry -Message $Message
}

$paramRegisterPSFLoggingProvider = @{
    Name               = "eventlog"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.EventLog'
    InstanceProperties = 'LogName', 'Source', 'UseFallback', 'Category', 'InfoID', 'WarningID', 'ErrorID', 'ErrorTag', 'TimeFormat'
    FunctionDefinitions = $functionDefinitions
    BeginEvent           = $begin_event
    MessageEvent       = $message_Event
    ConfigurationDefaultValues = @{
        LogName        = 'Application'
        Source        = 'PSFramework'
        UseFallback = $true
        Category    = 1000
        InfoID        = 1000
        WarningID   = 2000
        ErrorID        = 666
        ErrorTag    = 'error'
        TimeFormat  = 'yyyy-MM-dd HH:mm:ss.fff'
    }
}

Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

# Action that is performed on registration of the provider using Register-PSFLoggingProvider
$registrationEvent = {
    
}

#region Logging Execution
# Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script)
$begin_event = {
    #region Helper Functions
    function Clean-FileSystemErrorXml
    {
        [CmdletBinding()]
        Param (
            $Path
        )
        
        $totalLength = $Null
        $files = Get-ChildItem -Path $Path.FullName -Filter "$($env:ComputerName)_$($pid)_error_*.xml" | Sort-Object LastWriteTime
        $totalLength = $files | Measure-Object Length -Sum | Select-Object -ExpandProperty Sum
        if (([PSFramework.Message.LogHost]::MaxErrorFileBytes) -gt $totalLength) { return }
        
        $removed = 0
        foreach ($file in $files)
        {
            $removed += $file.Length
            Remove-Item -Path $file.FullName -Force -Confirm:$false
            
            if (($totalLength - $removed) -lt ([PSFramework.Message.LogHost]::MaxErrorFileBytes)) { break }
        }
    }
    
    function Clean-FileSystemMessageLog
    {
        [CmdletBinding()]
        Param (
            $Path
        )
        
        if ([PSFramework.Message.LogHost]::MaxMessagefileCount -eq 0) { return }
        
        $files = Get-ChildItem -Path $Path.FullName -Filter "$($env:ComputerName)_$($pid)_message_*.log" | Sort-Object LastWriteTime
        if (([PSFramework.Message.LogHost]::MaxMessagefileCount) -ge $files.Count) { return }
        
        $removed = 0
        foreach ($file in $files)
        {
            $removed++
            Remove-Item -Path $file.FullName -Force -Confirm:$false
            
            if (($files.Count - $removed) -le ([PSFramework.Message.LogHost]::MaxMessagefileCount)) { break }
        }
    }
    
    function Clean-FileSystemGlobalLog
    {
        [CmdletBinding()]
        Param (
            $Path
        )
        
        # Kill too old files
        Get-ChildItem -Path $Path.FullName | Where-Object Name -Match "^$([regex]::Escape($env:ComputerName))_.+" | Where-Object LastWriteTime -LT ((Get-Date) - ([PSFramework.Message.LogHost]::MaxLogFileAge)) | Remove-Item -Force -Confirm:$false
        
        # Handle the global overcrowding
        $files = Get-ChildItem -Path $Path.FullName | Where-Object Name -Match "^$([regex]::Escape($env:ComputerName))_.+" | Sort-Object LastWriteTime
        if (-not ($files)) { return }
        $totalLength = $files | Measure-Object Length -Sum | Select-Object -ExpandProperty Sum
        
        if (([PSFramework.Message.LogHost]::MaxTotalFolderSize) -gt $totalLength) { return }
        
        $removed = 0
        foreach ($file in $files)
        {
            $removed += $file.Length
            Remove-Item -Path $file.FullName -Force -Confirm:$false
            
            if (($totalLength - $removed) -lt ([PSFramework.Message.LogHost]::MaxTotalFolderSize)) { break }
        }
    }
    #endregion Helper Functions
    
    $filesystem_SelectTargetObject = @{
        Name = 'TargetObject'
        Expression = {
            if ($null -eq $_.TargetObject) { return }
            if ([PSFramework.Message.LogHost]::FileSystemSerializationDepth -lt 0) { return $_.TargetObject }
            if ([PSFramework.Message.LogHost]::FileSystemSerializationDepth -eq 0) { return ($_.TargetObject | ConvertTo-PSFClixml) }
            $_.TargetObject | ConvertTo-PSFClixml -Depth ([PSFramework.Message.LogHost]::FileSystemSerializationDepth)
        }
    }
    $filesystem_SelectTimestamp = @{
        Name = 'Timestamp'
        Expression = {
            $_.Timestamp.ToString([PSFramework.Message.LogHost]::TimeFormat)
        }
    }
}

# Action that is performed at the beginning of each logging cycle
$start_event = {
    $filesystem_path = [PSFramework.Message.LogHost]::LoggingPath
    if (-not (Test-Path $filesystem_path))
    {
        $filesystem_root = New-Item $filesystem_path -ItemType Directory -Force -ErrorAction Stop
    }
    else { $filesystem_root = Get-Item -Path $filesystem_path }
    
    try { [int]$filesystem_num_Error = (Get-ChildItem -Path $filesystem_path.FullName -Filter "$($env:ComputerName)_$($pid)_error_*.xml" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty Name | Select-String -Pattern "(\d+)" -AllMatches).Matches[1].Value }
    catch { }
    try { [int]$filesystem_num_Message = (Get-ChildItem -Path $filesystem_path.FullName -Filter "$($env:ComputerName)_$($pid)_message_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty Name | Select-String -Pattern "(\d+)" -AllMatches).Matches[1].Value }
    catch { }
    if (-not ($filesystem_num_Error)) { $filesystem_num_Error = 0 }
    if (-not ($filesystem_num_Message)) { $filesystem_num_Message = 0 }
}

# Action that is performed for each message item that is being logged
$message_Event = {
    Param (
        $Message
    )
    
    $filesystem_CurrentFile = Join-Path $filesystem_root.FullName "$($env:ComputerName)_$($pid)_message_$($filesystem_num_Message).log"
    if (Test-Path $filesystem_CurrentFile)
    {
        $filesystem_item = Get-Item $filesystem_CurrentFile
        if ($filesystem_item.Length -gt ([PSFramework.Message.LogHost]::MaxMessagefileBytes))
        {
            $filesystem_num_Message++
            $filesystem_CurrentFile = Join-Path $($filesystem_root.FullName) "$($env:ComputerName)_$($pid)_message_$($filesystem_num_Message).log"
        }
    }
    
    if ($Message)
    {
        if ([PSFramework.Message.LogHost]::FileSystemModernLog)
        {
            if (-not (Test-Path $filesystem_CurrentFile))
            {
                $Message | Select-PSFObject ComputerName, Username, $filesystem_SelectTimestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, $filesystem_SelectTargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ "} } | Export-Csv -Path $filesystem_CurrentFile -NoTypeInformation
            }
            else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Username, $filesystem_SelectTimestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, $filesystem_SelectTargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ " } }) -NoTypeInformation)[1] }
        }
        else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, $filesystem_SelectTargetObject, Runspace) -NoTypeInformation)[1] }
    }
}

# Action that is performed for each error item that is being logged
$error_Event = {
    Param (
        $ErrorItem
    )
    
    if ($ErrorItem)
    {
        $ErrorItem | Export-Clixml -Path (Join-Path $filesystem_root.FullName "$($env:ComputerName)_$($pid)_error_$($filesystem_num_Error).xml") -Depth 3
        $filesystem_num_Error++
    }
    
    Clean-FileSystemErrorXml -Path $filesystem_root
}

# Action that is performed at the end of each logging cycle
$end_event = {
    Clean-FileSystemMessageLog -Path $filesystem_root
    Clean-FileSystemGlobalLog -Path $filesystem_root
}

# Action that is performed when stopping the logging script
$final_event = {
    
}
#endregion Logging Execution

#region Function Extension / Integration
# Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider
$configurationParameters = {
    $configroot = "PSFramework.Logging.FileSystem"
    
    $configurations = Get-PSFConfig -FullName "$configroot.*"
    
    $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    
    foreach ($config in $configurations)
    {
        $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
        $ParamAttrib.ParameterSetName = '__AllParameterSets'
        $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttribColl.Add($ParamAttrib)
        $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl)
        
        $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam)
    }
    return $RuntimeParamDic
}

# Script that is executes when configuring the provider using Set-PSFLoggingProvider
$configurationScript = {
    $configroot = "PSFramework.Logging.FileSystem"
    
    $configurations = Get-PSFConfig -FullName "$configroot.*"
    
    foreach ($config in $configurations)
    {
        if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim("."))))
        {
            Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))]
        }
    }
}

# Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required
$isInstalledScript = {
    return $true
}

# Script that provides dynamic parameter for Install-PSFLoggingProvider
$installationParameters = {
    # None needed
}

# Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script
$installationScript = {
    # Nothing to be done - if you need to install your filesystem, you probably have other issues you need to deal with first ;)
}
#endregion Function Extension / Integration

# Configuration settings to initialize
$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxMessagefileBytes' -Value 5MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxMessagefileBytes = $args[0] } -Description "The maximum size of a given logfile. When reaching this limit, the file will be abandoned and a new log created. Set to 0 to not limit the size. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxMessagefileCount' -Value 5 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxMessagefileCount = $args[0] } -Description "The maximum number of logfiles maintained at a time. Exceeding this number will cause the oldest to be culled. Set to 0 to disable the limit. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxErrorFileBytes' -Value 20MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxErrorFileBytes = $args[0] } -Description "The maximum size all error files combined may have. When this number is exceeded, the oldest entry is culled. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxTotalFolderSize' -Value 100MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxTotalFolderSize = $args[0] } -Description "This is the upper limit of length all items in the log folder may have combined across all processes."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxLogFileAge' -Value (New-TimeSpan -Days 7) -Initialize -Validation "timespan" -Handler { [PSFramework.Message.LogHost]::MaxLogFileAge = $args[0] } -Description "Any logfile older than this will automatically be cleansed. This setting is global."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MessageLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogFileEnabled = $args[0] } -Description "Governs, whether a log file for the system messages is written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ErrorLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogFileEnabled = $args[0] } -Description "Governs, whether log files for errors are written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ModernLog' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::FileSystemModernLog = $args[0] } -Description "Enables the modern, more powereful version of the filesystem log, including headers and extra columns"
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.LogPath' -Value $script:path_Logging -Initialize -Validation "string" -Handler { [PSFramework.Message.LogHost]::LoggingPath = $args[0] } -Description "The path where the PSFramework writes all its logs and debugging information."
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.TimeFormat' -Value "$([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.ShortDatePattern) $([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.LongTimePattern)" -Initialize -Validation string -Handler { [PSFramework.Message.LogHost]::TimeFormat = $args[0] } -Description "The format used for timestamps in the logfile"
    Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.TargetSerializationDepth' -Value -1 -Initialize -Validation "integer" -Handler { [PSFramework.Message.LogHost]::FileSystemSerializationDepth = $args[0] } -Description "Whether the target object should be stored as a serialized object. 0 or less will see it logged as string, 1 or greater will see it logged as compressed CLIXML."
    
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.Enabled' -Value $true -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.InstallOptional' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.IncludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].IncludeModules = ($args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].ExcludeModules = ($args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].IncludeTags = ($args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged"
    Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].ExcludeTags = ($args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged"
}

$paramRegisterPSFLoggingProvider = @{
    Name                    = "filesystem"
    RegistrationEvent        = $registrationEvent
    BeginEvent                = $begin_event
    StartEvent                = $start_event
    MessageEvent            = $message_Event
    ErrorEvent                = $error_Event
    EndEvent                = $end_event
    FinalEvent                = $final_event
    ConfigurationParameters = $configurationParameters
    ConfigurationScript        = $configurationScript
    IsInstalledScript        = $isInstalledScript
    InstallationScript        = $installationScript
    InstallationParameters  = $installationParameters
    ConfigurationSettings   = $configuration_Settings
}

Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

#region Logging Execution
# Action that is performed at the beginning of each logging cycle
$start_event = {
    $script:paramSendPsgelfTcp = @{
        'GelfServer' = Get-ConfigValue -Name 'GelfServer'
        'Port'         = Get-ConfigValue -Name 'Port'
        'Encrypt'    = Get-ConfigValue -Name 'Encrypt'
    }
}

# Action that is performed for each message item that is being logged
$message_Event = {
    param (
        $Message
    )
    
    $gelf_params = $script:paramSendPsgelfTcp.Clone()
    $gelf_params['ShortMessage'] = $Message.LogMessage
    $gelf_params['HostName'] = $Message.ComputerName
    $gelf_params['DateTime'] = $Message.Timestamp
    
    $gelf_params['Level'] = switch ($Message.Level)
    {
        'Critical' { 1 }
        'Important' { 1 }
        'Output' { 3 }
        'Host' { 4 }
        'Significant' { 5 }
        'VeryVerbose' { 6 }
        'Verbose' { 6 }
        'SomewhatVerbose' { 6 }
        'System' { 6 }
        
        default { 7 }
    }
    
    if ($Message.ErrorRecord)
    {
        $gelf_params['FullMessage'] = $Message.ErrorRecord | ConvertTo-Json
    }
    
    # build the additional fields
    $gelf_properties = $Message.PSObject.Properties | Where-Object {
        $_.Name -notin @('Message', 'LogMessage', 'ComputerName', 'Timestamp', 'Level', 'ErrorRecord')
    }
    
    $gelf_params['AdditionalField'] = @{ }
    foreach ($gelf_property in $gelf_properties)
    {
        $gelf_params['AdditionalField'][$gelf_property.Name] = $gelf_property.Value
    }
    
    PSGELF\Send-PSGelfTCP @gelf_params
}
#endregion Logging Execution

#region Installation
$installationParameters = {
    $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute
    $parameterAttribute.ParameterSetName = '__AllParameterSets'
    $attributesCollection.Add($parameterAttribute)
    
    $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute('CurrentUser', 'AllUsers')
    $attributesCollection.Add($validateSetAttribute)
    
    $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("Scope", [string], $attributesCollection)
    $results.Add("Scope", $RuntimeParam)
    $results
}

$installation_script = {
    param (
        $BoundParameters
    )
    
    $paramInstallModule = @{
        Name = 'PSGELF'
    }
    if ($BoundParameters.Scope) { $paramInstallModule['Scope'] = $BoundParameters.Scope }
    elseif (-not (Test-PSFPowerShell -Elevated)) { $paramInstallModule['Scope'] = 'CurrentUser' }
    
    Install-Module @paramInstallModule
}

$isInstalled_script = {
    (Get-Module PSGELF -ListAvailable) -as [bool]
}
#endregion Installation

# Configuration settings to initialize
$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.GelfServer' -Value "" -Initialize -Validation string -Description "The GELF server to send logs to"
    Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.Port' -Value "" -Initialize -Validation string -Description "The port number the GELF server listens on"
    Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.Encrypt' -Value $true -Initialize -Validation bool -Description "Whether to use TLS encryption when communicating with the GELF server"
}

$paramRegisterPSFLoggingProvider = @{
    Name               = "gelf"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.GELF'
    InstanceProperties = 'GelfServer', 'Port', 'Encrypt'
    StartEvent           = $start_event
    MessageEvent       = $message_Event
    IsInstalledScript  = $isInstalled_script
    InstallationScript = $installation_script
    InstallationParameters = $installationParameters
    ConfigurationSettings = $configuration_Settings
    ConfigurationDefaultValues = @{
        Encrypt = $true
    }
}

Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

$functionDefinitions = {
    function Get-LogFilePath {
        [CmdletBinding()]
        param (
            
        )
        
        $path = Get-ConfigValue -Name 'FilePath'
        $logname = Get-ConfigValue -Name 'LogName'
        
        $scriptBlock = {
            param (
                [string]
                $Match
            )
            
            $hash = @{
                '%date%' = (Get-Date -Format 'yyyy-MM-dd')
                '%dayofweek%' = (Get-Date).DayOfWeek
                '%day%'  = (Get-Date).Day
                '%hour%' = (Get-Date).Hour
                '%minute%' = (Get-Date).Minute
                '%username%' = $env:USERNAME
                '%userdomain%' = $env:USERDOMAIN
                '%computername%' = $env:COMPUTERNAME
                '%processid%' = $PID
                '%logname%' = $logname
            }
            
            $hash.$Match
        }
        
        [regex]::Replace($path, '%day%|%computername%|%hour%|%processid%|%date%|%username%|%dayofweek%|%minute%|%userdomain%|%logname%', $scriptBlock, 'IgnoreCase')
    }
    
    function Write-LogFileMessage {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline = $true)]
            $Message,
            
            [bool]
            $IncludeHeader,
            
            [string]
            $FileType,
            
            [string]
            $Path,
            
            [string]
            $CsvDelimiter,
            
            $MessageItem
        )
        
        $parent = Split-Path $Path
        if (-not (Test-Path $parent)) {
            $null = New-Item $parent -ItemType Directory -Force
        }
        $fileExists = Test-Path $Path
        
        #region Type-Based Output
        switch ($FileType) {
            #region Csv
            "Csv"
            {
                if ((-not $fileExists) -and $IncludeHeader) { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Set-Content -Path $Path -Encoding $script:encoding }
                else { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Select-Object -Skip 1 | Add-Content -Path $Path -Encoding $script:encoding }
            }
            #endregion Csv
            #region Json
            "Json"
            {
                if ($fileExists -and -not $script:JsonSettings.JsonNoComma) { Add-Content -Path $Path -Value "," -Encoding $script:encoding }
                if (-not $script:JsonSettings) { $Message | ConvertTo-Json -Compress:$script:JsonSettings.JsonCompress | Add-Content -Path $Path -NoNewline:$(-not $script:JsonSettings.JsonNoComma) -Encoding $script:encoding }
                else { $Message | ConvertFrom-Enumeration | ConvertTo-Json -Compress:$script:JsonSettings.JsonCompress | Add-Content -Path $Path -NoNewline:$(-not $script:JsonSettings.JsonNoComma) -Encoding $script:encoding }
            }
            #endregion Json
            #region XML
            "XML"
            {
                [xml]$xml = $message | ConvertTo-Xml -NoTypeInformation
                $xml.Objects.InnerXml | Add-Content -Path $Path -Encoding $script:encoding
            }
            #endregion XML
            #region Html
            "Html"
            {
                [xml]$xml = $message | ConvertTo-Html -Fragment
                
                if ((-not $fileExists) -and $IncludeHeader) {
                    $xml.table.tr[0].OuterXml | Add-Content -Path $Path -Encoding $script:encoding
                }
                
                $xml.table.tr[1].OuterXml | Add-Content -Path $Path -Encoding $script:encoding
            }
            #endregion Html
            #region CMTrace
            "CMTrace"
            {
                $cType = 1
                if ($MessageItem.Level -eq 'Warning') { $cType = 2 }
                if ($MessageItem.ErrorRecord) { $cType = 3 }
                $fileEntry = '<no file>'
                if ($MessageItem.File) { $fileEntry = Split-Path -Path $MessageItem.File -Leaf }
                
                $format = '<![LOG[{0}]LOG]!><time="{1:HH:mm:ss.fff}+000" date="{1:MM-dd-yyyy}" component="{6}:{2} > {7}" context="{3}" type="{4}" thread="{5}" file="{6}:{2} > {7}">'
                $line = $format -f $MessageItem.LogMessage, $MessageItem.Timestamp, $MessageItem.Line, $MessageItem.TargetObject, $cType, $MessageItem.Runspace, $fileEntry, $MessageItem.FunctionName
                $line | Add-Content -Path $Path -Encoding $script:encoding
            }
            #endregion CMTrace
        }
        #endregion Type-Based Output
    }
    
    function Invoke-LogRotate {
        [CmdletBinding()]
        param (
            
        )
        
        $basePath = Get-ConfigValue -Name 'LogRotatePath'
        if (-not $basePath) { return }
        
        #region Resolve Paths
        $scriptBlock = {
            param (
                [string]
                $Match
            )
            
            $hash = @{
                '%date%' = (Get-Date -Format 'yyyy-MM-dd')
                '%dayofweek%' = (Get-Date).DayOfWeek
                '%day%'  = (Get-Date).Day
                '%hour%' = (Get-Date).Hour
                '%minute%' = (Get-Date).Minute
                '%username%' = $env:USERNAME
                '%userdomain%' = $env:USERDOMAIN
                '%computername%' = $env:COMPUTERNAME
                '%processid%' = $PID
                '%logname%' = $logname
            }
            
            $hash.$Match
        }
        
        $basePath = [regex]::Replace($basePath, '%day%|%computername%|%hour%|%processid%|%date%|%username%|%dayofweek%|%minute%|%userdomain%|%logname%', $scriptBlock, 'IgnoreCase')
        #endregion Resolve Paths
        
        $minimumRetention = (Get-ConfigValue -Name 'LogRetentionTime') -as [PSFTimeSpan] -as [Timespan]
        if (-not $minimumRetention) { throw "No minimum retention defined" }
        if ($minimumRetention.TotalSeconds -le 0) { throw "Minimum retention must be positive! Retention: $minimumRetention" }
        
        # Don't logrotate more than every 5 minutes
        if ($script:lastRotate -gt (Get-Date).AddMinutes(-5)) { return }
        $script:lastRotate = Get-Date
        
        $limit = (Get-Date).Subtract($minimumRetention)
        Get-ChildItem -Path $basePath -Filter (Get-ConfigValue -Name 'LogRotateFilter') -Recurse:(Get-ConfigValue -Name 'LogRotateRecurse') -File | Where-Object LastWriteTime -LT $limit | Remove-Item -Force -ErrorAction Stop
    }
    
    function Update-Mutex {
        [CmdletBinding()]
        param ()
        
        $script:mutexName = Get-ConfigValue -Name 'MutexName'
        if ($script:mutexName -and -not $script:mutex) {
            $script:mutex = New-Object System.Threading.Mutex($false, $script:mutexName)
            Add-Member -InputObject $script:mutex -MemberType NoteProperty -Name Name -Value $script:mutexName
        }
        elseif ($script:mutexName -and $script:mutex.Name -ne $script:mutexName) {
            $script:mutex.Dispose()
            $script:mutex = New-Object System.Threading.Mutex($false, $script:mutexName)
            Add-Member -InputObject $script:mutex -MemberType NoteProperty -Name Name -Value $script:mutexName
        }
        elseif (-not $script:mutexName -and $script:mutex) {
            $script:mutex.Dispose()
            $script:mutex = $null
        }
    }
    
    function ConvertFrom-Enumeration {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline = $true)]
            $InputObject
        )
        
        process {
            $data = @{ }
            foreach ($property in $InputObject.PSObject.Properties) {
                if ($property.Value -is [enum]) {
                    $data[$property.Name] = $property.Value -as [string]
                }
                else {
                    $data[$property.Name] = $property.Value
                }
            }
            [pscustomobject]$data
        }
    }
    
    function Move-LogFile {
        [CmdletBinding()]
        param (
            
        )
        
        $destinationPath = Get-ConfigValue -Name 'MoveOnFinal'
        if (-not $destinationPath) { return }
        
        if (-not (Test-Path $destinationPath)) { throw "Final log destination not found: $destinationPath" }
        $folder = Get-Item -Path $destinationPath
        if (-not $folder.PSIsContainer) { throw "Final log destination is not a folder: $destinationPath" }
        
        foreach ($filePath in $script:logPathList.Keys) {
            if (-not (Test-Path -Path $filePath)) { continue }
            
            Move-Item -Path $filePath -Destination $folder.FullName -Force -ErrorAction Stop
        }
    }
    
    function Copy-LogFile {
        [CmdletBinding()]
        param (
            
        )
        
        $destinationPath = Get-ConfigValue -Name 'CopyOnFinal'
        if (-not $destinationPath) { return }
        
        if (-not (Test-Path $destinationPath)) { throw "Final log destination not found: $destinationPath" }
        $folder = Get-Item -Path $destinationPath
        if (-not $folder.PSIsContainer) { throw "Final log destination is not a folder: $destinationPath" }
        
        foreach ($filePath in $script:logPathList.Keys) {
            if (-not (Test-Path -Path $filePath)) { continue }
            
            Copy-Item -Path $filePath -Destination $folder.FullName -Force -ErrorAction Stop
        }
    }
}

#region Events
$begin_event = {
    $script:lastRotate = (Get-Date).AddMinutes(-10)
    $script:logPathList = @{ }
}

$start_event = {
    $script:logfile_headers = Get-ConfigValue -Name 'Headers' | ForEach-Object {
        switch ($_) {
            'Tags'
            {
                @{
                    Name       = 'Tags'
                    Expression = { $_.Tags -join "," }
                }
            }
            'Message'
            {
                @{
                    Name       = 'Message'
                    Expression = { $_.LogMessage }
                }
            }
            'Timestamp'
            {
                @{
                    Name       = 'Timestamp'
                    Expression = {
                        if (Get-ConfigValue -Name 'UTC') {
                            if (-not (Get-ConfigValue -Name 'TimeFormat')) { $_.Timestamp.ToUniversalTime() }
                            else { $_.Timestamp.ToUniversalTime().ToString((Get-ConfigValue -Name 'TimeFormat')) }
                        }
                        else {
                            if (-not (Get-ConfigValue -Name 'TimeFormat')) { $_.Timestamp }
                            else { $_.Timestamp.ToString((Get-ConfigValue -Name 'TimeFormat')) }
                        }
                    }
                }
            }
            default { $_ }
        }
    }
    
    $script:logfile_paramWriteLogFileMessage = @{
        IncludeHeader = Get-ConfigValue -Name 'IncludeHeader'
        FileType      = Get-ConfigValue -Name 'FileType'
        CsvDelimiter  = Get-ConfigValue -Name 'CsvDelimiter'
        Path          = Get-LogFilePath
    }
    # Cache path for final move action
    $script:logPathList[$script:logfile_paramWriteLogFileMessage.Path] = $script:logfile_paramWriteLogFileMessage.Path
    
    $script:encoding = Get-ConfigValue -Name 'Encoding'
    
    $script:JsonSettings = @{
        JsonCompress = Get-ConfigValue -Name JsonCompress
        JsonString   = Get-ConfigValue -Name JsonString
        JsonNoComma  = Get-ConfigValue -Name JsonNoComma
    }
    Update-Mutex
}

$message_event = {
    param (
        $Message
    )
    
    if ($script:mutex) { $null = $script:mutex.WaitOne() }
    try { $Message | Select-Object $script:logfile_headers | Write-LogFileMessage @script:logfile_paramWriteLogFileMessage -MessageItem $Message }
    finally { if ($script:mutex) { $script:mutex.ReleaseMutex() } }
}

$end_event = {
    Invoke-LogRotate
}

$final_event = {
    Move-LogFile
    Copy-LogFile
}
#endregion Events

$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.FilePath' -Value "" -Initialize -Validation string -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.Logname' -Value "" -Initialize -Validation string -Description "A special string you can use as a placeholder in the logfile path (by using '%logname%' as placeholder)"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.IncludeHeader' -Value $true -Initialize -Validation bool -Description "Whether a written csv file will include headers"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.Headers' -Value @('ComputerName', 'File', 'FunctionName', 'Level', 'Line', 'Message', 'ModuleName', 'Runspace', 'Tags', 'TargetObject', 'Timestamp', 'Type', 'Username') -Initialize -Validation stringarray -Description "The properties to export, in the order to select them."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.FileType' -Value "CSV" -Initialize -Validation psframework.logfilefiletype -Description "In what format to write the logfile. Supported styles: CSV, XML, Html, Json or CMTrace. Html, XML and Json will be written as fragments."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.CsvDelimiter' -Value "," -Initialize -Validation string -Description "The delimiter to use when writing to csv."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.TimeFormat' -Value "$([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.ShortDatePattern) $([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.LongTimePattern)" -Initialize -Validation string -Description "The format used for timestamps in the logfile"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.Encoding' -Value "UTF8" -Initialize -Validation string -Description "In what encoding to write the logfile."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.UTC' -Value $false -Initialize -Validation bool -Description "Whether the timestamp in the logfile should be converted to UTC"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.LogRotatePath' -Value "" -Initialize -Validation string -Description "The path where to logrotate. Specifying this setting will cause the logging provider to also rotate older logfiles"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.LogRetentionTime' -Value "30d" -Initialize -Validation timespan -Description "The minimum age for a logfile to be considered for deletion as part of logrotation"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.LogRotateFilter' -Value "*" -Initialize -Validation string -Description "A filter to apply to all files logrotated"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.LogRotateRecurse' -Value $false -Initialize -Validation bool -Description "Whether the logrotate aspect should recursively look for files to logrotate"
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.MutexName' -Value '' -Initialize -Validation string -Description "Name of a mutex to use. Use this to handle parallel logging into the same file from multiple processes, by picking the same name in each process."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.JsonCompress' -Value $false -Initialize -Validation bool -Description "Will compress the json entries, condensing each entry into a single line."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.JsonString' -Value $false -Initialize -Validation bool -Description "Will convert all enumerated properties to string values when converting to json. This causes the level property to be 'Debug','Host', ... rather than 8,2,..."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.JsonNoComma' -Value $false -Initialize -Validation bool -Description "Prevent adding commas between two json entries."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.MoveOnFinal' -Value '' -Initialize -Validation string -Description "Path to a target folder to move logfiles to when shutting down the logging provider instance. This happens automatically when PSFramework ends or the provider instance is disabled again."
    Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.CopyOnFinal' -Value '' -Initialize -Validation string -Description "Path to a target folder to copy logfiles to when shutting down the logging provider instance. This happens automatically when PSFramework ends or the provider instance is disabled again."
}

$paramRegisterPSFLoggingProvider = @{
    Name               = "logfile"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.LogFile'
    InstanceProperties = 'CsvDelimiter', 'FilePath', 'FileType', 'Headers', 'IncludeHeader', 'Logname', 'TimeFormat', 'Encoding', 'UTC', 'LogRotatePath', 'LogRetentionTime', 'LogRotateFilter', 'LogRotateRecurse', 'MutexName', 'JsonCompress', 'JsonString', 'JsonNoComma', 'MoveOnFinal', 'CopyOnFinal'
    FunctionDefinitions = $functionDefinitions
    BeginEvent           = $begin_event
    StartEvent           = $start_event
    MessageEvent       = $message_event
    EndEvent           = $end_event
    FinalEvent           = $final_event
    ConfigurationDefaultValues = @{
        IncludeHeader = $true
        Headers          = 'ComputerName', 'File', 'FunctionName', 'Level', 'Line', 'Message', 'ModuleName', 'Runspace', 'Tags', 'TargetObject', 'Timestamp', 'Type', 'Username'
        FileType      = 'CSV'
        CsvDelimiter  = ','
        TimeFormat    = "$([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.ShortDatePattern) $([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat.LongTimePattern)"
        Encoding      = 'UTF8'
        LogRetentionTime = '30d'
        LogRotateFilter = '*'
        LogRotateRecurse = $false
    }
    ConfigurationSettings = $configuration_Settings
}

Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

$functionDefinitions = {
    function Send-SplunkData
    {
<#
    .SYNOPSIS
        Writes data to a splunk http event collector.
     
    .DESCRIPTION
        Writes data to a splunk http event collector.
        See this blog post for setting up the Splunk server:
        https://ntsystems.it/post/sending-events-to-splunks-http-event-collector-with-powershell
     
    .PARAMETER InputObject
        The object to send as message.
     
    .PARAMETER HostName
        The name of the computer from which the message was generated.
     
    .PARAMETER Timestamp
        The timestamp fron when the message was generated.
     
    .PARAMETER Uri
        Link to the http collector endpoint to which to write to.
        Example: https://localhost:8088/services/collector
     
    .PARAMETER Token
        The token associated with the http event collector.
#>

        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
            $InputObject,
            
            [Parameter(Mandatory = $true)]
            [string]
            $HostName,
            
            [Parameter(Mandatory = $true)]
            [System.DateTime]
            $Timestamp,
            
            [Parameter(Mandatory = $true)]
            [string]
            $Uri,
            
            [Parameter(Mandatory = $true)]
            [string]
            $Token
        )
        process
        {
            # Splunk events can have a 'time' property in epoch time. If it's not set, use current system time.
            $unixEpochStart = New-Object -TypeName DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, ([DateTimeKind]::Utc)
            $unixEpochTime = [int]($Timestamp.ToUniversalTime() - $unixEpochStart).TotalSeconds
            
            # Create json object to send
            $eventData = @{
                event = $InputObject
                host  = $HostName
                time = $unixEpochTime
            }
            if ($index = Get-ConfigValue -Name Index) { $eventData.index = $index }
            if ($source = Get-ConfigValue -Name Source) { $eventData.source = $source }
            if ($sourcetype = Get-ConfigValue -Name SourceType) { $eventData.sourcetype = $sourcetype }
            
            $body = ConvertTo-Json -InputObject $eventData -Compress
            
            # Only return if something went wrong, i.e. http response is not "success"
            try { $null = Invoke-RestMethodCustom -Uri $uri -Method Post -Headers @{ Authorization = "Splunk $Token" } -Body $body -ErrorAction Stop -IgnoreCert:$(Get-ConfigValue -Name IgnoreCert) }
            catch { throw }
        }
    }
    
    function Invoke-RestMethodCustom
    {
        [CmdletBinding()]
        param (
            [string]
            $Uri,
            
            [System.Collections.Hashtable]
            $Headers,
            
            [string]
            $Method,
            
            [string]
            $ContentType = 'application/json',
            
            [string]
            $Body,
            
            [switch]
            $IgnoreCert
        )
        
        process
        {
            $request = [System.Net.WebRequest]::Create($Uri)
            foreach ($key in $Headers.Keys) { $request.Headers[$key] = $Headers[$key] }
            $request.Method = $Method
            if ($IgnoreCert) { $request.ServerCertificateValidationCallback = { $true } }
            $request.ContentLength = $Body.Length
            
            $requestWriter = New-Object System.IO.StreamWriter($request.GetRequestStream(), [System.Text.Encoding]::ASCII)
            $requestWriter.Write($Body)
            $requestWriter.Close()
            
            try
            {
                $responseStream = $request.GetResponse().GetResponseStream()
                $reader = New-Object System.IO.StreamReader($responseStream)
                $reader.ReadToEnd()
                $reader.Close()
            }
            catch { throw }
        }
    }
    
    function Write-SplunkMessage
    {
        [CmdletBinding()]
        param (
            $Message
        )
        
        $splunkUrl = Get-ConfigValue -Name 'Url'
        $splunkToken = Get-ConfigValue -Name 'Token'
        $properties = Get-ConfigValue -Name 'Properties'
        $name = Get-ConfigValue -Name 'LogName'
        
        $selectProps = switch ($properties)
        {
            'Message' { 'LogMessage as Message' }
            'Timestamp' { 'Timestamp.ToUniversalTime().ToString("yyyy-MM-dd_HH:mm:ss.fff") as Timestamp' }
            'Level' { 'Level to String' }
            'Type' { 'Type to String' }
            'CallStack' { 'CallStack to String' }
            'ErrorRecord' { 'ErrorRecord to String' }
            default { $_ }
        }
        $selectProps = @($selectProps) + @(@{ Name = 'LogName'; Expression = { $name } })
        
        $Message | Select-PSFObject $selectProps | Send-SplunkData -HostName $Message.ComputerName -Timestamp $Message.Timestamp -Uri $splunkUrl -Token $splunkToken
    }
}

$message_event = {
    param (
        $Message
    )
    Write-SplunkMessage -Message $Message
}

$configuration_Settings = {
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.Url' -Description 'The url to the Splunk http event collector. Example: https://localhost:8088/services/collector'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.Token' -Description 'The token used to authenticate to the Splunk event collector.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.Properties' -Initialize -Value 'Timestamp', 'Message', 'Level', 'Tags', 'FunctionName', 'ModuleName', 'Runspace', 'Username', 'ComputerName', 'TargetObject', 'Data' -Description 'The properties to write to Splunk.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.LogName' -Initialize -Value 'Undefined' -Validation string -Description 'Name associated with the task. Included in each entry, making it easier to reuse the same http event collector for multiple tasks.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.IgnoreCert' -Initialize -Value $false -Validation bool -Description 'Whether the server certificate should be validated or not.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.Index' -Initialize -Value '' -Validation string -Description 'The index to apply to all messages. Uses the splunk-defined default index if omitted.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.Source' -Initialize -Value '' -Validation string -Description 'Event source to add to all messages.'
    Set-PSFConfig -Module 'PSFramework' -Name 'Logging.Splunk.SourceType' -Initialize -Value '' -Validation string -Description 'Event source type to add to all messages.'
}

$paramRegisterPSFLoggingProvider = @{
    Name               = "splunk"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.Splunk'
    InstanceProperties = 'Url', 'Token', 'Properties', 'LogName', 'IgnoreCert', 'Index', 'Source', 'SourceType'
    MessageEvent       = $message_Event
    ConfigurationSettings = $configuration_Settings
    FunctionDefinitions = $functionDefinitions
    ConfigurationDefaultValues = @{
        Properties = 'Timestamp', 'Message', 'Level', 'Tags', 'FunctionName', 'ModuleName', 'Runspace', 'Username', 'ComputerName', 'TargetObject', 'Data'
        LogName    = 'Undefined'
        IgnoreCert = $false
    }
}

# Register the Azure logging provider
Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider

$FunctionDefinitions = {

    Function Export-DataToSql {
        <#
        .SYNOPSIS
            Function to send logging data to a Sql database
 
        .DESCRIPTION
            This function is the main function that takes a PSFMessage object to log in a Sql database.
 
        .PARAMETER ObjectToProcess
            This is a PSFMessage object that will be converted and serialized then injected to a Sql database.
 
        .EXAMPLE
            Export-DataToAzure $objectToProcess
 
        .NOTES
            How to register this provider
            -----------------------------
            Set-PSFLoggingProvider -Name sqllog -InstanceName sqlloginstance -Enabled $true
        #>


        [cmdletbinding()]
        param(
            [parameter(Mandatory = $True)]
            $ObjectToProcess
        )

        begin {
            $SqlServer = Get-ConfigValue -Name 'SqlServer'
            $SqlTable = Get-ConfigValue -Name 'Table'
            $SqlDatabaseName = Get-ConfigValue -Name 'Database'
        }

        process {
            $QueryParameters = @{
                "Message"      = $ObjectToProcess.LogMessage
                "Level"        = $ObjectToProcess.Level -as [string]
                "TimeStamp"    = $ObjectToProcess.TimeStamp.ToUniversalTime()
                "FunctionName" = $ObjectToProcess.FunctionName
                "ModuleName"   = $ObjectToProcess.ModuleName
                "Tags"         = $ObjectToProcess.Tags -join "," -as [string]
                "Runspace"     = $ObjectToProcess.Runspace -as [string]
                "ComputerName" = $ObjectToProcess.ComputerName
                "TargetObject" = $ObjectToProcess.TargetObject -as [string]
                "File"         = $ObjectToProcess.File
                "Line"         = $ObjectToProcess.Line
                "ErrorRecord"  = $ObjectToProcess.ErrorRecord -as [string]
                "CallStack"    = $ObjectToProcess.CallStack -as [string]
            }

            try {
                $SqlInstance = Connect-DbaInstance -SqlInstance $SqlServer
                if ($SqlInstance.ConnectionContext.IsOpen -ne 'True') {
                    $SqlInstance.ConnectionContext.Connect() # Try to connect to the database
                }

                $insertQuery = "INSERT INTO [$SqlDatabaseName].[dbo].[$SqlTable](Message, Level, TimeStamp, FunctionName, ModuleName, Tags, Runspace, ComputerName, TargetObject, [File], Line, ErrorRecord, CallStack)
                VALUES (@Message, @Level, @TimeStamp, @FunctionName, @ModuleName, @Tags, @Runspace, @ComputerName, @TargetObject, @File, @Line, @ErrorRecord, @CallStack)"

                Invoke-DbaQuery -SqlInstance $SqlInstance -Database $SqlDatabaseName -Query $insertQuery -SqlParameters $QueryParameters -EnableException
            }
            catch { throw }
        }
    }

    function New-DefaultSqlDatabaseAndTable {
        <#
        .SYNOPSIS
                This function will create a default sql database object
 
        .DESCRIPTION
                This function will create the default sql default logging database
 
        .EXAMPLE
            None
        #>


        [cmdletbinding()]
        param(
        )

        # need to use dba tools to create the database and credentials for connecting.


        begin {

            # set instance and database name variables
            $Credential = Get-ConfigValue -Name 'Credential'
            $SqlServer = Get-ConfigValue -Name 'SqlServer'
            $SqlTable = Get-ConfigValue -Name 'Table'
            $SqlDatabaseName = Get-ConfigValue -Name 'Database'

            $parameters = @{
                SqlInstance = $SqlServer
            }
            if ($Credential) { $parameters.SqlCredential = $Credential }
        }
        process {
            try {
                $dbaconnection = Connect-DbaInstance @parameters
                if (-NOT (Get-DbaDatabase -SqlInstance $dbaconnection | Where-Object Name -eq $SqlDatabaseName)) {
                    $database = New-DbaDatabase -SqlInstance $dbaconnection -Name $SqlDatabaseName
                }
                if (-NOT($database.Tables | Where-Object Name -eq $SqlTable)) {
                    $createtable = "CREATE TABLE $SqlTable (Message VARCHAR(max), Level VARCHAR(max), TimeStamp [DATETIME], FunctionName VARCHAR(max), ModuleName VARCHAR(max), Tags VARCHAR(max), Runspace VARCHAR(36), ComputerName VARCHAR(max), TargetObject VARCHAR(max), [File] VARCHAR(max), Line BIGINT, ErrorRecord VARCHAR(max), CallStack VARCHAR(max))"
                    Invoke-dbaquery -SQLInstance $SqlServer -Database $SqlDatabaseName -query $createtable
                }
            }
            catch {
                throw
            }
        }
    }
}

#region Installation
$installationParameters = {
    $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute
    $parameterAttribute.ParameterSetName = '__AllParameterSets'
    $attributesCollection.Add($parameterAttribute)

    $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute('CurrentUser', 'AllUsers')
    $attributesCollection.Add($validateSetAttribute)

    $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("Scope", [string], $attributesCollection)
    $results.Add("Scope", $RuntimeParam)
    $results
}

$installation_script = {
    param (
        $BoundParameters
    )

    $paramInstallModule = @{
        Name = 'dbatools'
    }
    if ($BoundParameters.Scope) { $paramInstallModule['Scope'] = $BoundParameters.Scope }
    elseif (-not (Test-PSFPowerShell -Elevated)) { $paramInstallModule['Scope'] = 'CurrentUser' }

    Install-Module @paramInstallModule
}

$isInstalled_script = {
    (Get-Module dbatools -ListAvailable) -as [bool]
}
#endregion Installation
#region Events
$begin_event = {
    New-DefaultSqlDatabaseAndTable
}

$message_event = {
    param (
        $Message
    )

    Export-DataToSql -ObjectToProcess $Message
}

# Action that is performed when stopping the logging script.
$final_event = {

}
#endregion Events

# Configuration values for the logging provider
$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Credential' -Initialize -Validation 'credential' -Description "Credentials used for connecting to the SQL server."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Database' -Value "LoggingDatabase" -Initialize -Validation 'string' -Description "SQL server database."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Table' -Value "LoggingTable" -Initialize -Validation 'string' -Description "SQL server database table."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.SqlServer' -Value "" -Initialize -Description "SQL server hosting logs."
}

# Registered parameters for the logging provider.
# ConfigurationDefaultValues are used for all instances of the sql log provider
$paramRegisterPSFSqlProvider = @{
    Name                       = "Sql"
    Version2                   = $true
    ConfigurationRoot          = 'PSFramework.Logging.Sql'
    InstanceProperties         = 'Database', 'Table', 'SqlServer', 'Credential'
    MessageEvent               = $message_Event
    BeginEvent                 = $begin_event
    FinalEvent                 = $final_event
    IsInstalledScript          = $isInstalled_script
    InstallationScript         = $installation_script
    ConfigurationSettings      = $configuration_Settings
    InstallationParameters     = $installationParameters
    FunctionDefinitions        = $functionDefinitions
    ConfigurationDefaultValues = @{
        'Database'  = "LoggingDatabase"
        'Table'     = "LoggingTable"
    }
}

# Register the Azure logging provider
Register-PSFLoggingProvider @paramRegisterPSFSqlProvider

[PSFramework.Logging.ProviderHost]::ProviderV2ModuleScript = {
    param (
        $LoggingProviderInstance
    )
    
    try
    {
        $module = New-Module -Name ([guid]::NewGuid()) -ArgumentList $LoggingProviderInstance -ScriptBlock {
            param (
                $LoggingProviderInstance
            )
            
            $Instance = [pscustomobject]@{
                Name = $LoggingProviderInstance.Name
                Provider = $LoggingProviderInstance.Provider.Name
                ConfigurationRoot = $LoggingProviderInstance.Provider.ConfigurationRoot
            }
            
            # Validate Language Mode for security reasons
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.BeginEvent -Mode 'ConstrainedLanguage') { throw "The event BeginEvent is in constrained language mode and cannot be loaded!" }
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.StartEvent -Mode 'ConstrainedLanguage') { throw "The event StartEvent is in constrained language mode and cannot be loaded!" }
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.MessageEvent -Mode 'ConstrainedLanguage') { throw "The event MessageEvent is in constrained language mode and cannot be loaded!" }
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.ErrorEvent -Mode 'ConstrainedLanguage') { throw "The event ErrorEvent is in constrained language mode and cannot be loaded!" }
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.EndEvent -Mode 'ConstrainedLanguage') { throw "The event EndEvent is in constrained language mode and cannot be loaded!" }
            if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.FinalEvent -Mode 'ConstrainedLanguage') { throw "The event FinalEvent is in constrained language mode and cannot be loaded!" }
            
            if ($LoggingProviderInstance.Provider.Functions)
            {
                if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.Functions -Mode "ConstrainedLanguage") { throw "The functions resource scriptblock is in constrained language mode and cannot be loaded!" }
                # Invoke in current scope after localizing the scriptblock into the current context
                $LoggingProviderInstance.Provider.Functions.InvokeEx($false, $true, $false)
            }
            
            ${ functionNames } = @{
                Begin   = [guid]::NewGuid()
                Start   = [guid]::NewGuid()
                Message = [guid]::NewGuid()
                Error   = [guid]::NewGuid()
                End        = [guid]::NewGuid()
                Final   = [guid]::NewGuid()
            }
            
            function Get-ConfigValue
            {
                [CmdletBinding()]
                param (
                    [string]
                    $Name
                )
                
                $rootPath = $script:Instance.ConfigurationRoot
                if ($script:Instance.Name -and $script:Instance.Name -ne "Default")
                {
                    $rootPath += ".$($script:Instance.Name)"
                }
                
                Get-PSFConfigValue -FullName "$rootPath.$Name"
            }
            
            Set-Content -Path "function:\$(${ functionNames }.Begin)" -Value $LoggingProviderInstance.Provider.BeginEvent.ToString()
            $LoggingProviderInstance.BeginCommand = Get-Command ${ functionNames }.Begin
            Set-Content -Path "function:\$(${ functionNames }.Start)" -Value $LoggingProviderInstance.Provider.StartEvent.ToString()
            $LoggingProviderInstance.StartCommand = Get-Command ${ functionNames }.Start
            Set-Content -Path "function:\$(${ functionNames }.Message)" -Value $LoggingProviderInstance.Provider.MessageEvent.ToString()
            $LoggingProviderInstance.MessageCommand = Get-Command ${ functionNames }.Message
            Set-Content -Path "function:\$(${ functionNames }.Error)" -Value $LoggingProviderInstance.Provider.ErrorEvent.ToString()
            $LoggingProviderInstance.ErrorCommand = Get-Command ${ functionNames }.Error
            Set-Content -Path "function:\$(${ functionNames }.End)" -Value $LoggingProviderInstance.Provider.EndEvent.ToString()
            $LoggingProviderInstance.EndCommand = Get-Command ${ functionNames }.End
            Set-Content -Path "function:\$(${ functionNames }.Final)" -Value $LoggingProviderInstance.Provider.FinalEvent.ToString()
            $LoggingProviderInstance.FinalCommand = Get-Command ${ functionNames }.Final
            
            $ExecutionContext.SessionState.Module.PrivateData = @{
                Commands = ${ functionNames }
            }
            Remove-Variable -Name 'event', ' functionNames ', 'LoggingProviderInstance'
            
            Export-ModuleMember
        } -ErrorAction Stop
    }
    catch
    {
        $LoggingProviderInstance.Errors.Enqueue($_)
        $LoggingProviderInstance.Enabled = $false
    }
    if ($module)
    {
        $LoggingProviderInstance.Module = $module
    }
}

$scriptBlock = {
    try
    {
        $script:___ScriptName = 'PSFramework.Logging'
        
        Import-Module (Join-Path ([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot) 'PSFramework.psd1')
        
        while ($true)
        {
            # This portion is critical to gracefully closing the script
            if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].State -notlike "Running")
            {
                break
            }
            if (-not ([PSFramework.Message.LogHost]::LoggingEnabled)) { break }
            
            # Create instances as needed on cycle begin
            [PSFramework.Logging.ProviderHost]::UpdateAllInstances()
            
            #region Manage Begin Event
            #region V1 providers
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetEnabled())
            {
                if (-not $___provider.Initialized)
                {
                    [PSFramework.Logging.ProviderHost]::LoggingState = 'Initializing'
                    $___provider.LocalizeEvents()
                    
                    try
                    {
                        $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.BeginEvent, $null, $null)
                        $___provider.Initialized = $true
                    }
                    catch { $___provider.Errors.Push($_) }
                }
            }
            #endregion V1 providers
            
            #region V2 provider Instances
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetEnabledInstances())
            {
                if ($___instance.Initialized) { continue }
                
                [PSFramework.Logging.ProviderHost]::LoggingState = 'Initializing'
                
                try
                {
                    & $___instance.BeginCommand
                    $___instance.Initialized = $true
                }
                catch { $___instance.Errors.Enqueue($_)}
            }
            #endregion V2 provider Instances
            
            [PSFramework.Logging.ProviderHost]::LoggingState = 'Ready'
            #endregion Manage Begin Event
            
            #region Start Event
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
            {
                try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.StartEvent, $null, $null) }
                catch { $___provider.Errors.Push($_) }
            }
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
            {
                try { & $___instance.StartCommand }
                catch { $___instance.Errors.Enqueue($_) }
            }
            #endregion Start Event
            
            #region Message Event
            while ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0)
            {
                $Entry = $null
                [PSFramework.Message.LogHost]::OutQueueLog.TryDequeue([ref]$Entry)
                if ($Entry)
                {
                    [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing'
                    foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
                    {
                        if ($___provider.MessageApplies($Entry))
                        {
                            try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.MessageEvent, $null, $Entry) }
                            catch { $___provider.Errors.Push($_) }
                        }
                    }
                    
                    foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
                    {
                        if ($___instance.MessageApplies($Entry))
                        {
                            try { & $___instance.MessageCommand $Entry }
                            catch { $___instance.Errors.Enqueue($_) }
                        }
                    }
                }
                [PSFramework.Message.LogHost]::LastLogged = [DateTime]::Now
            }
            #endregion Message Event
            
            #region Error Event
            while ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0)
            {
                $Record = $null
                [PSFramework.Message.LogHost]::OutQueueError.TryDequeue([ref]$Record)
                
                if ($Record)
                {
                    [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing'
                    foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
                    {
                        if ($___provider.MessageApplies($Record))
                        {
                            try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.ErrorEvent, $null, $Record) }
                            catch { $___provider.Errors.Push($_) }
                        }
                    }
                    
                    foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
                    {
                        if ($___instance.MessageApplies($Record))
                        {
                            try { & $___instance.ErrorCommand $Record }
                            catch { $___instance.Errors.Enqueue($_) }
                        }
                    }
                }
                [PSFramework.Message.LogHost]::LastLogged = [DateTime]::Now
            }
            #endregion Error Event
            
            #region End Event
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
            {
                try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.EndEvent, $null, $null) }
                catch { $___provider.Errors.Push($_) }
            }
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
            {
                try { & $___instance.EndCommand }
                catch { $___instance.Errors.Enqueue($_) }
            }
            #endregion End Event
            
            #region Finalize / Cleanup
            # Adding $true will cause it to also return disabled providers / instances that are intitialized
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized($true))
            {
                if ($___provider.Enabled) { continue }
                try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.FinalEvent, $null, $null) }
                catch { $___provider.Errors.Push($_) }
                $___provider.Initialized = $false
            }
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances($true))
            {
                if ($___instance.Enabled) { continue }
                try { & $___instance.FinalCommand }
                catch { $___instance.Errors.Enqueue($_) }
                $___instance.Initialized = $false
            }
            #endregion Finalize / Cleanup
            
            [PSFramework.Logging.ProviderHost]::LoggingState = 'Ready'
            
            # Skip sleeping if the next messages already await
            if ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) { continue }
            Start-Sleep -Milliseconds ([PSFramework.Message.LogHost]::NextInterval)
        }
    }
    catch
    {
        $wasBroken = $true
    }
    finally
    {
        #region Flush log on exit
        if (([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].State -like "Running") -and (-not [PSFramework.Configuration.ConfigurationHost]::Configurations["psframework.logging.disablelogflush"].Value))
        {
            #region Start Event
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
            {
                try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.StartEvent, $null, $null) }
                catch { $___provider.Errors.Push($_) }
            }
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
            {
                try { & $___instance.StartCommand }
                catch { $___instance.Errors.Enqueue($_) }
            }
            #endregion Start Event
            
            #region Message Event
            while ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0)
            {
                $Entry = $null
                [PSFramework.Message.LogHost]::OutQueueLog.TryDequeue([ref]$Entry)
                if ($Entry)
                {
                    [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing'
                    foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
                    {
                        if ($___provider.MessageApplies($Entry))
                        {
                            try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.MessageEvent, $null, $Entry) }
                            catch { $___provider.Errors.Push($_) }
                        }
                    }
                    
                    foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
                    {
                        if ($___instance.MessageApplies($Entry))
                        {
                            try { & $___instance.MessageCommand $Entry }
                            catch { $___instance.Errors.Enqueue($_) }
                        }
                    }
                }
            }
            #endregion Message Event
            
            #region Error Event
            while ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0)
            {
                $Record = $null
                [PSFramework.Message.LogHost]::OutQueueError.TryDequeue([ref]$Record)
                
                if ($Record)
                {
                    [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing'
                    foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
                    {
                        if ($___provider.MessageApplies($Record))
                        {
                            try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.MessageEvent, $null, $Record) }
                            catch { $___provider.Errors.Push($_) }
                        }
                    }
                    
                    foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
                    {
                        if ($___instance.MessageApplies($Record))
                        {
                            try { & $___instance.ErrorCommand $Record }
                            catch { $___instance.Errors.Enqueue($_) }
                        }
                    }
                }
            }
            #endregion Error Event
            
            #region End Event
            foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
            {
                try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.EndEvent, $null, $null) }
                catch { $___provider.Errors.Push($_) }
            }
            foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
            {
                try { & $___instance.EndCommand }
                catch { $___instance.Errors.Enqueue($_) }
            }
            #endregion End Event
        }
        #endregion Flush log on exit
        
        #region Final Event
        foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
        {
            try { $ExecutionContext.InvokeCommand.InvokeScript($false, $___provider.FinalEvent, $null, $null) }
            catch { $___provider.Errors.Push($_) }
        }
        foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
        {
            try { & $___instance.FinalCommand }
            catch { $___instance.Errors.Enqueue($_) }
        }
        
        foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized())
        {
            $___provider.Initialized = $false
        }
        foreach ($___instance in [PSFramework.Logging.ProviderHost]::GetInitializedInstances())
        {
            $___instance.Initialized = $false
        }
        foreach ($___provider in [PSFramework.Logging.ProviderHost]::Providers.Values)
        {
            if ($___provider.ProviderVersion -eq 'Version_1') { continue }
            
            $___provider.Instances.Clear()
        }
        #endregion Final Event
        
        if ($wasBroken) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Broken' }
        else { [PSFramework.Logging.ProviderHost]::LoggingState = 'Stopped' }
        
        [PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].SignalStopped()
    }
}

Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'PSFramework.Logging' -NoMessage

$exemptedProcesses = 'CacheBuilder64', 'CacheBuilder', 'ImportModuleHelp'
# Do not start background Runspace if ...
if (
    -not (
        # ... run in the PowerShell Studio Cache Builder
        (($Host.Name -eq 'Default Host') -and ((Get-Process -Id $PID).ProcessName -in $exemptedProcesses)) -or
        # ... run in Azure Functions
        ($env:AZUREPS_HOST_ENVIRONMENT -like 'AzureFunctions/*')
    )
)
{
    Start-PSFRunspace -Name 'PSFramework.Logging' -NoMessage
}

Register-PSFTeppScriptblock -Name 'PSFramework.Callback.Name' -ScriptBlock {
    (Get-PSFCallback).Name | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-config-fullname" -ScriptBlock {
    [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { -not $_.Hidden } | Select-Object -ExpandProperty FullName
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-config-module" -ScriptBlock {
    [PSFramework.Configuration.ConfigurationHost]::Configurations.Values.Module | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-config-name" -ScriptBlock {
    $moduleName = "*"
    if ($fakeBoundParameter.Module) { $moduleName = $fakeBoundParameter.Module }
    [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { -not $_.Hidden -and ($_.Module -like $moduleName) } | Select-Object -ExpandProperty Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-Config-Schema' -ScriptBlock {
    [PSFramework.Configuration.ConfigurationHost]::Schemata.Keys
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-config-validation' -ScriptBlock {
    [PSFramework.Configuration.ConfigurationHost]::Validation.Keys
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-dynamiccontentobject-name' -ScriptBlock {
    [PSFramework.Utility.DynamicContentObject]::List
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-Encoding" -ScriptBlock {
    'Unicode'
    'BigEndianUnicode'
    'UTF8'
    'UTF8Bom'
    'UTF8NoBom'
    'UTF7'
    'UTF32'
    'Ascii'
    'Default'
    'BigEndianUTF32'
    if (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.FullTabCompletion')
    {
        [System.Text.Encoding]::GetEncodings().BodyName
    }
} -Global

Register-PSFTeppScriptblock -Name "PSFramework.Feature.Name" -ScriptBlock {
    (Get-PSFFeature).Name
} -Global

Register-PSFTeppScriptblock -Name "PSFramework.Filter.Module" -ScriptBlock {
    (Get-PSFFilterCondition).Module | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name "PSFramework.Filter.SetModule" -ScriptBlock {
    (Get-PSFFilterConditionSet).Module | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name "PSFramework.Filter.Name" -ScriptBlock {
    $module = '*'
    if ($fakeBoundParameters.Module) { $module = $fakeBoundParameters.Module }
    (Get-PSFFilterCondition -Module $module).Name | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name "PSFramework.Filter.SetName" -ScriptBlock {
    $module = '*'
    if ($fakeBoundParameters.Module) { $module = $fakeBoundParameters.Module }
    if ($fakeBoundParameters.SetModule) { $module = $fakeBoundParameters.SetModule }
    (Get-PSFFilterConditionSet -Module $module).Name | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name PSFramework-Input-ObjectProperty -ScriptBlock {
    #region Utility Functions
    function Get-Property
    {
        [CmdletBinding()]
        param (
            $InputObject
        )
        
        if (-not $InputObject) { return @{ } }
        $properties = @{ }
        
        switch ($InputObject.GetType().FullName)
        {
            #region Variables or static input
            'System.Management.Automation.Language.CommandExpressionAst'
            {
                switch ($InputObject.Expression.GetType().Name)
                {
                    'BinaryExpressionAst'
                    {
                        # Return an empty array. A binary expression ast means pure numbers as input, no properties
                        return @{ }
                    }
                    'VariableExpressionAst'
                    {
                        $members = Get-Variable -Name $InputObject.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Write-Output | Select-Object -First 1 | Get-Member -MemberType Properties
                        foreach ($member in $members)
                        {
                            try
                            {
                                $typeString = $member.Definition.Split(" ")[0]
                                $memberType = [type]$typeString
                                $typeKnown = $true
                            }
                            catch
                            {
                                $memberType = $null
                                $typeKnown = $false
                            }
                            
                            $properties[$member.Name] = [pscustomobject]@{
                                Name = $member.Name
                                Type = $memberType
                                TypeKnown = $typeKnown
                            }
                        }
                        return $properties
                    }
                    'MemberExpressionAst'
                    {
                        try { $members = Get-Variable -Name $InputObject.Expression.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Where-Object $InputObject.Expression.Member.Value -ne $null | Select-Object -First 1 -ExpandProperty $InputObject.Expression.Member.Value -ErrorAction Ignore | Get-Member -MemberType Properties }
                        catch { return $properties }
                        foreach ($member in $members)
                        {
                            try
                            {
                                $typeString = $member.Definition.Split(" ")[0]
                                $memberType = [type]$typeString
                                $typeKnown = $true
                            }
                            catch
                            {
                                $memberType = $null
                                $typeKnown = $false
                            }
                            
                            $properties[$member.Name] = [pscustomobject]@{
                                Name = $member.Name
                                Type = $memberType
                                TypeKnown = $typeKnown
                            }
                        }
                        return $properties
                    }
                    'ArrayLiteralAst'
                    {
                        # Not yet supported
                        return @{ }
                    }
                }
                #region Input from Variable
                if ($pipelineAst.PipelineElements[$inputIndex].Expression -and $pipelineAst.PipelineElements[0].Expression[0].VariablePath)
                {
                    $properties += ((Get-Variable -Name $pipelineAst.PipelineElements[0].Expression[0].VariablePath.UserPath -ValueOnly) | Select-Object -First 1 | Get-Member -MemberType Properties).Name
                }
                #endregion Input from Variable
            }
            #endregion Variables or static input
            
            #region Input from Command
            'System.Management.Automation.Language.CommandAst'
            {
                $command = Get-Command $InputObject.CommandElements[0].Value -ErrorAction Ignore
                if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
                if (-not $command) { return $properties }
                
                foreach ($type in $command.OutputType.Type)
                {
                    foreach ($member in $type.GetMembers("Instance, Public"))
                    {
                        # Skip all members except Fields (4) or Properties (16)
                        if (-not ($member.MemberType -band 20)) { continue }
                        
                        $properties[$member.Name] = [pscustomobject]@{
                            Name = $member.Name
                            Type = $null
                            TypeKnown = $true
                        }
                        if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType }
                        else { $properties[$member.Name].Type = $member.FieldType }
                    }
                    
                    foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$type.FullName]))
                    {
                        $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
                    }
                }
                
                #region Command Specific Inserts
                foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionCommandData[$command.Name]))
                {
                    $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
                }
                #endregion Command Specific Inserts
                
                return $properties
            }
            #endregion Input from Command
            
            # Unknown / Unexpected input
            default { return @{ } }
        }
    }
    
    function Update-Property
    {
        [CmdletBinding()]
        param (
            [Hashtable]
            $Property,
            
            $Step
        )
        
        $properties = @{ }
        #region Expand Property
        if ($Step.ExpandProperty)
        {
            if (-not ($Property[$Step.ExpandProperty])) { return $properties }
            
            $expanded = $Property[$Step.ExpandProperty]
            if (-not $expanded.TypeKnown) { return $properties }
            
            foreach ($member in $expanded.Type.GetMembers("Instance, Public"))
            {
                # Skip all members except Fields (4) or Properties (16)
                if (-not ($member.MemberType -band 20)) { continue }
                
                $properties[$member.Name] = [pscustomobject]@{
                    Name = $member.Name
                    Type = $null
                    TypeKnown = $true
                }
                if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType }
                else { $properties[$member.Name].Type = $member.FieldType }
            }
            
            foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$expanded.Type.FullName]))
            {
                $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
            }
            
            return $properties
        }
        #endregion Expand Property
        
        # In keep input mode, the original properties will not be affected in any way
        if ($Step.KeepInputObject) { $properties = $Property.Clone() }
        $filterProperties = $Step.Properties | Where-Object Kind -eq "Property"
        
        #region Select What to keep
        if (-not $Step.KeepInputObject)
        {
            :main foreach ($propertyItem in $Property.Values)
            {
                #region Excluded Properties
                foreach ($exclusion in $Step.Excluded)
                {
                    if ($propertyItem.Name -like $exclusion) { continue main }
                }
                #endregion Excluded Properties
                
                foreach ($stepProperty in $filterProperties)
                {
                    if ($propertyItem.Name -like $stepProperty.Name)
                    {
                        $properties[$propertyItem.Name] = $propertyItem
                        continue main
                    }
                }
            }
        }
        #endregion Select What to keep
        
        #region Adding Content
        :main foreach ($stepProperty in $Step.Properties)
        {
            switch ($stepProperty.Kind)
            {
                'Property'
                {
                    if ($stepProperty.Filter) { continue main }
                    if ($properties[$stepProperty.Name]) { continue main }
                    
                    foreach ($exclusion in $Step.Excluded)
                    {
                        if ($stepProperty.Name -like $exclusion) { continue main }
                    }
                    
                    $properties[$stepProperty.Name] = [PSCustomObject]@{
                        Name = $stepProperty.Name
                        Type = $null
                        TypeKnown = $false
                    }
                    continue main
                }
                'CalculatedProperty'
                {
                    if ($properties[$stepProperty.Name]) { continue main }
                    
                    $properties[$stepProperty.Name] = [PSCustomObject]@{
                        Name = $stepProperty.Name
                        Type = $null
                        TypeKnown = $false
                    }
                    continue main
                }
                'ScriptProperty'
                {
                    if ($properties[$stepProperty.Name]) { continue main }
                    
                    $properties[$stepProperty.Name] = [PSCustomObject]@{
                        Name = $stepProperty.Name
                        Type = $null
                        TypeKnown = $false
                    }
                    continue main
                }
                'AliasProperty'
                {
                    if ($properties[$stepProperty.Name]) { continue main }
                    
                    $properties[$stepProperty.Name] = [PSCustomObject]@{
                        Name = $stepProperty.Name
                        Type = $null
                        TypeKnown = $false
                    }
                    if ($properties[$stepProperty.Target].TypeKnown)
                    {
                        $properties[$stepProperty.Name].Type = $properties[$stepProperty.Target].Type
                        $properties[$stepProperty.Name].TypeKnown = $properties[$stepProperty.Target].TypeKnown
                    }
                    
                    continue main
                }
            }
        }
        #endregion Adding Content
        $properties
    }
    
    function Read-SelectObject
    {
        [CmdletBinding()]
        param (
            [System.Management.Automation.Language.CommandAst]
            $Ast,
            
            [string]
            $CommandName = 'Select-Object'
        )
        
        $results = [pscustomobject]@{
            Ast                = $Ast
            BoundParameters = @()
            Property        = @()
            ExcludeProperty = @()
            ExpandProperty  = ''
            ScriptProperty  = @()
            AliasProperty   = @()
            KeepInputObject = $false
        }
        
        #region Process Ast
        if ($Ast.CommandElements.Count -gt 1)
        {
            $index = 1
            $parameterName = ''
            $position = 0
            while ($index -lt $Ast.CommandElements.Count)
            {
                $element = $Ast.CommandElements[$index]
                switch ($element.GetType().FullName)
                {
                    'System.Management.Automation.Language.CommandParameterAst'
                    {
                        $parameterName = $element.ParameterName
                        if ($parameterName -like "k*") { $results.KeepInputObject = $true }
                        $results.BoundParameters += $element.ParameterName
                        break
                    }
                    'System.Management.Automation.Language.StringConstantExpressionAst'
                    {
                        if (-not $parameterName)
                        {
                            switch ($position)
                            {
                                0 { $results.Property = $element }
                                1 { $results.AliasProperty = $element }
                                2 { $results.ScriptProperty = $element }
                            }
                            $position = $position + 1
                        }
                        
                        if ($parameterName -like "pr*") { $results.Property = $element }
                        if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value }
                        if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value }
                        if ($parameterName -like "a*") { $results.AliasProperty = $element }
                        if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
                        $parameterName = ''
                        break
                    }
                    'System.Management.Automation.Language.ArrayLiteralAst'
                    {
                        if (-not $parameterName)
                        {
                            switch ($position)
                            {
                                0 { $results.Property = $element.Elements }
                                1 { $results.AliasProperty = $element.Elements }
                                2 { $results.ScriptProperty = $element.Elements }
                            }
                            $position = $position + 1
                        }
                        
                        if ($parameterName -like "pr*") { $results.Property = $element.Elements }
                        if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Elements.Value }
                        if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Elements.Value }
                        if ($parameterName -like "a*") { $results.AliasProperty = $element.Elements }
                        if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element.Elements }
                        
                        $parameterName = ''
                        break
                    }
                    'System.Management.Automation.Language.ConstantExpressionAst'
                    {
                        if (-not $parameterName)
                        {
                            switch ($position)
                            {
                                0 { $results.Property = $element }
                                1 { $results.AliasProperty = $element }
                                2 { $results.ScriptProperty = $element }
                            }
                            $position = $position + 1
                        }
                        
                        if ($parameterName -like "pr*") { $results.Property = $element }
                        if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value.ToString() }
                        if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value.ToString() }
                        if ($parameterName -like "a*") { $results.AliasProperty = $element }
                        if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
                        $parameterName = ''
                        break
                    }
                    'System.Management.Automation.Language.HashtableAst'
                    {
                        if (-not $parameterName)
                        {
                            switch ($position)
                            {
                                0 { $results.Property = $element }
                                1 { $results.AliasProperty = $element }
                                2 { $results.ScriptProperty = $element }
                            }
                            $position = $position + 1
                        }
                        
                        if ($parameterName -like "pr*") { $results.Property = $element }
                        if ($parameterName -like "a*") { $results.AliasProperty = $element }
                        if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
                        $parameterName = ''
                        break
                    }
                    default
                    {
                        $parameterName = ''
                    }
                }
                $index = $index + 1
            }
        }
        #endregion Process Ast
        
        #region Convert Results
        $resultsProcessed = [pscustomobject]@{
            HasIncludeFilter = $false
            RawResult         = $results
            Properties         = @()
            Excluded         = $results.ExcludeProperty
            ExpandProperty   = $results.ExpandProperty
            KeepInputObject  = $results.KeepInputObject
        }
        
        switch ($CommandName)
        {
            #region Select-Object
            'Select-Object'
            {
                #region Properties
                foreach ($element in $results.Property)
                {
                    switch ($element.GetType().FullName)
                    {
                        'System.Management.Automation.Language.HashtableAst'
                        {
                            try
                            {
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value
                                    Kind = "CalculatedProperty"
                                    Type = "Unknown"
                                    Filter = $false
                                }
                            }
                            catch { }
                        }
                        default
                        {
                            if ($element.Value -match "\*") { $resultsProcessed.HasIncludeFilter = $true }
                            
                            $resultsProcessed.Properties += [pscustomobject]@{
                                Name = $element.Value.ToString()
                                Kind = "Property"
                                Type = "Inherited"
                                Filter = $element.Value -match "\*"
                            }
                        }
                    }
                }
                #endregion Properties
            }
            #endregion Select-Object
            
            #region Select-PSFObject
            'Select-PSFObject'
            {
                #region Properties
                foreach ($element in $results.Property)
                {
                    switch ($element.GetType().FullName)
                    {
                        'System.Management.Automation.Language.HashtableAst'
                        {
                            try
                            {
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value
                                    Kind = "CalculatedProperty"
                                    Type = "Unknown"
                                    Filter = $false
                                }
                            }
                            catch { }
                        }
                        default
                        {
                            try { $parameterItem = ([PSFramework.Parameter.SelectParameter]$element.Value).Value }
                            catch { continue }
                            
                            if ($parameterItem -is [System.String])
                            {
                                if ($parameterItem -match "\*") { $resultsProcessed.HasIncludeFilter = $true }
                                
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name   = $parameterItem
                                    Kind   = "Property"
                                    Type   = "Inherited"
                                    Filter = $parameterItem -match "\*"
                                }
                            }
                            else
                            {
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name   = $parameterItem
                                    Kind   = "CalculatedProperty"
                                    Type   = "Unknown"
                                    Filter = $false
                                }
                            }
                        }
                    }
                }
                #endregion Properties
                
                #region Script Properties
                foreach ($scriptProperty in $results.ScriptProperty)
                {
                    switch ($scriptProperty.GetType().FullName)
                    {
                        'System.Management.Automation.Language.HashtableAst'
                        {
                            foreach ($name in $scriptProperty.KeyValuePairs.Item1.Value)
                            {
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name   = $name
                                    Kind   = "ScriptProperty"
                                    Type   = "Unknown"
                                    Filter = $false
                                }
                            }
                        }
                        default
                        {
                            try { $propertyValue = [PSFramework.Parameter.SelectScriptPropertyParameter]$scriptProperty.Value }
                            catch { continue }
                            
                            $resultsProcessed.Properties += [pscustomobject]@{
                                Name = $propertyValue.Value.Name
                                Kind = "ScriptProperty"
                                Type = "Unknown"
                                Filter = $false
                            }
                        }
                    }
                }
                #endregion Script Properties
                
                #region Alias Properties
                foreach ($scriptProperty in $results.AliasProperty)
                {
                    switch ($scriptProperty.GetType().FullName)
                    {
                        'System.Management.Automation.Language.HashtableAst'
                        {
                            foreach ($aliasPair in $scriptProperty.KeyValuePairs)
                            {
                                $resultsProcessed.Properties += [pscustomobject]@{
                                    Name = $aliasPair.Item1.Value
                                    Kind = "AliasProperty"
                                    Type = "Alias"
                                    Filter = $false
                                    Target = $aliasPair.Item2.PipelineElements.Expression.Value
                                }
                            }
                        }
                        default
                        {
                            try { $propertyValue = [PSFramework.Parameter.SelectAliasParameter]$scriptProperty.Value }
                            catch { continue }
                            
                            $resultsProcessed.Properties += [pscustomobject]@{
                                Name = $propertyValue.Aliases[0].Name
                                Kind = "AliasProperty"
                                Type = "Alias"
                                Filter = $false
                                Target = $propertyValue.Aliases[0].ReferencedMemberName
                            }
                        }
                    }
                }
                #endregion Alias Properties
            }
            #endregion Select-PSFObject
        }
        #endregion Convert Results
        
        $resultsProcessed
    }
    #endregion Utility Functions
    
    # Grab Pipeline and find starting index
    [System.Management.Automation.Language.PipelineAst]$pipelineAst = $commandAst.parent
    $index = $pipelineAst.PipelineElements.IndexOf($commandAst)
    
    # If it's the first item: Skip, no input to parse
    if ($index -lt 1) { return }
    
    $inputIndex = $index - 1
    $steps = @{ }
    
    #region Step backwards through the pipeline until the definitive object giver is found
    :outmain while ($true)
    {
        if ($pipelineAst.PipelineElements[$inputIndex].CommandElements)
        {
            # Resolve command and fail if it breaks
            $command = $null
            # Work around the ? alias for Where-Object being a wildcard
            if ($pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -eq "?") { $command = Get-Alias -Name "?" | Where-Object Name -eq "?" }
            else { $command = Get-Command $pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -ErrorAction Ignore }
            if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
            if (-not $command) { return }
            
            switch ($command.Name)
            {
                'Where-Object'
                {
                    $steps[$inputIndex] = [pscustomobject]@{
                        Index = $inputIndex
                        Skip  = $true
                        Type  = 'Where'
                    }
                    $inputIndex = $inputIndex - 1
                    continue outmain
                }
                'Tee-Object'
                {
                    $steps[$inputIndex] = [pscustomobject]@{
                        Index = $inputIndex
                        Skip  = $true
                        Type  = 'Tee'
                    }
                    $inputIndex = $inputIndex - 1
                    continue outmain
                }
                'Sort-Object'
                {
                    $steps[$inputIndex] = [pscustomobject]@{
                        Index = $inputIndex
                        Skip  = $true
                        Type  = 'Sort'
                    }
                    $inputIndex = $inputIndex - 1
                    continue outmain
                }
                #region Select-Object
                'Select-Object'
                {
                    $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-Object'
                    
                    $steps[$inputIndex] = [pscustomobject]@{
                        Index = $inputIndex
                        Skip  = $false
                        Type  = 'Select'
                        Data  = $selectObject
                    }
                    
                    if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty)
                    {
                        $inputIndex = $inputIndex - 1
                        continue outmain
                    }
                    break outmain
                }
                #endregion Select-Object
                #region Select-PSFObject
                'Select-PSFObject'
                {
                    $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-PSFObject'
                    
                    $steps[$inputIndex] = [pscustomobject]@{
                        Index = $inputIndex
                        Skip  = $false
                        Type  = 'PSFSelect'
                        Data  = $selectObject
                    }
                    
                    if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty)
                    {
                        $inputIndex = $inputIndex - 1
                        continue outmain
                    }
                    break outmain
                }
                #endregion Select-PSFObject
                default { break outmain }
            }
        }
        
        else
        {
            break
        }
    }
    #endregion Step backwards through the pipeline until the definitive object giver is found
    
    # Catch moving through _all_ options in the pipeline
    if ($inputIndex -lt 0) { return }
    
    #region Process resulting / reaching properties
    $properties = Get-Property -InputObject $pipelineAst.PipelineElements[$inputIndex]
    $inputIndex = $inputIndex + 1
    
    while ($inputIndex -lt $index)
    {
        # Eliminate preliminary follies
        if (-not $steps[$inputIndex]) { $inputIndex = $inputIndex + 1; continue }
        if ($steps[$inputIndex].Skip) { $inputIndex = $inputIndex + 1; continue }
        
        # Process the current step, then move on unless done
        $properties = Update-Property -Property $properties -Step $steps[$inputIndex].Data
        
        $inputIndex = $inputIndex + 1
    }
    #endregion Process resulting / reaching properties
    
    $properties.Keys | Sort-Object
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-license-name' -ScriptBlock {
    (Get-PSFLicense).Product
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-LanguageNames' -ScriptBlock {
    [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | Where-Object { $_ -and ($_.Trim()) }
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-LocalizedStrings-Names' -ScriptBlock {
    ([PSFRamework.Localization.LocalizationHost]::Strings.Values | Where-Object Module -EQ $fakeBoundParameter.Module).Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-LocalizedStrings-Modules' -ScriptBlock {
    [PSFRamework.Localization.LocalizationHost]::Strings.Values.Module | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-logging-provider' -ScriptBlock {
    (Get-PSFLoggingProvider).Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-logging-instance-provider' -ScriptBlock {
    (Get-PSFLoggingProviderInstance).Provider.Name | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-logging-instance-name' -ScriptBlock {
    if ($fakeBoundParameters.ProviderName)
    {
        return (Get-PSFLoggingProviderInstance -ProviderName $fakeBoundParameters.ProviderName).Name
    }
    (Get-PSFLoggingProviderInstance).Name | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Message.Module' -ScriptBlock {
    Get-PSFMessage | Select-Object -ExpandProperty ModuleName | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Message.Function' -ScriptBlock {
    Get-PSFMessage | Select-Object -ExpandProperty FunctionName | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Message.Tags' -ScriptBlock {
    Get-PSFMessage | Select-Object -ExpandProperty Tags | Remove-PSFNull -Enumerate | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Message.Runspace' -ScriptBlock {
    Get-PSFMessage | Select-Object -ExpandProperty Runspace | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Message.Level' -ScriptBlock {
    Get-PSFMessage | Select-Object -ExpandProperty Level | Select-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Utility.PathName' -ScriptBlock {
    (Get-PSFConfig "PSFramework.Path.*").Name -replace '^.+\.([^\.]+)$', '$1'
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-utility-psprovider' -ScriptBlock {
    (Get-PSProvider).Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-runspace-name' -ScriptBlock {
    (Get-PSFRunspace).Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Utility.Scriptblock.Name' -ScriptBlock {
    (Get-PSFScriptblock -List).Name
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework.Utility.Scriptblock.Tag' -ScriptBlock {
    (Get-PSFScriptblock -List).Tag | Sort-Object -Unique
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-tepp-scriptblockname' -ScriptBlock {
    [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys
} -Global

Register-PSFTeppScriptblock -Name 'PSFramework-tepp-parametername' -ScriptBlock {
    if ($fakeBoundParameter.Command)
    {
        $common = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm'
        
        try
        {
            $command = Get-Command $fakeBoundParameter.Command
            if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
            $command.Parameters.Keys | Where-Object { $_ -notin $common }
        }
        catch { }
    }
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-Unregister-PSFConfig-FullName" -ScriptBlock {
    switch ("$($fakeBoundParameter.Scope)")
    {
        "UserDefault" { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" }
        "UserMandatory" { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" }
        "SystemDefault" { $path = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" }
        "SystemMandatory" { $path = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" }
        default { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" }
    }
    
    if (Test-Path $path)
    {
        $properties = Get-ItemProperty -Path $path
        $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider'
        $properties.PSObject.Properties.Name | Where-Object { $_ -notin $common }
    }
} -Global

Register-PSFTeppScriptblock -Name "PSFramework-Unregister-PSFConfig-Module" -ScriptBlock {
    [PSFramework.Configuration.ConfigurationHost]::Configurations.Values.Module | Select-Object -Unique
} -Global

#region Configuration
Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname'
Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter Module -Name 'PSFramework-config-module'
Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter Name -Name 'PSFramework-config-name'

Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname'
Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter Module -Name 'PSFramework-config-module'
Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter Name -Name 'PSFramework-config-name'

Register-PSFTeppArgumentCompleter -Command Import-PSFConfig -Parameter Schema -Name 'PSFramework-Config-Schema'

Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname'
Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Module -Name 'PSFramework-config-module'
Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Name -Name 'PSFramework-config-name'
Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Validation -Name 'PSFramework-config-validation'

Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname'
Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter Module -Name 'PSFramework-config-module'
Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter Name -Name 'PSFramework-config-name'

Register-PSFTeppArgumentCompleter -Command Get-PSFConfigValue -Parameter FullName -Name 'PSFramework-config-fullname'

Register-PSFTeppArgumentCompleter -Command Unregister-PSFConfig -Parameter FullName -Name 'PSFramework-Unregister-PSFConfig-FullName'
Register-PSFTeppArgumentCompleter -Command Unregister-PSFConfig -Parameter Module -Name 'PSFramework-Unregister-PSFConfig-Module'
#endregion Configuration

#region Features
Register-PSFTeppArgumentCompleter -Command Get-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name'
Register-PSFTeppArgumentCompleter -Command Set-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name'
Register-PSFTeppArgumentCompleter -Command Test-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name'
#endregion Features

#region Flow Control
Register-PSFTeppArgumentCompleter -Command Get-PSFCallback -Parameter Name -Name 'PSFramework.Callback.Name'
Register-PSFTeppArgumentCompleter -Command Unregister-PSFCallback -Parameter Name -Name 'PSFramework.Callback.Name'
#endregion Flow Control

#region License
Register-PSFTeppArgumentCompleter -Command Get-PSFLicense -Parameter Filter -Name 'PSFramework-license-name'
#endregion License

#region Localization
Register-PSFTeppArgumentCompleter -Command Import-PSFLocalizedString -Parameter Language -Name 'PSFramework-LanguageNames'
Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Module -Name 'PSFramework-LocalizedStrings-Modules'
Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Name -Name 'PSFramework-LocalizedStrings-Names'
#endregion Localization

#region Logging
Register-PSFTeppArgumentCompleter -Command Get-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider'
Register-PSFTeppArgumentCompleter -Command Install-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider'
Register-PSFTeppArgumentCompleter -Command Set-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider'
Register-PSFTeppArgumentCompleter -Command Get-PSFLoggingProviderInstance -Parameter ProviderName -Name 'PSFramework-logging-instance-provider'
Register-PSFTeppArgumentCompleter -Command Get-PSFLoggingProviderInstance -Parameter Name -Name 'PSFramework-logging-instance-name'
#endregion Logging

#region Message
Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter ModuleName -Name 'PSFramework.Message.Module'
Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter FunctionName -Name 'PSFramework.Message.Function'
Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Tag -Name 'PSFramework.Message.Tags'
Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Runspace -Name 'PSFramework.Message.Runspace'
Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Level -Name 'PSFramework.Message.Level'
#endregion Message

#region Runspace
Register-PSFTeppArgumentCompleter -Command Get-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name'
Register-PSFTeppArgumentCompleter -Command Register-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name'
Register-PSFTeppArgumentCompleter -Command Stop-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name'
Register-PSFTeppArgumentCompleter -Command Start-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name'

Register-PSFTeppArgumentCompleter -Command Get-PSFDynamicContentObject -Parameter Name -Name 'PSFramework-dynamiccontentobject-name'
Register-PSFTeppArgumentCompleter -Command Set-PSFDynamicContentObject -Parameter Name -Name 'PSFramework-dynamiccontentobject-name'
#endregion Runspace

#region Serialization
Register-PSFTeppArgumentCompleter -Command Export-PSFClixml -Parameter Encoding -Name 'PSFramework-Encoding'
Register-PSFTeppArgumentCompleter -Command Import-PSFClixml -Parameter Encoding -Name 'PSFramework-Encoding'
#endregion Serialization

#region Tab Completion
Register-PSFTeppArgumentCompleter -Command Set-PSFTeppResult -Parameter TabCompletion -Name 'PSFramework-tepp-scriptblockname'
Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Name -Name 'PSFramework-tepp-scriptblockname'
Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Parameter -Name 'PSFramework-tepp-parametername'
#endregion Tab Completion

#region Utility
Register-PSFTeppArgumentCompleter -Command ConvertFrom-PSFArray -Parameter PropertyName -Name PSFramework-Input-ObjectProperty

Register-PSFTeppArgumentCompleter -Command ConvertTo-PSFHashtable -Parameter Include -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command ConvertTo-PSFHashtable -Parameter Exclude -Name PSFramework-Input-ObjectProperty

Register-PSFTeppArgumentCompleter -Command Resolve-PSFPath -Parameter Provider -Name PSFramework-utility-psprovider
Register-PSFTeppArgumentCompleter -Command Get-PSFPath -Parameter Name -Name 'PSFramework.Utility.PathName'
Register-PSFTeppArgumentCompleter -Command Set-PSFPath -Parameter Name -Name 'PSFramework.Utility.PathName'

Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExpandProperty -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExcludeProperty -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowProperty -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowExcludeProperty -Name PSFramework-Input-ObjectProperty
#endregion Utility


$mappings = @{
    "deserialized.microsoft.activedirectory.management.addomaincontroller" = @("HostName", "Name")
    "microsoft.activedirectory.management.addomaincontroller"               = @("HostName", "Name")
    "microsoft.sqlserver.management.smo.server"                               = @("NetName", "DomainInstanceName")
    "deserialized.microsoft.sqlserver.management.smo.server"               = @("NetName", "DomainInstanceName")
    "microsoft.sqlserver.management.smo.linkedserver"                       = @("Name")
    "deserialized.microsoft.sqlserver.management.smo.linkedserver"           = @("Name")
    "microsoft.activedirectory.management.adcomputer"                       = @("DNSHostName", "Name")
    "deserialized.microsoft.activedirectory.management.adcomputer"           = @("DNSHostName", "Name")
    "Microsoft.DnsClient.Commands.DnsRecord_A"                               = @("Name", "IPAddress")
    "Deserialized.Microsoft.DnsClient.Commands.DnsRecord_A"                   = @("Name", "IPAddress")
    "Microsoft.DnsClient.Commands.DnsRecord_AAAA"                           = @("Name", "IPAddress")
    "Deserialized.Microsoft.DnsClient.Commands.DnsRecord_AAAA"               = @("Name", "IPAddress")
}


foreach ($key in $mappings.Keys)
{
    Register-PSFParameterClassMapping -ParameterClass 'Computer' -TypeName $key -Properties $mappings[$key]
}

#region Configuration Static Remove() Compatibility
Update-TypeData -TypeName "System.Collections.Concurrent.ConcurrentDictionary``2[[$([System.String].AssemblyQualifiedName)],[$([PSFramework.Configuration.Config].AssemblyQualifiedName)]]" -MemberType ScriptMethod -MemberName Remove -Value ([scriptblock]::Create(@'
param (
    $Item
)
 
$dummyItem = $null
$null = $this.TryRemove($Item, [ref] $dummyItem)
'@
)) -Force
#endregion Configuration Static Remove() Compatibility

$scriptBlock = {
    $script:___ScriptName = 'psframework.taskengine'
    
    try
    {
        #region Main Execution
        while ($true)
        {
            # This portion is critical to gracefully closing the script
            if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].State -notlike "Running")
            {
                break
            }
            
            $task = $null
            $tasksDone = @()
            while ($task = [PSFramework.TaskEngine.TaskHost]::GetNextTask($tasksDone))
            {
                $task.State = 'Running'
                try
                {
                    [PSFramework.Utility.UtilityHost]::ImportScriptBlock($task.ScriptBlock)
                    $task.ScriptBlock.Invoke()
                    $task.State = 'Pending'
                }
                catch
                {
                    $task.State = 'Error'
                    $task.LastError = $_
                    Write-PSFMessage -EnableException $false -Level Warning -Message "[Maintenance] Task '$($task.Name)' failed to execute" -ErrorRecord $_ -FunctionName "task:TaskEngine" -Target $task -ModuleName PSFramework
                }
                $task.LastExecution = Get-Date
                if (-not $task.Pending -and ($task.Status -eq "Pending")) { $task.Status = 'Completed' }
                $tasksDone += $task.Name
            }
            
            # If there will no more tasks need executing in the future, might as well kill the runspace
            if (-not ([PSFramework.TaskEngine.TaskHost]::HasPendingTasks)) { break }
            
            Start-Sleep -Seconds 5
        }
        #endregion Main Execution
    }
    catch {  }
    finally
    {
        [PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].SignalStopped()
    }
}

Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'psframework.taskengine' -NoMessage

#region Handle Module Removal
$PSF_OnRemoveScript = {
    # Stop all managed runspaces ONLY on the main runspace's termination
    if ([runspace]::DefaultRunspace.Id -eq 1)
    {
        Wait-PSFMessage -Timeout 30s -Terminate
        Get-PSFRunspace | Stop-PSFRunspace
        [PSFramework.PSFCore.PSFCoreHost]::Uninitialize()
    }
    
    # Properly disconnect all remote sessions still held open
    $psframework_pssessions.Values | Remove-PSSession
    # Remove all Runspace-specific callbacks
    [PSFramework.FlowControl.CallbackHost]::RemoveRunspaceOwned()
}
$ExecutionContext.SessionState.Module.OnRemove += $PSF_OnRemoveScript
Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $PSF_OnRemoveScript
#endregion Handle Module Removal

#region Declare runtime variable for the flow control component
$paramNewVariable = @{
    Name  = "psframework_killqueue"
    Value = (New-Object PSFramework.Utility.LimitedConcurrentQueue[int](25))
    Option = 'ReadOnly'
    Scope = 'Script'
    Description = 'Variable that is used to maintain the list of commands to kill. This is used by Test-PSFFunctionInterrupt. Note: The value tested is the hashcade from the callstack item.'
}

New-Variable @paramNewVariable
#endregion Declare runtime variable for the flow control component

#region Declare PSSession Cache
$paramNewVariable2 = @{
    Name  = "psframework_pssessions"
    Value = (New-Object PSFramework.ComputerManagement.PSSessionContainer)
    Option = 'ReadOnly'
    Scope = 'Script'
    Description = 'Variable containing the list of established powershell remoting sessions. This is used by Invoke-PSFCommand to track connections, disconnect expired sessions and reconnect sessions by name.'
}

New-Variable @paramNewVariable2
#endregion Declare PSSession Cache

#region Register Features
Register-PSFFeature -Name 'PSFramework.InheritEnableException' -NotGlobal -Owner PSFramework -Description 'Causes all PSFramework commands with the -EnableException parameter to check, whether the caller has that variable set (e.g. by having a parameter with the same name) and respect that as well.'
Register-PSFFeature -Name 'PSFramework.Stop-PSFFunction.ShowWarning' -Owner PSFramework -Description 'Causes calls to Stop-PSFFunction to always show warnings. By default, using "-EnableException $true" will only throw the exception but not show the warning.'
Register-PSFFeature -Name 'PSFramework.Sort-PSFObject.IgnoreVersionError' -Owner PSFramework -Description 'Causes Sort-PSFObject to ignore parameters that do not exist in the current PowerShell version.'
[PSFramework.Feature.FeatureHost]::WriteGlobalFlag('PSFramework.Sort-PSFObject.IgnoreVersionError', $true) # $true by default
#endregion Register Features

# The main runspace-local host used for the Filter feature
$script:filterContainer = [PSFramework.Filter.FilterContainer]::Filters

# Load Session Registrations for the Session Container feature
# See: New-PSSessionContainer

Register-PSFSessionObjectType -DisplayName CimSession -TypeName Microsoft.Management.Infrastructure.CimSession
Register-PSFSessionObjectType -DisplayName PSSession -TypeName System.Management.Automation.Runspaces.PSSession
Register-PSFSessionObjectType -DisplayName SmoServer -TypeName Microsoft.SqlServer.Management.Smo.Server

[PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.FileInfo'] = @(
    [PSCustomObject]@{
        Name      = 'PSChildName'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSDrive'
        Type      = ([type]'System.Management.Automation.PSDriveInfo')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSIsContainer'
        Type      = ([type]'System.Boolean')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSParentPath'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSPath'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSProvider'
        Type      = ([type]'System.Management.Automation.ProviderInfo')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'BaseName'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'VersionInfo'
        Type      = ([type]'System.Diagnostics.FileVersionInfo')
        TypeKnown = $true
    }
)

[PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.DirectoryInfo'] = @(
    [PSCustomObject]@{
        Name      = 'PSChildName'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSDrive'
        Type      = ([type]'System.Management.Automation.PSDriveInfo')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSIsContainer'
        Type      = ([type]'System.Boolean')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSParentPath'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSPath'
        Type      = ([type]'System.String')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'PSProvider'
        Type      = ([type]'System.Management.Automation.ProviderInfo')
        TypeKnown = $true
    },
    [PSCustomObject]@{
        Name      = 'BaseName'
        Type      = ([type]'System.String')
        TypeKnown = $true
    }
)

Set-PSFScriptblock -Name 'PSFramework.Validate.Filter.ConditionName' -Scriptblock {
    $_ -match '^[\d\w_]+$' -and $_ -notin 0,1
} -Global

#region Path Generic
Set-PSFScriptblock -Name 'PSFramework.Validate.Path' -Scriptblock {
    Test-Path -Path $_
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.Path.Container' -Scriptblock {
    Test-Path -Path $_ -PathType Container
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.Path.Leaf' -Scriptblock {
    Test-Path -Path $_ -PathType Leaf
} -Global
#endregion Path Generic

#region Path: File System
Set-PSFScriptblock -Name 'PSFramework.Validate.FSPath' -Scriptblock {
    if (-not (Test-Path -Path $_)) { return $false }
    if ((Get-Item $_ -Force).PSProvider.Name -ne 'FileSystem') { return $false }
    
    $true
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.FSPath.File' -Scriptblock {
    if (-not (Test-Path -Path $_)) { return $false }
    if ((Get-Item $_ -Force).PSProvider.Name -ne 'FileSystem') { return $false }
    
    Test-Path -Path $_ -PathType Leaf
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.FSPath.FileOrParent' -Scriptblock {
    try { Resolve-PSFPath -Path $_ -Provider FileSystem -NewChild -SingleItem }
    catch { $false }
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.FSPath.Folder' -Scriptblock {
    if (-not (Test-Path -Path $_)) { return $false }
    if ((Get-Item $_ -Force).PSProvider.Name -ne 'FileSystem') { return $false }
    
    Test-Path -Path $_ -PathType Container
} -Global
#endregion Path: File System

#region Uri
Set-PSFScriptblock -Name 'PSFramework.Validate.Uri.Absolute' -Scriptblock {
    $uri = $_ -as [uri]
    $uri.IsAbsoluteUri
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.Uri.Absolute.Https' -Scriptblock {
    $uri = $_ -as [uri]
    $uri.IsAbsoluteUri -and ($uri.Scheme -eq 'https')
} -Global

Set-PSFScriptblock -Name 'PSFramework.Validate.Uri.Absolute.File' -Scriptblock {
    $uri = $_ -as [uri]
    $uri.IsAbsoluteUri -and ($uri.Scheme -eq 'file')
} -Global
#endregion Uri

Set-PSFScriptblock -Name 'PSFramework.Validate.TimeSpan.Positive' -Scriptblock {
    if ($_ -is [PSFTimeSpan]) { $_.Value.Ticks -gt 0 }
    else { $_.Ticks -gt 0 }
} -Global

$paramCon = @{
    Module = 'PSFramework'
    Type = 'Static'
}

$null = New-PSFFilterConditionSet -Module PSFramework -Name Environment -Version '1.0.0' -ScriptBlock {
    #region OS Version
    New-PSFFilterCondition @paramCon -Name OSWindows -ScriptBlock {
        $PSVersionTable.PSVersion.Major -lt 6 -or $global:IsWindows
    }
    New-PSFFilterCondition @paramCon -Name OSLinux -ScriptBlock {
        $PSVersionTable.PSVersion.Major -ge 6 -and $global:IsLinux
    }
    New-PSFFilterCondition @paramCon -Name OSMacOS -ScriptBlock {
        $PSVersionTable.PSVersion.Major -ge 6 -and $global:IsMacOS
    }
    #endregion OS Version
    
    #region PS Version
    New-PSFFilterCondition @paramCon -Name PS3 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 3
    }
    New-PSFFilterCondition @paramCon -Name PS4 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 4
    }
    New-PSFFilterCondition @paramCon -Name PS5 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 5
    }
    New-PSFFilterCondition @paramCon -Name PS6 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 6
    }
    New-PSFFilterCondition @paramCon -Name PS7_0 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -eq 0
    }
    New-PSFFilterCondition @paramCon -Name PS7_1 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -eq 1
    }
    New-PSFFilterCondition @paramCon -Name PS7_2 -ScriptBlock {
        $PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -eq 2
    }
    New-PSFFilterCondition @paramCon -Name PS5Plus -ScriptBlock {
        $PSVersionTable.PSVersion.Major -ge 5
    }
    New-PSFFilterCondition @paramCon -Name PS6Plus -ScriptBlock {
        $PSVersionTable.PSVersion.Major -ge 6
    }
    New-PSFFilterCondition @paramCon -Name PS7Plus -ScriptBlock {
        $PSVersionTable.PSVersion.Major -ge 7
    }
    #endregion PS Version
    
    #region Elevation
    New-PSFFilterCondition @paramCon -Name Elevated -ScriptBlock {
        if ($PSVersionTable.PSVersion.Major -ge 6 -and $global:IsLinux) { return $true }
        if ($PSVersionTable.PSVersion.Major -ge 6 -and $global:IsLinux) { return $true }
        
        $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = New-Object Security.Principal.WindowsPrincipal $identity
        $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
    }
    #endregion Elevation
    
    #region Pipelines
    New-PSFFilterCondition @paramCon -Name EnvGithubAction -ScriptBlock {
        (Get-Item env:GITHUB_ACTION -ErrorAction Ignore) -as [bool]
    }
    New-PSFFilterCondition @paramCon -Name EnvAzDevPipeline -ScriptBlock {
        (Get-Item 'env:System.CollectionId' -ErrorAction Ignore) -and (Get-Item 'env:System.DefaultWorkingDirectory' -ErrorAction Ignore)
    }
    #endregion Pipelines
}

$license = New-PSFLicense -Product 'PSFramework' -Manufacturer 'Friedrich Weinmann' -ProductVersion $ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date -Year 2017 -Month 04 -Day 27 -Hour 0 -Minute 0 -Second 0) -Text @"
Copyright (c) Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


#region Chris Dent
$null = New-PSFLicense -Product 'Import-PSCmdlet' -Manufacturer 'Chris Dent' -ProductVersion '1.0.0.0' -ProductType Script -Name MIT -Version '1.0.0.0' -Date (Get-Date -Year 2018 -Month 05 -Day 16).Date -Text @"
Copyright (c) Chris Dent
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@
 -Description @"
The PSFramework is happy to publish the Import-PSFCmdlet command, based on the
original work of Chris Dent's, 'Import-PSCmdlet'
 
Thank you for allowing its use :)
- Original Source: https://www.indented.co.uk/cmdlets-without-a-dll/
- Author blog: https://www.indented.co.uk/
"@
 -Parent $license
#endregion Chris Dent

#region Joel Bennet
$null = New-PSFLicense -Product 'Configuration-ExportPaths' -Manufacturer 'Joel Bennet' -ProductVersion '1.3.0' -ProductType Script -Name MIT -Version '1.0.0.0' -Date (Get-Date -Year 2018 -Month 05 -Day 16).Date -Text @"
Copyright (c) 2015 Joel Bennett
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"@
 -Description @"
The PSFramework is happy to base its internal path selection for configuration
exports on the original work of Joel Bennet's, 'Configuration' module.
Its implementation can be found in the internal script file:
internal/scripts/loadConfigurationPersisted.ps1
 
Thank you for allowing its use :)
- Original Source: https://github.com/PoshCode/Configuration
- Author blog: http://huddledmasses.org/blog/
- Author Twitter: https://twitter.com/jaykul?lang=en
"@
 -Parent $license
#endregion Joel Bennet

#region Jason Shirk: Tab Expansion Plus Plus
$null = New-PSFLicense -Product 'TabExpansionPlusPlus' -Manufacturer 'Jason Shirk' -ProductVersion '1.2' -ProductType Module -Name BSD-2 -Version '2.0.0.0' -Date (Get-Date -Year 2013 -Month 05 -Day 8).Date -Text @'
Copyright (c) 2013, Jason Shirk
All rights reserved.
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 
1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'@
 -Description @'
The PSFramework would like to thank Jason Shirk for his work on improving user experience.
 
We include major portions of his module "TabExpansionPlusPlus" which can be found on Github:
https://github.com/lzybkr/TabExpansionPlusPlus
The source we use can be found at:
internal/scripts/teppCoreCode.ps1
 
It is used to provide improved tab expansion experience on PowerShell versions 3 or 4.
'@
 -Parent $license
#endregion Jason Shirk: Tab Expansion Plus Plus
#endregion Load compiled code