include/ModuleHelperFunctions.ps1


#region Module Configuration

function Get-ModuleConfiguration {
    try {
        Get-Variable -Scope Global -Name ('ModuleConfiguration_{0}' -f ($MyInvocation.MyCommand.Module.Name)) -ValueOnly -ErrorAction Stop
    } catch {
        Write-Error -Message 'Failed to retrevie module configuration' -ErrorRecord $_
    }
}

function Initialize-ModuleConfiguration {
    # Define ModuleConfiguration class
    class ModuleConfiguration {
        [string]$ModuleName = ''
        [string]$ModuleRootPath = ''
        [string]$ModuleManifestPath = ''
        [hashtable]$ModuleFolders = @{}
        [hashtable]$ModuleFiles = @{}
        [hashtable]$ModuleFilePaths = @{}
        $ModuleManifest

        [void] CollectModuleFilePaths () {
            Get-ChildItem -Path $this.ModuleRootPath -Recurse -File | foreach-object {
                $this.ModuleFilePaths.($PSItem.Name) = $PSItem.FullName
            }
        }

        [void] ImportFiles () {
            foreach ($File in (Get-ChildItem -Path $this.ModuleFolders.Settings -File -Recurse)) {
                try {
                    $Content = $null
                    switch ($File.Extension) {
                        '.csv' { $Content = Import-Csv $File.FullName -Delimiter ';' -Encoding UTF8 -ErrorAction Stop }
                        '.psd1' { $Content = Import-PowerShellDataFile -Path $File.fullname -ErrorAction Stop }
                        '.json' { $Content = Get-Content -Path $File.FullName -ErrorAction Stop -raw | ConvertFrom-Json -ErrorAction Stop }
                        '.cred' { $Content = Import-Clixml -Path $File.FullName  -ErrorAction Stop }
                        default {
                            Write-Warning -Message 'Failed to import configuration file, unknown extension' -Target $File.Name
                            $Content = $null
                        }
                    }
                    if ($Content) {
                        $null = $this.ModuleFiles.Add($File.BaseName, $Content)
                    }
                } catch {
                    Write-Error -Message 'Failed to import configuration' -Targetobject $File.BaseName -ErrorRecord $_
                }
            }
        }

        ModuleConfiguration (
            $MyInvoc
        ) {
            # ModuleName
            $this.ModuleName = $MyInvoc.MyCommand.Module.Name

            # ModuleRootPath
            $this.ModuleRootPath = $MyInvoc.PSScriptRoot

            # ModuleFolders
            Get-ChildItem -Path $this.ModuleRootPath -Directory | ForEach-Object { $null = $this.ModuleFolders.Add($_.Name, $_.Fullname) }
            
            # ModuleManifestPath
            $this.ModuleManifestPath = (Join-Path -Path $this.ModuleRootPath -ChildPath ('{0}.psd1' -f $this.ModuleName))

            # ModuleManifest
            $this.ModuleManifest = Import-PowershellDataFile -Path $this.ModuleManifestPath

            # ModuleFiles
            $this.ImportFiles()

            # ModuleFilePaths
            $this.CollectModuleFilePaths()
        }
    }

    # Store module configuration
    try {
        $null = New-Variable -Scope Global -Name ('ModuleConfiguration_{0}' -f ($MyInvocation.MyCommand.Module.Name)) -Value ([ModuleConfiguration]::New($MyInvocation)) -Force -ErrorAction Stop
    } catch {
        Write-Error -Message 'Failed to store ModuleConfiguration' -ErrorRecord $_
    }
}

#endregion

#region Logging

# Class definition
class PSLogEntry {
    [string]$Message
    [string]$MessageForConsole
    [string]$Target
    [datetime]$TimeStamp
    [string]$Severity
    [string]$Source
    $LogSettings
    $ErrorRecord
    $ChangeObject
    

    [void]WriteToConsole() {
        switch ($this.Severity) {
            'SKIP' {
                Write-Host -Object ('SKIP: {0}' -f $this.MessageForConsole) -ForegroundColor Yellow
            }
            'SUCCESS' {
                Write-Host -Object ('SUCCESS: {0}' -f $this.MessageForConsole) -ForegroundColor Green
            }
            'INFO' {
                Write-Host -Object ('INFO: {0}' -f $this.MessageForConsole)
            }
            'TASK' {
                Write-host
                Write-host (' Task | ') -foregroundcolor Cyan -NoNewline
                Write-host $This.Message -ForegroundColor Magenta -NoNewline 
                Write-Host ' |' -ForegroundColor Cyan
            }
            'ERROR' {
                Write-Host -Object ('ERROR: {0}' -f $this.MessageForConsole) -ForegroundColor Red

                if ($this.ErrorRecord) {
                    if ($this.ErrorRecord.InvocationInfo) {
                        Write-Host  -Object (' {0,-20}{1}' -f '+ ScriptName: ', $this.ErrorRecord.InvocationInfo.ScriptName) -ForegroundColor Red
                        Write-Host  -Object (' {0,-20}{1}' -f '+ LineNbr: ', $this.ErrorRecord.InvocationInfo.ScriptLineNumber) -ForegroundColor Red
                        Write-Host  -Object (' {0,-16}{1}' -f '+ Code:', $this.ErrorRecord.InvocationInfo.Line.Replace("`r", "").Replace("`n", "").TrimStart(' ')) -ForegroundColor Red
                    }
                    Write-Host  -Object (' {0,-20}{1}' -f '+ ExpMessage:', $this.ErrorRecord.Exception.Message) -ForegroundColor Red
                }
            }
            'CHANGE' {
                function Write-PSProperty {
                    param ($Value)
                    Write-Host '[' -ForegroundColor Magenta -NoNewline
                    Write-Host $Value -ForegroundColor Blue -NoNewline
                    Write-Host ']' -ForegroundColor Magenta -NoNewline
                }

                function Write-PSResult {
                    param($Result)
                    switch ($Result) {
                        'SUCCESS' {
                            Write-Host ' succeeded' -ForegroundColor Green -NoNewline
                        }
                        'FAILED' {
                            Write-Host ' failed' -ForegroundColor Red -NoNewline
                        }
                        'UNCHANGED' {
                            Write-Host ' was skipped, no change' -ForegroundColor Yellow -NoNewline
                        }
                    }
                }

                switch ($this.ChangeObject.Operation) {
                    'Add' {
                        Write-Host 'CHANGE: Adding value ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Value
                        Write-Host ' to property ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Property
                        Write-Host ' for target ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Target
                        Write-PSResult -Result $this.ChangeObject.Result
                        Write-Host
                    }
                    'Remove' {
                        Write-Host 'CHANGE: Removing value ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Value
                        Write-Host ' to property ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Property
                        Write-Host ' for target ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Target
                        Write-PSResult -Result $this.ChangeObject.Result
                        Write-Host
                    }
                    'replace' {
                        Write-Host 'CHANGE: Replacing value ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.PreviousValue
                        Write-Host ' with new value ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Value
                        Write-Host ' in property ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Property
                        Write-Host ' for target ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Target
                        Write-PSResult -Result $this.ChangeObject.Result
                        Write-Host
                    }
                    'Clear' {
                        Write-Host 'CHANGE: Clearing value ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.PreviousValue
                        Write-Host ' from property ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Property
                        Write-Host ' for target ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Target
                        Write-PSResult -Result $this.ChangeObject.Result
                        Write-Host
                    }
                    'Move' {
                        Write-Host 'CHANGE: Moving object ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Target
                        Write-Host ' from ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.PreviousValue
                        Write-Host ' to ' -NoNewline
                        Write-PSProperty -Value $this.ChangeObject.Value
                        Write-PSResult -Result $this.ChangeObject.Result
                        Write-Host  
                    }
                }
            }
        }
    }
    
    [void]WriteToFile() {
        $ConvertFromCSVSplatting = @{
            Delimiter   = ';'
            Header      = 'Datetime', 'Source' , 'Type', 'Target', 'Message'
            InputObject = ('{0};{1};{4};{2};{3}' -f $this.TimeStamp.ToString('yyyy-MM-dd_HH:mm:ss:fff'), $this.Source , $this.Target, $this.Message, $this.Severity)
        }
        $ExportCSVSplatting = @{
            Path              = $this.LogSettings.GetLogFullName()
            Append            = $true
            NoTypeInformation = $true
            Encoding          = $this.LogSettings.Encoding
            Delimiter         = $this.LogSettings.LogDelimiter
        }
        ConvertFrom-Csv @ConvertFromCSVSplatting | Export-Csv @ExportCSVSplatting
        if ($this.ErrorRecord) {
            $this.WriteErrorRecordToFile()
        }
    }

    [void]WriteErrorRecordToFile() {
        $ExportCSVSplatting = @{
            Path              = $this.LogSettings.GetLogFullName()
            Append            = $true
            NoTypeInformation = $true
            Encoding          = $this.LogSettings.Encoding
            Delimiter         = $this.LogSettings.LogDelimiter
        } 
        $ConvertFromCSVSplatting = @{
            Delimiter   = ';'
            Header      = 'Datetime', 'Source' , 'Type', 'Target', 'Message'
            InputObject = ''
        }
        if ($this.ErrorRecord.InvocationInfo) {
            $ConvertFromCSVSplatting.InputObject = ('{0};{1};{4};{2};{3}' -f $this.TimeStamp.ToString('yyyy-MM-dd_HH:mm:ss:fff'), $this.Source , $this.Target, (' + ScriptName: {0}' -f $this.ErrorRecord.InvocationInfo.ScriptName) , $this.Severity)
            ConvertFrom-Csv @ConvertFromCSVSplatting | Export-Csv @ExportCSVSplatting
            $ConvertFromCSVSplatting.InputObject = ('{0};{1};{4};{2};{3}' -f $this.TimeStamp.ToString('yyyy-MM-dd_HH:mm:ss:fff'), $this.Source , $this.Target, (' + LineNbr: {0}' -f $this.ErrorRecord.InvocationInfo.ScriptLineNumber)  , $this.Severity)
            ConvertFrom-Csv @ConvertFromCSVSplatting | Export-Csv @ExportCSVSplatting
            $ConvertFromCSVSplatting.InputObject = ('{0};{1};{4};{2};{3}' -f $this.TimeStamp.ToString('yyyy-MM-dd_HH:mm:ss:fff'), $this.Source , $this.Target, (' + Code: {0}' -f $this.ErrorRecord.InvocationInfo.Line.Replace("`r", "").Replace("`n", ""))  , $this.Severity)
            ConvertFrom-Csv @ConvertFromCSVSplatting | Export-Csv @ExportCSVSplatting
        }    
        $ConvertFromCSVSplatting.InputObject = ('{0};{1};{4};{2};{3}' -f $this.TimeStamp.ToString('yyyy-MM-dd_HH:mm:ss:fff'), $this.Source , $this.Target, (' + ExpMessage: {0}' -f $this.ErrorRecord.Exception.Message)  , $this.Severity)
        ConvertFrom-Csv @ConvertFromCSVSplatting | Export-Csv @ExportCSVSplatting
    }

    [string]FindSource() {
        return (get-pscallstack).Where( { $_.Command -ne '' -and $_.Command -notlike '*<ScriptBlock>*' }) | select-object -skip 1 | select-object -first 1 | select-object -expandproperty location        
    }

    [void]UpdateMessage() {
        $Separator = [string]''
        for ($i = 0; $i -lt (9 - ($this.Severity.Length + 2))  ; $i++) {
            $Separator += ' '
        }
        if ($this.Target -eq 'N/A') {
            $this.MessageForConsole = ('{0}{1}' -f $Separator, $this.Message)
        } else {
            $this.MessageForConsole = ('{0}{1} | {2}' -f $Separator, $this.Target, $this.Message)
        }
    }

    PSLogEntry (
        [string]$Target,
        [string]$Severity,
        [System.Management.Automation.EngineIntrinsics]$ExecCon,
        $ChangeObject
    ) {
        $this.LogSettings = (Get-PSLogSettings)
        $this.Target = $Target
        $this.Severity = $Severity
        $this.ChangeObject = $ChangeObject
    }

    PSLogEntry (
        [string]$Message,
        [string]$Severity,
        [System.Management.Automation.EngineIntrinsics]$ExecCon
    ) {
        $this.LogSettings = Get-PSLogSettings
        $this.Message = $Message
        $this.Target = $this.LogSettings.DefaultTargetValue
        $this.TimeStamp = (Get-Date)
        $this.Severity = $Severity
        $this.Source = $this.FindSource()
        $this.UpdateMessage()
    }

    PSLogEntry (
        [string]$Message,
        [string]$Severity,
        [string]$Target,
        [System.Management.Automation.EngineIntrinsics]$ExecCon
    ) {
        $this.LogSettings = Get-PSLogSettings
        $this.Message = $Message
        $this.Target = $Target
        $this.TimeStamp = (Get-Date)
        $this.Severity = $Severity
        $this.Source = $this.FindSource()
        $this.UpdateMessage()
    }
    PSLogEntry (
        [string]$Message,
        [string]$Severity,
        [System.Management.Automation.ErrorRecord]$ErrorRecord,
        [System.Management.Automation.EngineIntrinsics]$ExecCon
    ) {
        $this.LogSettings = Get-PSLogSettings
        $this.ErrorRecord = $ErrorRecord
        $this.Message = $Message
        $this.Target = $this.LogSettings.DefaultTargetValue
        $this.TimeStamp = (Get-Date)
        $this.Severity = $Severity
        $this.Source = $this.FindSource()
        $this.UpdateMessage()
    }

    PSLogEntry (
        [string]$Message,
        [string]$Severity,
        [string]$Target,
        [System.Management.Automation.ErrorRecord]$ErrorRecord,
        [System.Management.Automation.EngineIntrinsics]$ExecCon
    ) {
        $this.LogSettings = Get-PSLogSettings
        $this.ErrorRecord = $ErrorRecord
        $this.Message = $Message
        $this.Target = $Target
        $this.TimeStamp = (Get-Date)
        $this.Severity = $Severity
        $this.Source = $this.FindSource()
        $this.UpdateMessage()
    }
}

class PSLogSettings {
    [string]$DefaultTargetValue = 'N/A'
    [string]$LogFileTurnOver = 'Daily'
    [string]$LogDirectory
    [string]$LogNamePrefix = ''
    [string]$LogName = '{0}.log' -f (Get-Date).ToString('yyyy-MM-dd')
    [string]$ChangeLogName = '{0}_ChangeLog.log' -f (Get-Date).ToString('yyyy-MM-dd')
    [string]$LogDelimiter = ';'
    [string]$Encoding = 'UTF8'


    [string]GetLogName() {
        return $this.LogNamePrefix + $this.LogName
    }

    [string]GetChangeLogName() {
        return $this.LogNamePrefix + $this.ChangeLogName
    }

    [string]GetLogFullName() {
        return (Join-Path -Path $this.LogDirectory -ChildPath $this.GetLogName())
    }

    [string]GetChangeLogFullName() {
        return (Join-Path -Path $this.LogDirectory -ChildPath $this.GetChangeLogName())
    }

    PSLogSettings(
        $LogDirectory
    ) {
        $this.LogDirectory = $LogDirectory
    }
}

function Get-PSLogSettings {
    [Cmdletbinding()]
    param()

    process {
        Get-Variable -Name (Get-PSLogSettingsVarName) -ValueOnly
    }
}

function Get-PSLogSettingsVarName {
    return ('LogSettings')
}

function Test-PSLogSettings {
    if (Get-PSLogSettings -ErrorAction SilentlyContinue) {
        return $true
    } else {
        return $false
    }
}

function Initialize-PSLogSettings {
    if (-not (Test-PSLogSettings)) {
        if (Test-PSLogDirectoryPath) {
            $null = New-Variable -scope script -Name (Get-PSLogSettingsVarName) -Value ([PSLogSettings]::New((Resolve-Path -Path "$PSScriptRoot\..\logs"))) -PassThru -Force
        } else {
            $null = New-Variable -scope script -Name (Get-PSLogSettingsVarName) -Value ([PSLogSettings]::New((Resolve-Path -Path "$PSScriptRoot"))) -PassThru -Force
        }
    }
}

function Test-PSLogDirectoryPath {
    if (Test-Path -Path "$PSScriptRoot\..\logs") {
        return $true
    } else {
        return $false
    }
}

function Set-PSLogDirectory {
    param(
        [Parameter(Mandatory)]
        [string]
        $LogDirectory
    )
    if (Test-PSLogSettings) {
        $LogSettings = Get-PSLogSettings
        $LogSettings.LogDirectory = $LogDirectory
        Set-Variable -Name (Get-PSLogSettingsVarName) -Value $LogSettings
    } else {
        Initialize-PSLogSettings
        Set-PSLogDirectory -LogDirectory $LogDirectory
    }
}

function Write-Warning {
    <#
        .ForwardHelpTargetName Microsoft.PowerShell.Utility\Write-Warning
        .ForwardHelpCategory Cmdlet
    #>

    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113430', RemotingCapability = 'None', DefaultParameterSetName = 'Plain')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'The sole purpose of this function is do override the default behaviour')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Alias('Msg')]
        [AllowEmptyString()]
        [string]
        ${Message},

        [Parameter(Position = 1)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 2)]
        [switch]
        $File

    )

    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Write-Warning', [System.Management.Automation.CommandTypes]::Cmdlet)
            
            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState 

            # Create PSLogEntry object
            if ($Target) {
                $PSLogEntry = [PSLogEntry]::New($Message, 'WARNING', $Target, $ExecutionContext)
            } else {
                $PSLogEntry = [PSLogEntry]::New($Message, 'WARNING', $ExecutionContext)
            }

            # Write to file if selected
            if ($File) {
                $PSLogEntry.WriteToFile()
            }

            # Cleanup PSBoundParameters
            $PSBoundParameters.Remove('Message') | Out-Null
            $PSBoundParameters.Add('Message', $PSLogEntry.MessageForConsole) | Out-Null
            $PSBoundParameters.Remove('Target') | Out-Null
            $PSBoundParameters.Remove('File') | out-null

            # Invoke stock cmdlet
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }

            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }

    process {
        try {
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }

    end {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
}

function Write-Error {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'The sole purpose of this file')]
    [CmdletBinding(DefaultParameterSetName = 'NoException', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=2097039', RemotingCapability = 'None')]
    param(
        [Parameter(ParameterSetName = 'WithException', Mandatory = $true)]
        [System.Exception]
        ${Exception},

        [Parameter(ParameterSetName = 'WithException')]
        [Parameter(ParameterSetName = 'NoException', Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Parameter(ParameterSetName = 'ErrorRecord')]
        [Alias('Msg')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        ${Message},

        [Parameter(ParameterSetName = 'ErrorRecord', Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]
        ${ErrorRecord},

        [Parameter(ParameterSetName = 'NoException')]
        [Parameter(ParameterSetName = 'WithException')]
        [System.Management.Automation.ErrorCategory]
        ${Category},

        [Parameter(ParameterSetName = 'NoException')]
        [Parameter(ParameterSetName = 'WithException')]
        [string]
        ${ErrorId},

        [Parameter(ParameterSetName = 'NoException')]
        [Parameter(ParameterSetName = 'WithException')]
        [Parameter(ParameterSetName = 'ErrorRecord')]
        [System.Object]
        ${TargetObject},

        [string]
        ${RecommendedAction},

        [Alias('Activity')]
        [string]
        ${CategoryActivity},

        [Alias('Reason')]
        [string]
        ${CategoryReason},

        [Alias('TargetName')]
        [string]
        ${CategoryTargetName},

        [Alias('TargetType')]
        [string]
        ${CategoryTargetType},
    
        [switch]
        $File
    )

    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState 

            #$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Write-Error', [System.Management.Automation.CommandTypes]::Cmdlet)

            if ($Exception) {
                $ErrorRecord = New-Object Management.Automation.ErrorRecord $Exception, 'CustomErrorRecord', 'NotSpecified' , 'N/A'
            }

            # Create PSLogEntry object
            if ($TargetObject -and $TargetObject -is [string]) {
                if ($ErrorRecord) {
                    $PSLogEntry = [PSLogEntry]::New($Message, 'ERROR', $TargetObject, $ErrorRecord, $ExecutionContext)
                } else {
                    $PSLogEntry = [PSLogEntry]::New($Message, 'ERROR', $TargetObject, $ExecutionContext)
                }
            } else {
                if ($ErrorRecord) {
                    $PSLogEntry = [PSLogEntry]::New($Message, 'ERROR', $ErrorRecord, $ExecutionContext)
                } else {
                    $PSLogEntry = [PSLogEntry]::New($Message, 'ERROR', $ExecutionContext)
                }
            }

            if ($File) {
                $PSLogEntry.WriteToFile()
            }

            if ($Message) {
                $PSLogEntry.WriteToConsole()
            } elseif ($ErrorRecord) {
                $ErrorRecord
            }

            if (-not $Message -and -not $ErrorRecord) {
                $scriptCmd = { & $wrappedCmd @PSBoundParameters }
                $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
                $steppablePipeline.Begin($PSCmdlet)
            }

        } catch {
            throw
        }
    }

    process {
        try {
            #$steppablePipeline.Process($_)
        } catch {
            throw
        }
    }

    end {
        try {
            #$steppablePipeline.End()
        } catch {
            throw
        }
    }
    <#
 
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Write-Error
.ForwardHelpCategory Cmdlet
 
#>

}

function Write-Success {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [string]
        $Message,

        [Parameter(Position = 1)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 2)]
        [switch]
        $File

    )

    process {

        # Create PSLogEntry object
        if ($Target) {
            $PSLogEntry = [PSLogEntry]::New($Message, 'SUCCESS', $Target, $ExecutionContext)
        } else {
            $PSLogEntry = [PSLogEntry]::New($Message, 'SUCCESS', $ExecutionContext)
        }

        # Write to file if selected
        if ($File) {
            $PSLogEntry.WriteToFile()
        }

        # Write to console
        $PSLogEntry.WriteToConsole()

    }
}

function Write-Skip {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [string]
        $Message,

        [Parameter(Position = 1)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 2)]
        [switch]
        $File

    )

    process {

        # Create PSLogEntry object
        if ($Target) {
            $PSLogEntry = [PSLogEntry]::New($Message, 'SKIP', $Target, $ExecutionContext)
        } else {
            $PSLogEntry = [PSLogEntry]::New($Message, 'SKIP', $ExecutionContext)
        }

        # Write to file if selected
        if ($File) {
            $PSLogEntry.WriteToFile()
        }

        # Write to console
        $PSLogEntry.WriteToConsole()
    }
}

function Write-Information {
    <#
 
      .ForwardHelpTargetName Microsoft.PowerShell.Utility\Write-Information
      .ForwardHelpCategory Cmdlet
 
  #>


    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkId=525909', RemotingCapability = 'None')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'The sole purpose of this function is do override the default behaviour')]
    param(
        [Parameter(Mandatory, Position = 0)]
        [Alias('Msg')]
        [Alias('MessageData')]
        [System.Object]
        ${Message},

        [Parameter(Position = 1)]
        [string[]]
        ${Tags},

        [Parameter(Position = 2)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 3)]
        [switch]
        $File
    
    )

    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }

            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState 

            # Create PSLogEntry object
            if ($Target) {
                $PSLogEntry = [PSLogEntry]::New($Message, 'INFO', $Target, $ExecutionContext)
            } else {
                $PSLogEntry = [PSLogEntry]::New($Message, 'INFO', $ExecutionContext)
            }

            # Write to file if selected
            if ($File) {
                $PSLogEntry.WriteToFile()
            }

            $PSLogEntry.WriteToConsole()

        } catch {
            throw
        }
    }
}

function Write-Verbose {
    <#
 
      .ForwardHelpTargetName Microsoft.PowerShell.Utility\Write-Verbose
      .ForwardHelpCategory Cmdlet
 
  #>

    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113429', RemotingCapability = 'None')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'The sole purpose of this function is do override the default behaviour')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)]
        [Alias('Msg')]
        [string]
        ${Message},

        [Parameter(Position = 1)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 2)]
        [switch]
        $File
    )

    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Write-Verbose', [System.Management.Automation.CommandTypes]::Cmdlet)

            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState 

            # Create PSLogEntry object
            if ($Target) {
                $PSLogEntry = [PSLogEntry]::New($Message, 'VERBOSE', $Target, $ExecutionContext)
            } else {
                $PSLogEntry = [PSLogEntry]::New($Message, 'VERBOSE', $ExecutionContext)
            }

            # Write to file if selected
            if ($File) {
                $PSLogEntry.WriteToFile()
            }

            # Cleanup PSBoundParameters
            $PSBoundParameters.Remove('Message') | Out-Null
            $PSBoundParameters.Add('Message', $PSLogEntry.MessageForConsole) | Out-Null
            $PSBoundParameters.Remove('Target') | Out-Null
            $PSBoundParameters.Remove('File') | out-null

            # Invoke stock cmdlet
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }

    process {
        try {
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }

    end {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
}

function Write-Debug {
    <#
 
      .ForwardHelpTargetName Microsoft.PowerShell.Utility\Write-Debug
      .ForwardHelpCategory Cmdlet
 
  #>


    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113424', RemotingCapability = 'None')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'The sole purpose of this function is do override the default behaviour')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)]
        [Alias('Msg')]
        [string]
        ${Message},

        [Parameter(Position = 1)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 2)]
        [switch]
        $File
    )
    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Write-Debug', [System.Management.Automation.CommandTypes]::Cmdlet)

            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState 

            # Create PSLogEntry object
            if ($Target) {
                $PSLogEntry = [PSLogEntry]::New($Message, 'DEBUG', $Target, $ExecutionContext)
            } else {
                $PSLogEntry = [PSLogEntry]::New($Message, 'DEBUG', $ExecutionContext)
            }

            # Write to file if selected
            if ($File) {
                $PSLogEntry.WriteToFile()
            }
            # Cleanup PSBoundParameters
            $PSBoundParameters.Remove('Message') | Out-Null
            $PSBoundParameters.Add('Message', $PSLogEntry.MessageForConsole) | Out-Null
            $PSBoundParameters.Remove('Target') | Out-Null
            $PSBoundParameters.Remove('File') | out-null

            # Invoke stock cmdlet
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }

            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }

    process {
        try {
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }

    end {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
}

function Write-Change {
    param (
        [Parameter(Position = 0, Mandatory)]
        [Alias('TargetObject')]
        [string]
        $Target,

        [Parameter(Position = 1, Mandatory, ParameterSetName = 'Add')]
        [switch]
        $Add,
        [Parameter(Position = 1, Mandatory, ParameterSetName = 'Remove')]
        [switch]
        $Remove,
        [Parameter(Position = 1, Mandatory, ParameterSetName = 'Replace')]
        [switch]
        $Replace,
        [Parameter(Position = 1, Mandatory, ParameterSetName = 'Clear')]
        [switch]
        $Clear,
        [Parameter(Position = 1, Mandatory, ParameterSetName = 'Move')]
        [switch]
        $Move,

        [Parameter(Mandatory, ParameterSetName = 'Add')]
        [Parameter(Mandatory, ParameterSetName = 'Remove')]
        [Parameter(Mandatory, ParameterSetName = 'Replace')]
        [Parameter(Mandatory, ParameterSetName = 'Move')]
        [string]
        $Value,

        [Parameter(Mandatory, ParameterSetName = 'Replace')]
        [Parameter(Mandatory, ParameterSetName = 'Clear')]
        [Parameter(Mandatory, ParameterSetName = 'Move')]
        [string]
        $PreviousValue,

        [string]
        $Property = '',

        [Parameter(Mandatory)]
        [ValidateSet('Success', 'Failed', 'Unchanged')]
        [string]
        $Result,

        [switch]
        $WriteToConsole
    )

    $LogSettings = (Get-PSLogSettings)

    $Object = [pscustomobject]@{
        TimeStamp     = (Get-Date).ToString('yyyy-MM-dd_HH:mm:ss:fff')
        Target        = $Target 
        Operation     = $PSCmdlet.ParameterSetName
        Result        = $Result
        Property      = $Property
        PreviousValue = $PreviousValue
        Value         = $Value
    }

    $Object | Export-CSV -Path $LogSettings.GetChangeLogFullName() -Delimiter $LogSettings.LogDelimiter -NoTypeInformation -Encoding $LogSettings.Encoding -Append
    
    if ($WriteToConsole) {
        $PSLogEntry = [PSLogEntry]::New($Message, 'CHANGE', $ExecutionContext, $Object)
        $PSLogEntry.WriteToConsole()
    }
}

function Write-PSProgress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]     
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,        
        
        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,
        
        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')] 
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )
    
    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue)) {
        # Remove the global starttime variable if the Completed switch parameter is users
        try {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        } catch {
            throw $_
        }
    } elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue)) {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime) {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        } else {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    } else {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }
    
    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName) {
        'Standard' {
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total)) {
                
                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)
                if ($Percent -gt 100) { $Percent = 100 }
                $WriteProgressSplat = @{
                    Activity = $Activity
                    PercentComplete = $Percent
                    CurrentOperation = $Target
                }
                if ($Id) { $WriteProgressSplat.Id = $Id }
                if ($ParentId) { $WriteProgressSplat.ParentId = $ParentId }

                # Calculations for either timestats and without
                if ($NoTimeStats) {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                } else {
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds
                    $SecondsPerItem = if ($Counter -eq 0) { 0 } else { ($TotalSeconds / $Counter) }
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed' {
            Write-Progress -Activity $Activity -Id $Id -Completed 
        }
    }
}

function Write-Task {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]
        $Message,

        [Parameter(Position = 1)]
        [switch]
        $File
    )

    # Create PSLogEntry object
    $PSLogEntry = [PSLogEntry]::New($Message, 'TASK', $ExecutionContext)

    # Write to file if selected
    if ($File) {
        $PSLogEntry.WriteToFile()
    }

    # Write to console
    $PSLogEntry.WriteToConsole()

}

function Write-CustomLogMessage {
    param(
        [Parameter(Mandatory)]
        [string]
        $Header,

        [System.ConsoleColor]
        $HeaderColor = [consolecolor]::Green,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]
        $Message,

        [System.ConsoleColor]
        $MessageColor = [consolecolor]::Gray,

        [char]
        $SeparatorChar = '|',

        [System.ConsoleColor]
        $SeparatorColor = [consolecolor]::Cyan
    )

    Write-Host ('{0}' -f $Header) -NoNewline -ForegroundColor $HeaderColor
    Write-host (' {0} ' -f $SeparatorChar) -NoNewline -ForegroundColor $SeparatorColor
    Write-Host ('{0}' -f $Message) -ForegroundColor $MessageColor

}

function Use-CallerPreference {
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
  
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
  
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
  
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
      
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
  
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
  
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
  
    .LINK
    about_Preference_Variables
  
    .LINK
    about_CommonParameters
  
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
  
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
  
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
  
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [Management.Automation.SessionState]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        $SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
        'ErrorActionPreference' = 'ErrorAction';
        'DebugPreference'       = 'Debug';
        'ConfirmPreference'     = 'Confirm';
        'InformationPreference' = 'InformationAction';
        'VerbosePreference'     = 'Verbose';
        'WarningPreference'     = 'WarningAction';
        'WhatIfPreference'      = 'WhatIf';
    }

    foreach ( $prefName in $commonPreferences.Keys ) {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if ( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if ( -not $variable ) {
            continue
        }

        if ( $SessionState -eq $ExecutionContext.SessionState ) {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        } else {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}

function Write-CheckListItem {
    param (
        $InfoChar = 'O',
        $InfoColor = 'White',
        $PositiveChar = '+',
        $PositiveColor = 'Green',
        $IntermediateChar = '/',
        $IntermediateColor = 'Yellow',
        $NegativeChar = '-',
        $NegativeColor = 'Red',
        [ValidateSet('Positive', 'Intermediate', 'Negative', 'Info')]
        $Severity = 'Info',
        $Message = '',
        $Milliseconds
    )

    switch ($Severity) {
        'Positive' {
            $SelectedColor = $PositiveColor
            $SelectedChar = $PositiveChar
        }
        'Intermediate' {
            $SelectedColor = $IntermediateColor
            $SelectedChar = $IntermediateChar
        }
        'Negative' {
            $SelectedColor = $NegativeColor
            $SelectedChar = $NegativeChar
        }
        'Info' {
            $SelectedColor = $InfoColor
            $SelectedChar = $InfoChar
        }
    }
    if ($Milliseconds) {
        Write-Host (' [{0}] {1} ' -f $SelectedChar, $Message) -ForegroundColor $SelectedColor -NoNewline; Write-Host (' {0}ms' -f ([Math]::Round($Milliseconds))) -ForegroundColor DarkGray
    } else {
        Write-Host (' [{0}] {1} ' -f $SelectedChar, $Message) -ForegroundColor $SelectedColor
    }
}
#endregion

#region Assert Functions
filter Assert-FolderExists {
    $exists = Test-Path -Path $_ -PathType Container
    if (!$exists) { 
        Write-Warning "$_ did not exist. Folder created."
        $null = New-Item -Path $_ -ItemType Directory 
    }
}

filter Assert-FunctionRequirements {
    param(
        [string[]]
        $InstalledModules,

        [ValidateSet('Core','Desktop')]
        [string]
        $PowershellEdition,

        [ValidateSet('5.0','5.1','6.0','7.0')]
        [string]
        $PowershellVersionOrHigher,

        [string[]]
        $CmdletWhiteList,

        [switch]
        $Elevated,

        [switch]
        $NonElevated
    )

    $Result = $true

    foreach ($Key in $PSBoundParameters.Keys) {
        switch ($Key) {
            'InstalledModules' {
                foreach ($Module in $InstalledModules) {
                    if (-not ((Get-ChildItem -Path ((Get-ModuleConfiguration).ModuleFolders.Include) -Filter $Module) -or (Get-Module $Module -ListAvailable))) {
                        $Result = $false
                        Write-Error -Message ('Module availability failed for module [{0}]' -f $Module)
                    }
                }
            }
            'PowershellEdition' {
                if ($PSVersionTable.PSEdition -ne $PowershellEdition) {
                    $Result = $false
                    Write-Error -Message ('This cmdlet require PSEdition [{0}]' -f $PowershellEdition)
                }
            }
            'PowershellVersionOrHigher' {
                if ($PSVersionTable.PSVersion -lt ([system.version]$PowershellVersionOrHigher)) {
                    $Result = $false
                    Write-Error -Message ('This cmdlet require PSVersion [{0}] or higher' -f $PowershellVersionOrHigher)
                }
            }
            'Elevated' {
                if (-not (([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
                    $Result = $false
                    Write-Error -Message ('This cmdlet requires an elevated host')
                }                
            }
            'NonElevated' {
                if ((([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
                    $Result = $false
                    Write-Error -Message ('This cmdlet requires an elevated host')
                }                
            }
        }
    }
    return $Result
}
#endregion

#region Other helper functions
filter Invoke-GarbageCollect {
    [system.gc]::Collect()
}
#endregion